前言
這篇文章會講在 onCreate 中通過getWidth()和getMeasuredWidth()拿不到 View 的寬度和高度的原因,以及如何拿到的三種方法。
如果想了解原理,建議在看這篇文章之前先看一下這篇文章Android源碼分析之界面的構成和創建
原因
getMeasuredWidth
先來看getMeasuredWidth方法是怎麼獲取View的寬的:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
可以看到getMeasuredWidth的返回值是由mMeasuredWidth決定的,mMeasuredWidth是在那賦值的呢:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
通過上面的代碼可以看到mMeasuredWidth是在onMeasure中賦值的,先記住這個結論。
getWidth
public final int getWidth() {
return mRight - mLeft;
}
getWidth的返回值是由mRight和mLeft的差值決定的,右邊的座標減去左邊的座標就等於他的寬,猜測這可能是他在Layout中測量的座標,那麼我們來看一下他們是在哪賦值的:
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
}
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
return changed;
}
可以看到他們確實是在layout方法中賦值的,他們就是這個View的座標。
分析
由於界面繪製是在Activity生命週期的onResume之後開始的,所以在onCreate中給寬高賦值的onMeasure和layout方法還沒有被調用,這個時候當然獲取不到值。關於爲什麼界面繪製是在Activity生命週期的onResume之後可以看我開頭推薦的文章。
方法
1. View.post()
button.post(new Runnable() {
@Override
public void run() {
button.getWidth();
button.getMeasuredWidth();
}
});
我們知道MessageQueue是按順序處理消息的,就是先插入的先處理,那麼我們在onCreate中post一個消息爲什麼會比onResume之後執行的繪製任務還晚執行呢?實際上我們post的消息並不會立即執行,來看一下post的源碼:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
在開始繪製之前attachInfo爲空,所以運行的是getRunQueue().post(action):
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
我們的消息放到了mRunQueue這個隊列中進行緩存,那麼什麼時候執行呢?
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
dispatchAttachedToWindow這個方法是在消息隊列中繪製消息開始執行時調用的,所以這時候添加的消息會在繪製完成後執行,dispatchAttachedToWindow方法首先給mAttachInfo賦值,然後通過executeActions方法postDelayed之前緩存的消息,這樣之前post緩存的消息就會在View繪製完成後調用,就能獲取正真的寬高。
2. IdleHandler
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
button.getWidth();
button.getMeasuredWidth();
return false;
}
});
關於IdleHandler的介紹看這篇文章 IdleHandler 相關原理淺析
大致的意思就是添加到這個隊列的消息會在當前線程消息隊列空閒時執行,由於android系統是消息驅動的程序,Activity的所有生命週期都是通過handler來執行的,包括界面的繪製也是,所以在Activity創建和界面繪製之前主線程的handler都不會空閒,這樣也就能夠在IdleHandler中獲取View的寬和高了。
3. ViewTreeObserver
button.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
button.getViewTreeObserver().removeOnGlobalLayoutListener(this);
button.getWidth();
button.getMeasuredWidth();
}
});
ViewTreeObserver是用來監聽View的一些事件,其中有很多監聽,OnGlobalLayoutListener是在界面繪製完成的監聽,具體的原理看這篇文章安卓 ViewTreeObserver源碼分析