前言
前面俩篇文章分别介绍了ViewRootImpl类是如何开始View的测量、布局和绘制流程,以及View的测量的关键类:MeasureSpec的介绍和创建规则。
本篇文章就来梳理一下View的测量过程,我们直接从ViewRootImpl中调用DecorView的measure方法开始,前面就不说了,前面文章有介绍,本篇文章只需要知道测量开始是调用View的measure方法。
正文
measure过程要分情况看,如果只是一个原始的View,那么通过measure方法便可以完成测量过程;但是对于一个ViewGoup来说,除了需要完成自己的测量过程外,还需要去遍历完成所有子元素的测量过程。所以下面针对俩种情况分别讨论。
View的measure过程
View的measure方法是一个final类型的方法,说明子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,方法如下:
//measure方法内部调用
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这里的widthMeasureSpec和heightMeasureSpec就是别处调用measure方法的俩个参数,上面方法实现很短,但是非常重要,我们依次分析。
setMeasureDimension方法
会发现在上面函数内部会调用这个方法,我们来看一下这个函数的源码定义:
<p>This method must be called by {@link #onMeasure(int, int)} to store the
* measured width and measured height. Failing to do so will trigger an
* exception at measurement time.</p>
这里说这个方法必须在onMeasure中调用,它的作用就是设置测量的宽和高;该方法的参数,就是会被真正设置到View的测量宽高。
getDefaultSize方法
现在我们就接着看一下getDefaultSize方法,通过该方法获取真正测量宽高:
//这里第一个参数只在测量模式为UNSPECIFIED时使用,所以先不分析
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//会发现不论是AT_MOST还是EXACTLY,都是使用measureSpec的size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
该方法的调用第一个参数是建议值,第二个就是MeasureSpec,可以从代码实现发现,只有当模式是UNSPECIFIIED时,才使用建议值,否则都是使用MeasureSpec的值。
至于建议值即getSuggestedMininumWidth方法,它的逻辑如下:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这俩者中的最大值。
getSuggestedMininumWidth的返回值只会用在View在UNSPECIFIED情况下,而该模式是系统内部才使用,所以我们一般不用考虑。
wrap_content失效问题
从默认的getDefault方法可以看到,当是EXACTLY和AT_MOST模式时,其返回值都是获取的是MeasureSpec的值。
所以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
这是为什么呢,因为当View设置了wrap_content,其specMode会是AT_MOST模式,在该模式下其specSize是等于父View的specSize,而父View的specSize就是当前View容器可用大小,所以效果和设置match_parent是一样的。
而解决方法也比较简单,对于wrap_content做特殊处理,代码如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight)
}else if (widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSize)
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,mHeight)
}
}
这里只需要判断spec是AT_MOST时,给设置一个默认值即可。
到这里,View的measure过程就说完了,总体来看还是蛮简单的,流程如下:
其中调用View的measure方法开始,在onMeasure回调方法中通过setMeasureDimension方法来完成设置真正的宽高。
ViewGroup的measure过程
对于ViewGoup来说,除了需要完成它自己的measure过程以外,还需要遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。
这就需要在具体ViewGroup的实现类中进行具体逻辑处理,所以在ViewGoup中的onMeasure是没有实现的。下面我们以LinearLayout这个布局来举例说明。
大致流程如下:
LinearLayout的measure过程
直接看LinearLayout的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我们选择看其中排列方向为竖直方向的情况,调用measureVertical方法,方法比较多,先看一段代码:
for (int i = 0; i < count; ++i) {
//测量Child
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
...
}
在这里会遍历child,调用measureChildBeforeLayout方法把measure过程传递给child,并且系统用mTotalLength变量来表示在竖直方向的初始高度,每测量完一个元素,mTotalLength的值就会增加,增加部分包括子元素的高度以及子元素在竖直方向的margin等。
当所有子元素都测量完后,LinearLayout就可以设置其自己的大小了,这时就需要看LinearLayout自己的MeasureSpec了,假如设置的match_parent则就和父View一样大小;假如设置的为wrap_content,就需要判断这时的子View所有的高度是否高于父View,即不能超过父容器的剩余空间。
获取View的测量宽高
上面我们已经对View的measure过程进行了详细的分析,现在我们考虑一种情况,就是想在Activity已启动的时候就去获取某个View的宽/高。这时第一反应在onCreate或者onResume中去获取这个View的测量宽/高,但是实际上在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息。
之前我们可能无法理解,但是学习了前面的ViewRootImpl的原理以及Window的机制后,我们可以知道Activity的DecorView是在ActivityThread中handleResumeActivity方法中被添加到Window中的,而在这个阶段,会创建ViewRootImpl,ViewRootImpl一边加载View,一边和WMS通信,所以在Activity这几个回调中是无法得知View有没有已经测量完毕。
这里提供几个方法来解决这个问题:
- Activity/View#onWindowFocusChanged
该回调的含义就是View已经初始化完毕了,宽高已经准备好了,这时去获取宽高是没问题的。但是需要注意的是,onWindowFocusChanged会被回调多次,当Activity的Window获得和失去焦点都会被调用。典型代码如下:
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus){
val width = customView.width
}
}
- view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable时,View已经初始化好了。典型代码如下:
override fun onStart() {
super.onStart()
customView.post {
val width = customView.width
}
}
- ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生变化时,onGlobalLayout方法将被回调,所以这里是一个获取View的宽高一个好时机。
伴随着View树的状态改变,onGlobalLayout会被调用多次,典型代码如下:
override fun onStart() {
super.onStart()
customView.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val width = customView.width
}
})
}
总结
从前面俩篇文章我们了解了Android系统的View工作流程是由Window管理,而且是由ViewRootImpl负责三大步骤。本篇文章主要就是从DecorView的measure开始,分析了View和ViewGroup俩种不同情况的测量过程。
笔者水平有限,如果有错误,欢迎评论指正。