View的Measure过程解析

来源:互联网 发布:python 编码问题 编辑:程序博客网 时间:2024/05/16 15:20

背景

在《Activity.setContentView()内部实现解析》分析了View是如何被添加到DecorView中,但添加到DecorView后,View对用户来说还是不可见的。View在呈现给用户前,它需要经过Measure -> Layout -> Draw三个步骤。本文将从DecorView的源码开始,分析View的Measure过程。

预备知识–测量概述

在分析测量过程前需要确定测量的意义和我对测量过程的定义。View在呈现出来前,需要确定的第一个问题是:显示的View多大?即View的大小,而Measure就是为了确定View的大小。那View的大小是由父容器(提示:父容器是指包含该View的父布局)来决定还是由View自己决定呢?
现在我把Measure过程分为两个阶段
- 第一阶段:父容器计算出measureSpec值,这个值就是父容器对子View的显示区域的限定条件,并传递给子View
- 第二阶段:子View根据显示区域限定条件measureSpec来设置自身大小,当然View也可根据自己的需要设置大小

提示:measureSpec是从父容器传递给子View的,我们可以把measureSpec定义为父容器父子View显示范围的限定条件。
父容器有权利限定子View的显示范围,即预留多少空间给子View。而父容器对子View的限定条件就承载在measureSpec值中。所以在分析过程中说到“显示范围限定条件”指的就是measureSpec值

通过下面的图来说明我对View测量过程的划分
这里写图片描述
这里写图片描述

可得出下面的结论:
父容器传递给子View的measureSpec只是规定了子View的可显示区域,而View的实际显示大小还是得由View来自己设定,View可以根据measureSpec来设置大小,也可随意设置,当然如果子View设定的大小超出父容器规定的范围,那就可以,只是父容器给你预留的空间无法显示完全子View。

预备知识–MeasureSpec

在测量概述中说到父容器会给子View传递显示范围限定条件measureSpec。
现在来分析一下measureSpec是如何承载显示范围限定条件的。
measureSpec是int类型变量,在java中int类型由4个字节组成,每个字节8位,所以measureSpec一共有32位,这32位由两部份组成:specMode和specSize,前面的两位是specMode标志位, 后面30位是specSize,如下图所示:

MeasureSpec的组成

系统提供MeasureSpec工具类,使得我们可以很方便的从measureSpec中取出specSize和specMode

int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);// MeasureSpce的内部实现int  specMode = widthMeasureSpce & 0xB000;  // 0xB000的二进制为1100 0000 0000 0000int  specSize = widthMeasureSpec & 0x3fff;  // 0x3fff的二进制为0011 1111 1111 1111

specMode有三种模式,不同的模式对specSize有不同的解释
这里列出specMode的每个模式,以及每种模式对specSize的解释

specMode specSize MeasureSpec.EXACTLY 父容器告诉子View:你可以使用SpecSize作为的你的长宽值 MeasureSpec.AT_MOST 父容器告诉子View:你可根据需求设置你的大小,但你设定的大小不要超过SpecSize这个值 MeasureSpec.UNSPECIFIED 父容器告诉子View:你可根据需求设置你的大小,我不会限制你的可显示范围

对于MeasureSpec这个值是如何计算出来的,我们在下面会详细的分析,你只要知道,父容器会给子View传递显示范围限制条件measureSpec,而这个显示范围限制条件是以一个int类型的变量measureSpec

分析

《Activity.setContentView()内部实现解析》中说到了在ViewRootImpl.setView()方法中,会调用WindowSession.addToDisplay( )方法通知WindowManagerService添加一个Window,WindowManagerService进行相应的逻辑处理后,会通知ViewRootImpl对象对DecorView进行布局,而布局的入口函数是performTraversals( ),该方法代码量很长,但我们只需关注对DecorView开始测量的入口。
提示:由于DecorView是最顶级的View,所以DecorView不存在真正的父容器,在后面的分析中我们说到的DecorView的父容器指的就是ViewRootImpl这个类,虽然这个类不属于View,但由于ViewRootImpl是直接管理DecorView的,所以就把ViewRootImpl当做DecorView的父容器

