View工作原理 | measure

605 阅读6分钟

前言

前面俩篇文章分别介绍了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过程就说完了,总体来看还是蛮简单的,流程如下:

image.png

其中调用View的measure方法开始,在onMeasure回调方法中通过setMeasureDimension方法来完成设置真正的宽高

ViewGroup的measure过程

对于ViewGoup来说,除了需要完成它自己的measure过程以外,还需要遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。

这就需要在具体ViewGroup的实现类中进行具体逻辑处理,所以在ViewGoup中的onMeasure是没有实现的。下面我们以LinearLayout这个布局来举例说明。

大致流程如下:

image.png

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有没有已经测量完毕。

这里提供几个方法来解决这个问题:

  1. Activity/View#onWindowFocusChanged

该回调的含义就是View已经初始化完毕了,宽高已经准备好了,这时去获取宽高是没问题的。但是需要注意的是,onWindowFocusChanged会被回调多次,当Activity的Window获得和失去焦点都会被调用。典型代码如下:

override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    if (hasFocus){
        val width = customView.width
    }
}
  1. view.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable时,View已经初始化好了。典型代码如下:

override fun onStart() {
    super.onStart()
    customView.post {
        val width = customView.width
    }
}
  1. 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俩种不同情况的测量过程。

笔者水平有限,如果有错误,欢迎评论指正。