前言
这篇文章会讲在 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源码分析