private void performTraversals() {    省略....    // frame是Rect类对象,这里我们只需知道frame是系统给定的用于显示DecorView的区域大小    mWidth = frame.width();    mHeight = frame.height();    // lp是前面为DecorView创建的布局参数,这里lp.with和lp.height都是MATCH_PARENT    // 通过getRootMeaureSpec()方法确定DecorView的childWidthMeasureSpec和childHeightMeasureSpec    // 即DecorView的显示范围限定条件    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);    // 开始测量DecorView的大小    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    省略....}

从上面的源码中可知道getRootMeasureSpec( )方法确定了DecorView的显示范围限定条件,在分析getRootMeasureSpec( )的实现之前我们来猜测一下,这个显示范围限定条件是如何计算出来的?是DecorView来决定还是由系统来决定呢?
getRootMeasureSpec(int windowSize, int rootDimension)方法的windowSize参数是系统给定的窗口大小,rootDimension是DecorView的layout_width/layout_height属性值。我们直接查看getRootMeasureSpec()方法的源码实现。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {    int measureSpec;    switch (rootDimension) {    case ViewGroup.LayoutParams.MATCH_PARENT: //DecorView希望它的大小与系统给定的显示大小一样        // ViewRootImpl就会创建一个显示范围限定条件为“你可设置你的大小为windowSize”的measureSpec        // 并传递给DecorView        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);        break;    case ViewGroup.LayoutParams.WRAP_CONTENT: // DecorView希望它的大小可以足够显示DecorView中包含的内容        // ViewRootImpl就会创建显示范围限定条件为“你可根据需要设置你的大小,但最大不能超过windowSize”的measureSpec        // 并传递给DecorView        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);        break;    default:        // ViewRootImpl就会创建显示范围限定条件为“你可设置你的大小为rootDimension”的measureSpec        // 并传递给DecorView        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);        break;    }    return measureSpec;}

从上面的源码可以得出一下结论:对DecorView的MeasureSpec(显示范围的限定条件)是由系统和DecorView的布局参数layout_width/layout_height共同决定,对,共同决定,而不是由DecorView自己来决定,也不是由系统自己来决定。
下面用表格整理生成DecorView的MeasureSpec的不同组合

· match_parent match_parent rootDimension MeasureSpec windowSize+MeasureSpec.EXACTLY windowSize+MeasureSpec.AT_MOST rootDimension+MeasureSpec.EXACTLY

在performMeasure( )内部调用measure()进行测量DecorView,由于DecorView没有重写(也不可以重写)measure方法,所以进入View.mesure()方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {    try {        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);    } finally {    }}// widthMeasureSpec和heightMeasureSpec就是从父容器传递给子View的“显示范围限定条件”public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    boolean optical = isLayoutModeOptical(this);    ...省略    if (forceLayout || needsLayout) {        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);        // 最后还是在onMeasure()方法中设置View的大小        onMeasure(widthMeasureSpec, heightMeasureSpec);        ...省略    }    ...省略                                       }

onMeasure这个方法很重要,onMeasure()是View中的方法,子类可以重写onMeasure()来根据自己的需求来设置自身的大小,但如果不重写onMeasure( )也没有问题,View.onMeasure( )方法会根据widthMeasureSpec和heightMeasureSpec这两个显示范围限定条件来设置View的大小。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    // 最终调用setMeasuredDimension()方法给mMeasuredWidth和mMeasuredHeight这两个变量赋值    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}// View建议的自己的最小宽度,由于View这个时候还没有内容,所以只能由View的背景来决定protected int getSuggestedMinimumWidth() {    // 最小推荐值是由mBackground和mMinWidth决定,这两个属性值都是在创建View时设定的    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());}// size: View建议的最小值,通过getSuggestedMinimumWidth()方法获取, // measureSpec: 是父容器传递给View的限定条件public static int getDefaultSize(int size, int measureSpec) {    int result = size;    int specMode = MeasureSpec.getMode(measureSpec);    int specSize = MeasureSpec.getSize(measureSpec);    switch (specMode) {    // 父容器的告诉子View:你自己来决定你的大小吧,我不限制你的大小    // 这时View就会使用自己建议最小值,因为View目前还不知它的内部包含什么,所以只能用建议值    case MeasureSpec.UNSPECIFIED:        result = size;        break;    // 如果父容器告诉子View:你自己来决定你的大小吧,但你设置的大小不能超过specSize    // 或者告诉子View:设置specSize这个值为你的大小吧    // 子View收到父容器的上面两种限定条件都会把大小设置为specSize    // 注意:子View收到父容器“你自己来决定你的大小吧,但你设置的大小不能超过specSize”这个限定是,也会直接把大小设置为specSize    case MeasureSpec.AT_MOST:    case MeasureSpec.EXACTLY:        result = specSize;        break;    }    return result;}

下面用一个表格来整理出View.onMeasure( )方法内部是如何根据MeasureSpec来设定大小的

MeasureSpec.EXACTLY MeasureSpec.AT_MOST MeasureSpec.UNSPECIFIED 使用View建议的值 specSize specSize

从View派生出来的子类当然可以不按上面这种方法来确定自己的大小,那就需要重写onMeasure( ),并根据自己的需求来设置大小。比如:DecorView、FrameLayout . . .
我们来看一下DecorView重写的onMeasure( )方法

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    ...(此处就是根据一些属性调整一下withMeasureSpec和heightMeasureSpec,这里直接省略)          // 调用父类的onMeasure()继续进行测量          super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

我们都知道DecorView是从FrameLayout这个布局容器继承来,所以最终还是调用FrameLayout.onMeasure( )

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    int count = getChildCount();    int maxHeight = 0;    int maxWidth = 0;    // 遍历DecorView中包含的子View,并通过measureChildWithMargins()方法来测量每个子View的大小    for (int i = 0; i < count; i++) {        final View child = getChildAt(i);        if (mMeasureAllChildren || child.getVisibility() != GONE) {            // 测量child大小,下面会对这个方法进行分析            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);            // maxWidth和maxHeight记录最大那个View的长宽, 注意还需要加上View设定的layout_margin值            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            maxWidth = Math.max(maxWidth,                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);            maxHeight = Math.max(maxHeight,                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);        }    }    // 加上DecorView设定的padding值    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();    ...    // 最后通过resolveSizeAndState()方法来计算DecorView的长宽    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),            resolveSizeAndState(maxHeight, heightMeasureSpec,                    childState << MEASURED_HEIGHT_STATE_SHIFT));    ...}

我们先不分析measureChildWithMargins( )方法内部是如何对子View进行测量的,measureChildWithMargins( )测量完毕后,我们就可通过getMeasureWidth( )和getMeasureHeight( )来获取子View的测量结果。当DecorView测量完毕子View后,接下来就是确定DecorView(即子View的父容器)的大小,我们来分析resolveSizeAndState( )这个方法是如何确定DecorView的大小

// size为DecorView中包含的最大的子View的大小// measureSpec为DecorView的父容器(由于DecorView是最顶级的View所以不存在父容器,我们说DecorView的父容器指的就是ViewRootImpl)传递给DecorView的显示范围限定条件public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize =  MeasureSpec.getSize(measureSpec);        switch (specMode) {        // 父容器限定条件为:"由你自己来确定你的大小"        case MeasureSpec.UNSPECIFIED:            result = size;            break;            // 父容器限定条件为:"你可以自己设置你的大小,但不要超过specSize"            // 这时DecorView的大小设置为specSize和size中的最小值,这样就确保DecorView的大小不会超过specSize        case MeasureSpec.AT_MOST:            if (specSize < size) {                result = specSize | MEASURED_STATE_TOO_SMALL;            } else {                result = size;            }            break;            // 父容器限定条件为:"你可把specSize设置为你的大小值"        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result | (childMeasuredState&MEASURED_STATE_MASK);    }

注意:resolveSizeAndState( )方法的逻辑是确定FrameLayout布局的大小,但是对于其它的一些布局如LinearLayout、RelativeLayout就是另外一套处理逻辑

分析完DecorView大小的确定之后,我们返回去分析子View是如何测量的

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {    // 获取子View的Margin值参数    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();    // 通过getChildMeasureSpec()方法获取childWidthMeasureSpec和childHeightMeasureSpec    // childWidthMeasureSpec和childHeightMeasureSpec就是父容器(DecorView)即将要传递给子View的参数,即显示范围限定条件    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);    // 把childHeightMeasureSpec和childWidthMeasureSpec传递给子View,让子View根据这两个限定值来设置自己的大小    // measure的处理逻辑前面已经分析过了    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}// 这个方法很重要,该方法确定子View的显示范围限定条件// spec 显示范围限定条件// padding 包括了父容器的padding值和子View的layout_margin值// childDimension 子View在布局资源文件中或者ViewGroup.LayoutParam设置的layout_width或layout_height值public static int getChildMeasureSpec(int spec, int padding, int childDimension) {    int specMode = MeasureSpec.getMode(spec);    int specSize = MeasureSpec.getSize(spec);    // 先减掉父容器设定的Padding值和View设定的Margin值,即剩余的才是用于给View的范围    int size = Math.max(0, specSize - padding);    int resultSize = 0;    int resultMode = 0;    switch (specMode) {    // 若父容器(DecorView)传递的限定条件为:"你可把specSize设置为你的大小值",即父容器希望把specSize + MeasureSpec.EXACTLY这个限定范围传递给子View    // 但是子View还是会根据自己的layout_width/layout_height属性值,进一步作出确定,所以还会根据childDimension值才能确定    case MeasureSpec.EXACTLY:        if (childDimension >= 0) {  // 如果layout_width/layout_height属性设定了一个特定的值childDimension            // 那子View的显示范围限定条件为: childDimension + MeasureSpec.EXACTLY            // 即翻译为:"你可以把你的大小设定特定的大小值childDimension,这个值是你的在layout_width属性设置的"            resultSize = childDimension;              resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果layout_width/layout_height属性设置为LayoutParams.MATCH_PARENT            // 那子View的显示范围限定条件为:  size + MeasureSpec.EXACTLY            // 即翻译为:"你可以把你的大小设定特定的大小值size,这个值是父容器的大小"            resultSize = size;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 如果layout_width/layout_height属性设置为LayoutParams.WRAP_CONTENT            // 那子View的显示范围限定条件为:  size + MeasureSpec.AT_MOST            // 即翻译为:"你可随便设置你的大小,但不能超过size,这个值是父容器的大小"            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;    case MeasureSpec.UNSPECIFIED:        if (childDimension >= 0) {            resultSize = childDimension;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.MATCH_PARENT) {            resultSize = 0;            resultMode = MeasureSpec.UNSPECIFIED;        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            resultSize = 0;            resultMode = MeasureSpec.UNSPECIFIED;        }        break;    }    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

总结

View的测量过程已分析完毕,最后总结一下我在开发过程遇到的一些问题

  1. 直接看代码
    FrameLayout mPreviewContainer = findViewById(R.id.container);
    mPreviewContainer.measure(0, 0);
    上面代码能正确计算出mPreviewContainer的measureWidth和measureHeight吗?