Android View原理解析之测量流程(measure)

提示:本文的源码均取自Android 7.0(API 24)

前言

自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的测量流程进行讲解。相关内容如下:

  • Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
  • Android View原理解析之测量流程(measure)
  • Android View原理解析之布局流程(layout)
  • Android View原理解析之绘制流程(draw)

从View的角度看measure流程

在上一篇文章讲到整个视图树(ViewTree)的根容器是DecorView,ViewRootImpl通过调用DecorView的measure方法开启测量流程。measure是定义在View中的方法,我们就先从View的角度来看看测量过程中发生了什么。

首先来看一下measure方法中的逻辑,关键代码如下:

/**
 * This is called to find out how big a view should be. The parent
 * supplies constraint information in the width and height parameters.
 * 
 * 这个方法将调用onMeasure方法完成真正的测量工作
 * 因此View的派生类只需要也只能重写onMeasure方法完成布局逻辑
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    // ① 判断是否需要执行测量过程
    if (forceLayout || needsLayout) { 
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            
        	// ② 调用onMeasure方法,将在onMeasure方法中真正地设置自身的大小
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

可以看到measure是被final修饰的,说明View的子类是无法重写这个方法的,也就是说ViewGroup及其派生类调用的都是View中的measure方法。在这个方法中先是针对存在特殊边界的情况,对MeasureSpec进行了调整。随后在代码①的位置判断是否需要进行测量流程,最后在代码②的位置调用onMeasure方法。接下来我们继续看一下View#onMeasure方法,代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
    		getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

在onMeasure方法中调用了setMeasuredDimension方法,这个方法用于设置View测量后的宽高。我们通过View#getMeasuredWidthView#getMeasuredHeight获取的就是这个方法设置的值。这里的宽高都是通过getDefaultSize方法获取的,下来让我们来看看这个方法中都做了什么:

/**
 * Utility to return a default size. Uses the supplied size if the
 * MeasureSpec imposed no constraints. Will get larger if allowed
 * by the MeasureSpec.
 *
 * @param size View的默认宽度/高度
 * @param measureSpec 父容器传入的MeasureSpec
 * @return View最终的size(测量后的宽/高)
 */
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    // ① 对MeasureSpec进行解包
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

	// ② 根据测量模式决定最终的宽/高
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这个方法的逻辑很简单,首先在代码①的位置对传入的MeasureSpec进行解包操作,获取specSize和specMode。然后在代码②的位置判断测量模式(specMode),只有在测量模式为MeasureSpec.UNSPECIFIED时使用传入的默认大小,否则使用解包出来的specSize。这也说明默认情况下,View在测量模式为AT_MOST或EXACTLY时都会直接使用MeasureSpec中的宽/高。UNSPECIFIED一般是系统内部使用的测量模式,所以大部分情况下这个方法都会返回从MeasureSpec解包出来的specSize。

另外,这个方法中使用的默认大小(size)是通过getSuggestedMinimumWidth和getSuggestedMinimumHeight获得的,我们也来看一眼这两个方法中都做了什么:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}   

这两个方法的逻辑是相似的,这里仅分析getSuggestedMinimumWidth方法。首先会判断View是否存在背景(无非就是各种Drawable),如果不存在就直接返回mMinWidth,对应着XML中的android:minWidth属性;否则返回mMinWidth和背景最小宽度中的较大值。getMinimumWidth是Drawable中的方法,Drawable的子类都有自己的实现。

仅仅从View的角度来看,测量流程到此就结束了。因为不需要测量子View的大小,只需要确定自身的大小就行了。由此可见,如果我们想要通过继承View的方式实现自定义View,只需要重写onMeasure方法,并在这个方法中根据不同的情况为自己设置合适的宽/高,就可以保证测量流程正确进行。

从ViewGroup的角度看measure流程

ViewGroup需要承担测量子View的责任,而View#measure方法又是无法被重写的,那么可以很自然地想到去ViewGroup#onMeasure方法中寻找相应的测量逻辑。但是当我们兴致勃勃地在ViewGroup中寻觅时,会发现ViewGroup根本就没有重写onMeasure方法。

仔细想想也很正常,ViewGroup是一个抽象类,它的派生类们实现布局的方式也是多种多样,ViewGroup作为父类是无法提供一个统一的测量方案的。当然啦,ViewGroup确实也提供了很多方便测量的方法,下面我们就来一个一个地认识它们:

首先来看看ViewGroup#measureChild方法:

