Android 中 View 的测量机制是 View 渲染机制的一个重要过程,理解 View 的测量机制和流程有利于我们加深对 View 渲染机制的理解、提高自定义 View 的能力。

View 的测量机制主要涉及到 MeasureSpecView 的测量ViewGroup 的测量 和这三个知识点,下面一一展开讲解。

MeasureSpec

先看下面这样一个布局:

<FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

最后我们在界面上看到的效果是,一个 200 dp 的正方形容器包裹了一个 TextView,该 TextView 占满了容器,即其宽高也是 200 dp。

那么,FrameLayout 是怎么告知 TextView 自己宽高的呢?TextView 又是怎么据此来确定自己的宽度呢?这都跟 MeasureSpec 有关。

MeasureSpec 是对 View 的测量要求,它是一个 32 位的 int 值,高 2 为代表 SpecMode(测量模式),低 30 位代表 SpecSize(大小)。

SpecMode 有三种:

  • UNSPECIFIED,大小不作限制
  • AT_MOST,父容器对子 View 给定一个上限,子 View 可以去适应这个大小
  • EXACTLY,精确大小,View 的大小即是 SpecSize

对于顶级 View(无上级 View,DecorView),其 MeasureSpec 是有窗口的尺寸和自身的 Layoutparams 共同确定的;而普通 View(包括 ViewGroup) 其 MeasureSpec 由父容器的 MeasureSpec 和本身的 LayoutParams 确定。

View

View 的测量主要涉及到 measureonMeasuresetMeasuredDimension 这三个方法。

measure

measure 是被父容器调用的,根据传入的 MeasureSpec 结合自身的情况进行调整产生新的 MeasureSpec

onMeasure

onMeasure 是在 measure 方法中被调用,传入新的 MeasureSpec。(作为回调,可以在自定义 View 中定制)

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

setMeasureDimension

setMeasureDimension 是在 onMeasure 方法中被调用的,用以设置自身宽高,并将宽高保存起来。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

ViewGroup

以下将子 View 称为 childView,父容器为 container。

childView 的 MeasureSpec 受何影响?

container 本身有一个 MeasureSpec,上面说到 childView 的 MeasureSpec 由其自身 LayoutParams 和 container 的 MeasureSpec 确定。

ViewGroup#measureChild 中:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final 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);
}

可以看到,childView 宽度/高度 MeasureSpec 由 container 的 MeasureSpec 、自身的 LayoutParamspadding 共同产生。

如何影响 childView 的 MeasureSpec

接下来详细讲一下如何利用这三者生成子 View 的 MeasureSpec

假设我们是在生成 childView 的宽度 MeasureSpec。首先我们根据 container 的 MeasureSpec 获取其 SpecModeSpecSize。将 SpecSize 减去 padding 即可得到父容器的内容宽度。

接着,我们按照 container的 SpecMode 分为三种情况:

  • EXACTLY: container 宽度确定
    • childView 的宽度在布局文件中明确声明:那么 childView 的 SpecMode=EXACTLYSpecSize=明确的宽度
    • childView 的宽度为 match_parent,那么 childView 的 SpecMode=EXACTLYSpecSize=container内容宽度
    • childView 的宽度为 wrap_content,那么 childView 的 SpecMode=AT_MOSTSpecSize=container内容宽度
  • AT_MOST: container 宽度上限确定
    • childView 的宽度在布局文件中明确声明:那么 childView 的 SpecMode=EXACTLYSpecSize=明确的宽度
    • childView 的宽度为 match_parent,那么 childView 的 SpecMode=AT_MOSTSpecSize=container内容宽度
    • childView 的宽度为 wrap_content,那么 childView 的 SpecMode=AT_MOSTSpecSize=container内容宽度
  • UNSPECIFIED:container 宽度宽度不指定
    • childView 的宽度在布局文件中明确声明:那么 childView 的 SpecMode=EXACTLYSpecSize=明确的宽度
    • childView 的宽度为 match_parent,那么 childView 的 SpecMode=UNSPECIFIEDSpecSize=container内容宽度或0
    • childView 的宽度为 wrap_content,那么 childView 的 SpecMode=UNSPECIFIEDSpecSize=container内容宽度或0

这部分的实现逻辑在 ViewGroup#getChildMeasureSpec 中:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

container 自身如何测量?

确定了子 View 的 MeasureSpec,我们看回 ViewGroup#measureChild

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final 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 的 measure 方法,它会根据我们确定的 MeasureSpec 去测量自身。

measureChild 这个方法是在 ViewGroup#onMeasure 中遍历调用:

for (int i = 0; i < mMainViewList.size(); i++) {
    View mainView = mMainViewList.get(i);
    if (mainView.getVisibility() != View.GONE) {
        measureChild(mainView, unspecifiedSpec, unspecifiedSpec);
        mMeasuredWidth = Math.max(mMeasuredWidth, mainView.getMeasuredWidth());
        mainHeight += mainView.getMeasuredHeight();
        state = View.combineMeasuredStates(state, mainView.getMeasuredState());
    }
}

可以看到,ViewGroup 遍历了其下所有的子 View,让它们根据 MeasureSpec 去测量自身,然后获取它们测量后的大小,然后确定自身大小,再保存起来。

回顾

我们看回开头的例子:

<FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

这个结构的测量流程是:

  • FrameLayout 根据自身的 MeasureSpec(SpecMode=EXACTLY,SpecSize=200dp) 和 TextViewLayoutParams(match_parent),产生 TextViewMeasureSpec(SpecMode=EXACTLY,SpecSize=200dp)
  • TextView 测量自身并确定宽高
  • FrameLayout 获取 TextView 大小再确定自身大小