04_View的工作流程- MeasureSpec介绍

127 阅读5分钟

MeasureSpec

View的Measure过程主要是为了确定View的测量宽/高,而View的测量宽/高的确定是和MeasureSpec有关的。因此,在了解View的测量过程前,需要先了解MeasureSpec。

介绍

MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法。它的定义如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    /**
     * 将SpecMode和SpecSize打包成一个int值,避免过多的对象内存分配
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * 将打包后的int值解包,获取SpecMode
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    /**
     * 将打包后的int值解包,获取SpecSize
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

SpecMode有三种类型:

  • UNSPECIFIED
    父容器不对View有任何限制,View可以是任意大小。这种情况一般用于系统内部,表示一种测量的状态。

  • EXACTLY
    父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

  • AT_MOST
    父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content

3.1 DecorView的MeasureSpec

对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定。

先来看下DecorView的MeasureSpec的创建过程,在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

/**
 * @param desiredWindowWidth  屏幕宽度
 * @param desiredWindowHeight 屏幕高度
 * @param lp                  WindowManager.LayoutParams
 */
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着看getRootMeasureSpec方法的实现:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 窗口无法调整大小。强制根视图为windowSize。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 窗口可以调整大小。设置根视图的最大大小。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 窗户的尺寸要精确。强制根视图为该大小。
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

根据上述代码,DecorView的MeasureSpec的产生过程就很明确了,其由它的LayoutParams和屏幕尺寸共同确定,如下:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不确定,但不能超过窗口的大小。
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

普通View的MeasureSpec

对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定

普通View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:

/**
 * 测量子元素,包括子元素的margin
 *
 * @param child 子View
 * @param parentWidthMeasureSpec 父容器的宽度MeasureSpec
 * @param widthUsed 父容器已经使用的宽度
 * @param parentHeightMeasureSpec 父容器的高度MeasureSpec
 * @param heightUsed 父容器已经使用的高度
 */
protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
    // 获取子View的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // 根据父容器的MeasureSpec和子View的LayoutParams计算子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方法,传入计算好的MeasureSpec
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChildWithMargins方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,子元素MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,同时也和View的margin及padding有关。

具体情况在ViewGroup的getChildMeasureSpec方法中,如下:

/**
 * 根据父容器的MeasureSpec和View自身的LayoutParams来确定子元素的MeasureSpec
 *
 * @param spec 父容器的MeasureSpec
 * @param padding 父容器已占用的空间大小
 * @param childDimension View本身的LayoutParams,如MATCH_PARENT、WRAP_CONTENT、100dp
 * @return 返回View的MeasureSpec
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 获取父容器的SpecMode和SpecSize
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // 计算父容器剩余可用空间
    int size = Math.max(0, specSize - padding);

    // 子View的最终MeasureSpec的Size和Mode
    int resultSize = 0;
    int resultMode = 0;

    // 根据父容器的SpecMode进行判断
    switch (specMode) {
        // 父容器指定了一个确切的大小
        case MeasureSpec.EXACTLY:
            // 如果子View的LayoutParams是具体的大小
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            // 如果子View的LayoutParams是MATCH_PARENT
            else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // 子View希望填充父容器的剩余空间
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            // 如果子View的LayoutParams是WRAP_CONTENT
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子View希望确定自己的大小,但不能超过父容器的剩余空间
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器指定了一个最大的大小
        case MeasureSpec.AT_MOST:
            // 如果子View的LayoutParams是具体的大小
            if (childDimension >= 0) {
                // 子View希望有一个具体的大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            // 如果子View的LayoutParams是MATCH_PARENT
            else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // 子View希望填充父容器的剩余空间,但不能超过父容器的剩余空间
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            // 如果子View的LayoutParams是WRAP_CONTENT
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子View希望确定自己的大小,但不能超过父容器的剩余空间
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器没有对子View施加任何约束
        case MeasureSpec.UNSPECIFIED:
            // 如果子View的LayoutParams是具体的大小
            if (childDimension >= 0) {
                // 子View希望有一个具体的大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            // 如果子View的LayoutParams是MATCH_PARENT
            else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // 子View希望填充父容器的剩余空间,具体大小由子View自己决定
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            // 如果子View的LayoutParams是WRAP_CONTENT
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子View希望确定自己的大小,具体大小由子View自己决定
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }

    // 创建子View的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这段代码清楚地展示了普通View的MeasureSpec的创建规则,以及根据父容器的MeasureSpec和子元素的LayoutParams来决定子元素的MeasureSpec的过程。

下面是一个表格,展示了父容器的SpecMode和View自身的LayoutParams决定的View测量大小和测量模式:

父容器的SpecModeView自身LayoutParamsView测量模式View测量大小
EXACTLY固定值EXACTLY固定值
EXACTLYmatch_parentEXACTLY父容器可用大小
EXACTLYwrap_contentAT_MOST父容器可用大小
AT_MOST固定值EXACTLY固定值
AT_MOSTmatch_parentAT_MOST父容器可用大小的最大限制
AT_MOSTwrap_contentAT_MOST父容器可用大小的最大限制
UNSPECIFIED固定值EXACTLY固定值
UNSPECIFIEDmatch_parentUNSPECIFIED0 或 父容器可用大小
UNSPECIFIEDwrap_contentUNSPECIFIED0 或 父容器可用大小