/**
 * Ask one of the children of this view to measure itself, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * The heavy lifting is done in getChildMeasureSpec.
 *
 * @param child The child to measure
 * @param parentWidthMeasureSpec The width requirements for this view
 * @param parentHeightMeasureSpec The height requirements for this view
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
	// ① 获取子View的LayoutParams
    final LayoutParams lp = child.getLayoutParams();

    // ② 生成测量子View需要的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    // ③ 调用子View的measure方法开始测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先在代码①的位置获取子View的LayoutParams,里面封装着子View对父容器的期望,也就是告诉父容器自己期望的宽高。在代码②的位置调用了getChildMeasure方法分别获取子View宽高对应的MeasureSpec。这里传入了3个参数,分别是父容器的MeasureSpec、父容器的左右/上下内间距以及子View的LayoutParams中封装的width/height。最后在代码③的位置调用子View的measure方法开启子View的测量流程。getChildMeasureSpec是一个非常重要的方法,接下来我们就来分析一下这个方法的逻辑:

/**
 * Does the hard part of measureChildren: figuring out the MeasureSpec to
 * pass to a particular child. This method figures out the right MeasureSpec
 * for one dimension (height or width) of one child view.
 *
 * 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高
 * 为子View生成最合适的MeasureSpec
 *
 * @param spec 父容器的MeasureSpec
 * @param padding 父容器的内间距(可能还会加上子View的外间距)
 * @param childDimension 子View的LayoutParams中封装的width/height
 * @return 子View的MeasureSpec
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // ① 对父容器的MeasureSpec进行解包
	int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // ② 减去间距(可以简单认为size就是父容器剩余可用的空间)
    int size = Math.max(0, specSize - padding);

    // 记录子View最终的大小和测量模式
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // ③ 父容器是精准测量模式
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

     // ④ 父容器指定了一个最大可用的空间
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // ⑤ 父容器不对子View的大小作出限制
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    // ⑥ 将最终的size和mode打包为子View需要的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这个方法里的代码比较多,但都是有迹可循的,咱们来一点一点捋一遍。首先在代码①的位置,对父容器的MeasureSpec进行解包,获取specSize和specMode。然后在代码②的位置用specSize减去padding,其实就是获取父容器剩余的空间,并用resultSizeresultMode记录子View最终的大小和测量模式。接下来,就需要依照具体的测量模式和子View的LayoutParams进行分析了。

在代码③的位置,父容器的测量模式为MeasureSpec.EXACTLY,也就是说父容器的大小已经确定了,那么我们只需要参考子View的LayoutParams就好了:

  1. 如果LayoutParams中的宽/高是一个确定的值(比如20dp这样的形式),即childDimension>0,那就说明这是一个有着强烈自我意识的子View,它知道自己想要多大的空间。系统将充分尊重子View的需求,resultSize就将被赋值为子View声明的宽/高(childDimension),测量模式也会被赋值为MeasureSpec.EXACTLY。当然,如果子View声明的宽/高大于父容器剩余的空间,最终显示的时候超出的部分是会被裁剪的;

  2. 如果LayoutParams中的宽/高是LayoutParams.MATCH_PARENT,说明子View想要和父容器一样大,那就将父容器剩余的空间(size)赋给resultSize就好了。此时子View的宽/高依旧是确定的,测量模式同样会被赋值为MeasureSpec.EXACTLY;

  3. 如果LayoutParams中的宽/高是LayoutParams.WRAP_CONTENT,说明子View自己也不清楚想要多大的空间,那父容器也无可奈何。此时会将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,也就是为子View指定了一个最大可用的空间;

在代码④的位置,父容器的测量模式为MeasureSpec.AT_MOST,也就是说父容器只知道自己可以使用的最大空间,并不知道精确的大小,接下来结合子View的LayoutParams进行讲解:

  1. 如果LayoutParams中的宽/高是一个确定的值(childDimension>0),那就将resultSize赋值为子View声明的宽/高(childDimension),测量模式也会被赋值为MeasureSpec.EXACTLY;

  2. 如果LayoutParams中的宽/高是LayoutParams.MATCH_PARENT,说明子View想要和父容器一样大。但是此时父容器也不确定自己有多大,所以只能将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,也就是为子View指定了一个最大可用的空间;

  3. 如果LayoutParams中的宽/高是LayoutParams.WRAP_CONTENT,说明父容器和子View都不清楚自己想要多大的空间,那就直接将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,同样为子View指定了一个最大可用的空间。

可以看到在父容器的测量模式为MeasureSpec.AT_MOST时,无论子View的LayoutParams使用WRAP_CONTENT还是MATCH_PARENT,结果都是一样的。

在代码⑤的位置,父容器的测量模式为MeasureSpec.UNSPECIFIED,也就是不限制子View的大小。这一般是系统内部使用的测量模式,我们就不再重点讲解了。只说明一下如果LayoutParams中的宽/高是一个确定的值,那就将resultSize赋值为childDimension,测量模式也会被赋值为MeasureSpec.EXACTLY。

getChildMeasureSpec是一个非常重要的方法,希望大家可以好好理解这个过程。

接下来,我们再来一起看看ViewGroup#measureChildWithMargins方法:

/**
 * Ask one of the children of this view to measure itself, taking into
 * account both the MeasureSpec requirements for this view and its padding
 * and margins. The child must have MarginLayoutParams The heavy lifting is
 * done in getChildMeasureSpec.
 *
 * @param child 被测量的子View(必须要有MarginLayoutParams)
 * @param parentWidthMeasureSpec 父容器的MeasureSpec(针对width)
 * @param widthUsed 在水平方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)
 * @param parentHeightMeasureSpec 父容器的MeasureSpec(针对height)
 * @param heightUsed 在垂直方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)
 */
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
	// ① 获取子View的LayoutParams,并强制转型为MarginLayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // ② 生成测量子View需要的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    
    // ③ 调用子View的measure方法开始测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先在代码①的位置获取子View的LayoutParams,并强制转型MarginLayoutParams,这也就说明使用这个方法的父容器要支持margin属性(即父容器的LayoutParams要继承MarginLayoutParams)。

后面两步就和measureChild相似了,先在代码②的位置使用getChildMeasureSpec方法生成测量子View需要的MeasureSpec。这里和measureChild不同的是除了传入父容器的内边距之外,还传入了子View的外边距(margin)以及widthUsed/heightUsed。widthUsed和heightUsed是在水平/垂直方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)。

最后在代码③的位置调用子View的measure方法开始测量。其实看方法名也能明白这个方法和measureChild的区别,无非就是在测量的时候会考虑到外边距的影响。当我们需要考虑子View的margin时,就可以使用这个方法进行测量。

最后我们再来看看ViewGroup#measureChildren方法:

/**
 * Ask all of the children of this view to measure themselves, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * We skip children that are in the GONE state The heavy lifting is done in
 * getChildMeasureSpec.
 *
 * @param widthMeasureSpec 父容器的MeasureSpec(针对width)
 * @param heightMeasureSpec 父容器的MeasureSpec(针对height)
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount; // 子View的数量
    final View[] children = mChildren; // 包含子View的数组
    for (int i = 0; i < size; ++i) { // 逐个对子View进行测量
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // 只测量visibility不为GONE的子View(提高效率)
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

可以看到,这个方法的目的是对父容器所有的子View进行测量,其实就是逐次对每个visibility不为GONE的子View调用了measureChild方法。所以我们也就知道了,如果希望在测量过程中考虑子View的外间距的话,是不可以使用这个方法的。

到这里,我们基本就把ViewGroup与测量流程相关的方法分析完了。仔细想来,似乎ViewGroup并没有做出什么实质性的测量工作。毕竟ViewGroup是一个抽象的父类,确实也不能决定具体的测量步骤。

如果通过继承ViewGroup实现自定义View,就应该重写onMeasure方法,并在这个方法中合理利用ViewGroup提供的measureChild、measureChildWithMargins、measureChildren和getChildMeasureSpec等方法完成对子View的测量工作,并通过setMeasuredDimension方法设置自身的宽高。

整体的流程图

上面分别从View和ViewGroup的角度讲解了测量流程,这里再以流程图的形式归纳一下整个measure过程,便于加深记忆:

Android View原理解析之测量流程(measure)_第1张图片

小结

测量流程在三大流程中相对是比较复杂的,如果看完本文后依旧有些疑惑,不如打开AndroidStudio,沿着文章的脉络亲自探索一下整个measure过程的逻辑,可能学习效果会更好一点。

参考资料

https://blog.csdn.net/a553181867/article/details/51494058
https://blog.csdn.net/xmxkf/article/details/51490283
https://blog.csdn.net/lfdfhl/article/details/51347818

你可能感兴趣的:(Android进阶)