measure(测量)过程是View的工作流程中最开始、最核心的过程,在这个过程中负责确定View的测量宽/高。
对于View和ViewGroup,measure过程有不同的执行方法:如果目标是一个原始的View,那么通过measure过程就完成了其测量过程;如果是一个ViewGroup,那么除了完成自己的测量过程外,还会遍历调用所有子元素的measure过程,每个子元素又要递归地执行这个过程。
下面针对这两种情况分别进行讨论。
1、View的measure过程
View的measure过程的源头是从 ViewRootImpl 类的 performTraversals() 方法开始的,在performTraversals()方法中调用 performMeasure() 方法,performMeasure()方法中调用View类的 measure() 方法,measure()方法调用View类的 onMeasure() 方法。View类的onMeasure()方法的源码如下:
/** * 测量View及其内容,决定其宽度和高度; * 这个方法被View类中的measure(int, int)方法调用; * View的子类应该重写这个方法以提供更精准、更高效的测量数据; * 调用这个方法时,必须调用setMeasuredDimension(int, int)方法存储这个View的测量宽高; * 如果不调用setMeasuredDimension()方法,会触发IllegalStateException异常; * View类的measure()方法默认返回的是这个控件的背景的尺寸; * View的子类应该重写onMeasure(int, int)方法来更好的测量其内容; * 如果这个方法被重写,子类必须保证测量得到的宽度和高度都最小是这个View的最小宽高 * (最小宽高通过getSuggestedMinimumHeight()和getSuggestedMinimumWidth()得到)。 * * @param widthMeasureSpec 通过 MeasureSpec 打包的,父容器提供的宽度尺寸 * @param heightMeasureSpec 通过 MeasureSpec 打包的,父容器提供的高度尺寸 */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
上面代码中提到了两个方法: getDefaultSize() 和 getSuggestedMinimumXXX() 。先来看getDefaultSize()方法,源码如下:
/** * 返回默认尺寸的工具方法; * 如果MeasureSpec没有施加任何约束,则返回系统提供的尺寸(最小尺寸); * 如果MeasureSpec施加了约束,则会返回一个由MeasureSpec主导的尺寸。 * * @param size 这个View的默认尺寸 * @param measureSpec 从父容器传递过来的尺寸约束(MeasureSpec) * @return 返回这个View应有的尺寸 */ public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = View.MeasureSpec.getMode(measureSpec); int specSize = View.MeasureSpec.getSize(measureSpec); switch (specMode) { case View.MeasureSpec.UNSPECIFIED: result = size; break; case View.MeasureSpec.AT_MOST: case View.MeasureSpec.EXACTLY: result = specSize; break; } return result; }
可以看到,只要我们给View设置了尺寸(无论是MATCH_PARENT、WRAP_CONTENT,还是一个具体的值),getDefaultSize()方法都会原封不动的返回这个值;而如果我们不设置值,就会返回这个View的默认大小。从这一点,我们可以得到如下结论:如果一个自定义控件继承自View类,那么就一定要在它的onMeasure()方法中设置wrap_content时的大小,否则就必须要在使用这个自定义控件的时候将尺寸都设置为固定大小或match_parent。这么做的原因从 getDefaultSize() 方法的源码中就可以看到:wrap_content反应到SpecMode中是 AT_MOST ,返回的就是测量的尺寸值specSize,如果我们不在onMeasure()方法中设置这个值,那么就会直接返回父容器的值(这一点可以参考【这篇文章】最后的表格)。
再来看getSuggestedMinimumXXX()方法,这里以getSuggestedMinimumWidth()方法为例,看下源码:
/** * 返回当前View应有的、系统建议使用的最小宽度; * 返回的是 系统建议的最小宽度 和 背景的最小宽度 之间的较大值; * 当在onMeasure(int, int)方法中调用时,调用者仍然要保证返回的宽度在父容器要求的范围内。 * * @return 返回系统建议这个View使用的最小宽度 */ protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
从源码中可以看出这个方法的逻辑:如果这个View没有设置背景,那么返回mMinWidth的值(这个值对应的是 android:minWidth 属性指定的值),这个值可以为0;如果这个View设置了背景,则返回mMinWidth和背景的最小宽度(就是Drawable的原始宽度,可能是0)这两者之间的较大值。如果onMeasure()方法中返回的宽或高的测量模式是UNSPECIFIED,则返回的就是这个方法返回的值。
注意:在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,这种情况下,在onMeasure()方法中拿到的测量值可能是不准确的。一个比较好的习惯是在 onLayout() 方法中去获取。
2、ViewGroup的measure过程
对于ViewGroup来说,除了完成自己本身的measure过程以外,还要遍历调用所有子元素的measure过程,各个子元素再递归地去执行这个过程。
ViewGroup是一个抽象类,不同的ViewGroup的子类有不同的布局特性,因此,ViewGroup中没有onMeasure()方法,onMeasure()方法都被下放到了ViewGroup的子类中去单独实现。这样,ViewGroup只需要递归地让所有子元素都对自己进行测量,就完成了View树的measure过程。因此,ViewGroup中提供了一个 measureChildren() 方法,其源码如下:
/** * 让这个ViewGroup中的所有子元素都测量它们自身(将MeasureSpec的要求和padding都计算入内); * 如果某个子元素的显示状态为GONE,则跳过这个子元素不测量。 * * @param widthMeasureSpec 父容器对这个ViewGroup的宽度的要求 * @param heightMeasureSpec 父容器对这个ViewGroup的高度的要求 */ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
可以看到,在这个方法中,我们从ViewGroup中逐个取出子元素,并对那些可见的子元素调用 measureChild() 方法。measureChild()方法的源码如下:
/** * 让这个ViewGroup中的某一个子元素测量它自身(将MeasureSpec的要求和padding都计算入内) * * @param child 要测量的子元素 * @param parentWidthMeasureSpec 父容器对这个ViewGroup的宽度的要求 * @param parentHeightMeasureSpec 父容器对这个ViewGroup的高度的要求 */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final ViewGroup.LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
可以看到,在这个方法中,我们取出这个View的LayoutParams,然后通过 getChildMeasureSpec() 方法得到这个View的宽和高的MeasureSpec,最后将这个View的宽和高的MeasureSpec都交给 measure() 方法处理(这里的measure()方法就是View类中的measure()方法)。可以发现,转了一圈,原来ViewGroup的测量最终都是回到View类进行测量的。
#、获取测量值为0的解决方案
这个问题我在【这篇文章】中有提到过,就是在Activity的onCreate()方法中获取某个View的测量宽/高时通常都会返回0。实际上,在Activity的 onCreate() 、 onStart() 、 onResume() 生命周期函数中,都无法获取到View的测量值,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此我们无法保证Activity在执行onCreate()、onStart()或onResume()时,某个View已经测量完毕了。如果View还没有测量完毕,那么获得的宽/高就是0。
对于这个问题,有以下三种解决方案。
(1)Activity/View # onWindowFocusChanged()
onWindowFocusChanged() 这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,在这个时候去获取宽高是没有问题的。需要注意的是,这个方法在Activity获取焦点和失去焦点的时候分别会调用一次,也就是说在Activity生命周期函数的 onResume() 和 onPause() 方法被调用时,这个方法也会被调用。典型代码如下:
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
(2)view.post(runnable)
通过 post() 方法可以将一个Runnable对象投递到消息队列的尾部,等到Looper调用次Runnable的时候,View也已经测量完成了。典型代码如下:
@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
(3)ViewTreeObserver
ViewTreeObserver 是视图树的观察者,用来监听视图树的全局变化,包括视图树的布局变化、触摸状态改变等。在这个问题的解决中,我们使用的是 ViewTreeObserver.OnGlobalLayoutListener 这个监听器接口,当视图树的状态发生改变或树中某个View的可见性发生改变时,接口中的 onGlobalLayout() 方法将被回调。需要注意的是,伴随着视图树状态的改变,onGlobalLayout()方法会被回调多次。典型代码如下:
@Override protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }