Android界面生成流程:View的测量(Measure)

来源:互联网 发布:数据采集员需要什么 编辑:程序博客网 时间:2024/05/02 22:35

回顾

在文章《WindowManager和Window的关系》中说到,每个Activity都包含一个Window,而每个Window都包含一个DecorView。DecorView是FrameLayout的子类,是根节点视图,默认包含了一个LinearLayout垂直布局,这个垂直布局上面是titlebar,下面是我们的内容布局mContentParent。在生成的时候会根据主题的设置来修改titlebar风格。然后我们的主要布局文件通过setContentView的方式,添加到mContentParent中。这些操作都是在Activity中的handlerLaunchActivity调用onCreate时进行的,执行完之后会调用Activity中的handlerResumeActivity,在这里面会执行onResume操作。

onResume操作通过WindowManager将上一步生成的DectorView添加到Window中,然后调用ViewRoot开始进行View的测量,布局,绘制,在这里同时会进行“子线程不能修改UI线程的View”的判断,在《 再谈子线程-居然可以在非UI线程中更新UI》中提到,如果在onCreate中子线程不进行睡眠操作时操作UI时,那只是修改了属性而已,并没有等到在onResume里面做这一个判断。但是如果执行了一段睡眠操作,这个睡眠操作等到或超过这个生成的时间,正好遇到了这个判断,那么将会抛出异常。

注意的是,DecorView的宽高是和手机屏幕一样的,只是我们在设置主题时候如果没有设置隐藏状态栏,那么我们的titlebar会往下挪一点,这个高度恰恰是等于状态栏的高度。

Android绘制view的过程简单描述:

简单描述可以解释为:计算大小(measure),布局坐标计算(layout),绘制到屏幕(draw);

下面看看每一步的动作到底是什么

  • 第一步:当activity启动的时候,触发初始化view过程的是由Window对象的DecorView调用View(具体怎样从xml中读取是用LayoutInflater.from(context).inflate)对象的 public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法开始的,这个方法是final类型的,也就是所有的子类都不能继承该方法,保证android初始化view的原理不变。具体参数类值,后面会介绍。

  • 第二步:View的measure方法 onMeasure(widthMeasureSpec, heightMeasureSpec),该方法进行实质性的view大小计算。注意:view的大小是有父view和自己的大小决定的,而不是单一决定的。这也就是为什么ViewGroup的子类会重新该方法,比如LinearLayout等。因为他们要计算自己和子view的大小。View基类有自己的实现,只是设置大小。其实根据源码来看,measure的过程本质上就是把Match_parent和wrap_content转换为实际大小

  • 第三步:当measure结束时,回到DecorView,计算大小计算好了,那么就开始布局了,开始调用view的 public final void layout(int l, int t, int r, int b),该还是也是final类型的,目的和measure方法一样。layout方法内部会调用onlayout(int l, int t, int r, int b )方法,二ViewGroup将此方法abstract的了,所以我们继承ViewGroup的时候,需要重新该方法。该方法的本质是通过measure计算好的大小,计算出view在屏幕上的坐标点

  • 第四步:measure过了,layout过了,那么就要开始绘制到屏幕上了,所以开始调用view的 public void draw(Canvas canvas)方法,此时方法不是final了,原因是程序员可以自己画,内部会调用ondraw,我们经常需要重写的方法。

正文

从ViewRootImpl#PerformTraveals说起

private void performTraversals() {            ...        if (!mStopped) {            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                   }        }         if (didLayout) {            performLayout(lp, desiredWindowWidth, desiredWindowHeight);            ...        }        if (!cancelDraw && !newSurface) {            if (!skipDraw || mReportNextDraw) {                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {                    for (int i = 0; i < mPendingTransitions.size(); ++i) {                        mPendingTransitions.get(i).startChangingAnimations();                    }                    mPendingTransitions.clear();                }                performDraw();            }        }         ...}

方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec。

理解MeasureSpec

MeasureSpec是View类的一个内部类,我们先看看官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息,但是要注意,MeasureSpec并不是指View的测量宽高,这是不同的,是根据MeasueSpec而测出测量宽高。

MeasureSpec的作用在于:在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。

我们来看看这个类的源码:

public static class MeasureSpec {        private static final int MODE_SHIFT = 30;        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;        /**          * UNSPECIFIED 模式:          * 父View不对子View有任何限制,子View需要多大就多大          */         public static final int UNSPECIFIED = 0 << MODE_SHIFT;        /**          * EXACTYLY 模式:          * 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小          * 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式          */         public static final int EXACTLY     = 1 << MODE_SHIFT;        /**          * AT_MOST 模式:          * 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,          * 即对应wrap_content这种模式          */         public static final int AT_MOST     = 2 << MODE_SHIFT;        //将size和mode打包成一个32位的int型数值        //高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小        public static int makeMeasureSpec(int size, int mode) {            if (sUseBrokenMakeMeasureSpec) {                return size + mode;            } else {                return (size & ~MODE_MASK) | (mode & MODE_MASK);            }        }        //将32位的MeasureSpec解包,返回SpecMode,测量模式        public static int getMode(int measureSpec) {            return (measureSpec & MODE_MASK);        }        //将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小        public static int getSize(int measureSpec) {            return (measureSpec & ~MODE_MASK);        }        //...    }

可以看出,该类的思路是相当清晰的,对于每一个View,包括DecorView,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?

为了解决这个疑问,我们回到ViewRootImpl#PerformTraveals方法,看①号代码处,调用了getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec,那么我们看一下ViewRootImpl#getRootMeasureSpec方法的实现:

/** * @param windowSize *            The available width or height of the window * * @param rootDimension *            The layout params for one dimension (width or height) of the *            window. * * @return The measure spec to use to measure the root view. */private static int getRootMeasureSpec(int windowSize, int rootDimension) {    int measureSpec;    switch (rootDimension) {    case ViewGroup.LayoutParams.MATCH_PARENT:        // Window can't resize. Force root view to be windowSize.        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);        break;    //省略...    }    return measureSpec;}

思路也很清晰,根据不同的模式来设置MeasureSpec,如果是LayoutParams.MATCH_PARENT模式,则是窗口的大小,WRAP_CONTENT模式则是大小不确定,但是不能超过窗口的大小等等。

那么到目前为止,就已经获得了一份DecorView的MeasureSpec,它代表着根View的规格、尺寸,在接下来的measure流程中,就是根据已获得的根View的MeasureSpec来逐层测量各个子View。我们顺着①号代码往下走,来到performMeasure方法,看看它做了什么工作,ViewRootImpl#performMeasure:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");    try {        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);    } finally {        Trace.traceEnd(Trace.TRACE_TAG_VIEW);    }}

方法很简单,直接调用了mView.measure,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。

measure 测量流程

ViewGroup的测量流程

由于DecorView继承自FrameLayout,是PhoneWindow的一个内部类,而FrameLayout没有measure方法,因此调用的是父类View的measure方法,我们直接看它的源码,View#measure:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {        boolean optical = isLayoutModeOptical(this);        if (optical != isLayoutModeOptical(mParent)) {        ...        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||                widthMeasureSpec != mOldWidthMeasureSpec ||                heightMeasureSpec != mOldHeightMeasureSpec) {            ...            if (cacheIndex < 0 || sIgnoreMeasureCache) {                // measure ourselves, this should set the measured dimension flag back                onMeasure(widthMeasureSpec, heightMeasureSpec);                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            }         ...}

可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。

由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里不可能把所有布局方式的onMeasure方法都分析一遍,因此这里选择了FrameLayout的onMeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    //获取当前布局内的子View数量    int count = getChildCount();    //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.    final boolean measureMatchParentChildren =            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;    mMatchParentChildren.clear();    int maxHeight = 0;    int maxWidth = 0;    int childState = 0;    //遍历所有类型不为GONE的子View    for (int i = 0; i < count; i++) {        final View child = getChildAt(i);        if (mMeasureAllChildren || child.getVisibility() != GONE) {            //对每一个子View进行测量            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性            //那么它的大小取决于子View中的最大者            maxWidth = Math.max(maxWidth,                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);            maxHeight = Math.max(maxHeight,                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);            childState = combineMeasuredStates(childState, child.getMeasuredState());            //如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加            //宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响            if (measureMatchParentChildren) {                if (lp.width == LayoutParams.MATCH_PARENT ||                        lp.height == LayoutParams.MATCH_PARENT) {                    mMatchParentChildren.add(child);                }            }        }    }    // Account for padding too    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();    // Check against our minimum height and width    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());    // Check against our foreground's minimum height and width    final Drawable drawable = getForeground();    if (drawable != null) {        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());    }    //保存测量结果    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),            resolveSizeAndState(maxHeight, heightMeasureSpec,                    childState << MEASURED_HEIGHT_STATE_SHIFT));    //子View中设置为match_parent的个数    count = mMatchParentChildren.size();    //只有FrameLayout的模式为wrap_content的时候才会执行下列语句    if (count > 1) {        for (int i = 0; i < count; i++) {            final View child = mMatchParentChildren.get(i);            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();            //对FrameLayout的宽度规格设置,因为这会影响子View的测量            final int childWidthMeasureSpec;            /**              * 如果子View的宽度是match_parent属性,那么对当前FrameLayout的MeasureSpec修改:              * 把widthMeasureSpec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:              * 对于子Viw来说,如果要match_parent,那么它可以覆盖的范围是FrameLayout的测量宽度              * 减去padding和margin后剩下的空间。              *              * 以下两点的结论,可以查看getChildMeasureSpec()方法:              *              * 如果子View的宽度是一个确定的值,比如50dp,那么FrameLayout的widthMeasureSpec的宽度规格修改为:              * SpecSize为子View的宽度,即50dp,SpecMode为EXACTLY模式              *               * 如果子View的宽度是wrap_content属性,那么FrameLayout的widthMeasureSpec的宽度规格修改为:              * SpecSize为子View的宽度减去padding减去margin,SpecMode为AT_MOST模式              */            if (lp.width == LayoutParams.MATCH_PARENT) {                final int width = Math.max(0, getMeasuredWidth()                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()                        - lp.leftMargin - lp.rightMargin);                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(                        width, MeasureSpec.EXACTLY);            } else {                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +                        lp.leftMargin + lp.rightMargin,                        lp.width);            }            //同理对高度进行相同的处理,这里省略...            //对于这部分的子View需要重新进行measure过程            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);        }    }}

由以上的FrameLayout的onMeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法,这个方法下面会详细说明;对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。

在上面提到setMeasureDimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolveSizeAndState方法的返回值,那么我们直接看View#resolveSizeAndState方法:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {    final int specMode = MeasureSpec.getMode(measureSpec);    final int specSize = MeasureSpec.getSize(measureSpec);    final int result;    switch (specMode) {        case MeasureSpec.AT_MOST:            if (specSize < size) {                result = specSize | MEASURED_STATE_TOO_SMALL;            } else {                result = size;            }            break;        case MeasureSpec.EXACTLY:            result = specSize;            break;        case MeasureSpec.UNSPECIFIED:        default:            result = size;    }    return result | (childMeasuredState & MEASURED_STATE_MASK);}

可以看到该方法的思路是相当清晰的,当specMode是EXACTLY时,那么直接返回MeasureSpec里面的宽高规格,作为最终的测量宽高;当specMode时AT_MOST时,那么取MeasureSpec的宽高规格和size的最小值。(注:这里的size,对于FrameLayout来说,是其最大子View的测量宽高)。

小结

那么到目前为止,以DecorView为切入点,把ViewGroup的测量流程详细地分析了一遍,在ViewRootImpl#performTraversals中获得DecorView的尺寸,然后在performMeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onMeasure方法中,对每一个子View进行遍历,根据ViewGroup的MeasureSpec及子View的layoutParams来确定自身的测量宽高,然后最后根据所有子View的测量宽高信息再确定父容器的测量宽高。

那么接下来,我们继续分析对于一个子View来说,是怎么进行测量的

View的测量流程

还记得我们上面在FrameLayout测量内提到的measureChildWithMargin方法,它接收的主要参数是子View以及父容器的MeasureSpec,所以它的作用就是对子View进行测量,那么我们直接看这个方法,ViewGroup#measureChildWithMargins:

protected void measureChildWithMargins(View child,        int parentWidthMeasureSpec, int widthUsed,        int parentHeightMeasureSpec, int heightUsed) {    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();    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);    child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 1}

由源码可知,里面调用了getChildMeasureSpec方法,把父容器的MeasureSpec以及自身的layoutParams属性传递进去来获取子View的MeasureSpec,这也印证了“子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定”这个结论。那么,我们一起来看看ViewGroup#getChildMeasureSpec方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {    int specMode = MeasureSpec.getMode(spec);    int specSize = MeasureSpec.getSize(spec);    //size表示子View可用空间:父容器尺寸减去padding    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:       //省略..具体可自行参考源码        break;    // Parent asked to see how big we want to be    case MeasureSpec.UNSPECIFIED:       //省略...具体可自行参考源码        break;    }    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

上面方法也非常容易理解,大概是根据不同的父容器的模式及子View的layoutParams来决定子View的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的MeasureSpec和子View的LayoutParams的组合情况下所出现的不同的子View的MeasureSpec:

这里写图片描述

当子View的MeasureSpec获得后,我们返回measureChildWithMargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从ViewGroup转移到子View中了,可以看到传递的参数正是我们刚才获取的子View的MeasureSpec,接着会调用View#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onMeasure方法,当然了,对于不同类型的View,其onMeasure方法是不同的,但是对于不同的View,即使是自定义View,我们在重写的onMeasure方法内,也一定会调用到View#onMeasure方法的,因此我们看看它的源码:

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

显然,这里调用了setMeasureDimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getDefaultSize中获取,我们继续看看这个方法View#getDefaultSize:

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;    case MeasureSpec.AT_MOST:    case MeasureSpec.EXACTLY:        result = specSize;        break;    }    return result;}

好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看AT_MOST和EXACTLY模式,它直接把specSize返回了,即View在这两种模式下的测量宽高直接取决于specSize规格。也即是说,对于一个直接继承自View的自定义View来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法,对wrap_content属性进行处理。
接着,我们看UNSPECIFIED模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specSize,那么size从哪里来的呢?再往上看一层,它来自于getSuggestedMinimumWidth()或getSuggestedMinimumHeight(),我们选取其中一个方法,看看源码,View#getSuggestedMinimumWidth:

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

从以上逻辑可以看出,当View没有设置背景的时候,返回mMinWidth,该值对应于android:minWidth属性;如果设置了背景,那么返回mMinWidth和mBackground.getMinimumWidth中的最大值。那么mBackground.getMinimumWidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个Bitmap来说,它的原始宽度就是图片的尺寸。到此,子View的测量流程也完成了。

总结

这里简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

带着问题来思考整个measure过程

View框架的工作流程为:测量每个View大小(measure)–>把每个View放置到相应的位置(layout)–>绘制每个View(draw)

1、系统为什么要有measure过程?

开发人员在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。

官方文档截图:

这里写图片描述

所以这两个属性的值是必须要指定的,这两个属性的取值只能为三种类型:

  1. 固定的大小,比如100dp。

  2. 刚好包裹其中的内容,wrap_content。

  3. 想要和父布局一样大,match_parent / fill_parent。

由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。

试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。

2、measure过程都干了点什么事?

由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能确定。但是View尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,把各种尺寸值,经过计算,得到具体的像素值。measure过程会遍历整棵View树,然后依次测量每个View真实的尺寸。具体是每个ViewGroup会向它内部的每个子View发送measure命令,然后由具体子View的onMeasure()来测量自己的尺寸。最后测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的数据单位是像素。

3、对于自适应的尺寸机制,如何合理的测量一颗View树?

系统在遍历完布局文件后,针对布局文件,在内存中生成对应的View树结构,这个时候,整棵View树种的所有View对象,都还没有具体的尺寸,因为measure过程最终是要确定每个View打的准确尺寸,也就是准确的像素值。但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。

  1. 针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。

  2. 针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。

由于树这种数据结构的特殊性,我们在研究measure的过程时,可以只研究一个ViewGroup和2个View的简单场景。大概示意图如下:

这里写图片描述

也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果。

4、那么ViewGroup是如何向子View传递限制信息的?

谈到传递限制信息,那就是MeasureSpec类了,该类贯穿于整个measure过程,用来传递父布局对子View尺寸测量的约束信息。简单来说,该类就保存两类数据。

  1. 子View当前所在父布局的具体尺寸。

  2. 父布局对子View的限制类型。

那么限制类型又分为三种类型:

  1. UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)

  2. EXACTLY,精确的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。

  3. AT_MOST,最多的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。

源代码分析

在View的源代码中,提取到了下面一些关于measure过程的信息。

我们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,所以它是一个ViewGroup,所以整棵View树的测量是从一个ViewGroup对象的measure方法开始的。

View:

1、measure

/** 开始测量一个View有多大,parent会在参数中提供约束信息,实际的测量工作是在onMeasure()中进行的,该方法会调用onMeasure()方法,所以只有onMeasure能被也必须要被override */public final void measure(int widthMeasureSpec, int heightMeasureSpec);

父布局会在自己的onMeasure方法中,调用child.measure ,这就把measure过程转移到了子View中。

2、onMeasure

/** 具体测量过程,测量view和它的内容,来决定测量的宽高(mMeasuredWidth  mMeasuredHeight )。该方法中必须要调用setMeasuredDimension(int, int)来保存该view测量的宽高。 */protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

子View会在该方法中,根据父布局给出的限制信息,和自己的content大小,来合理的测量自己的尺寸。

3、setMeasuredDimension

/** 保存测量结果 */protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);

当View测量结束后,把测量结果保存起来,具体保存在mMeasuredWidth和mMeasuredHeight中。

ViewGroup:

1、measureChildren

/** 让所有子view测量自己的尺寸,需要考虑当前ViewGroup的MeasureSpec和Padding。跳过状态为gone的子view */protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

测量所有的子View尺寸,把measure过程交到子View内部。

2、measureChild

/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding。 */protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。

3、measureChildWithMargins

/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding、margins。 */protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。但是需要考虑到margin等信息。

4、getChildMeasureSpec

/** measureChildren过程中最困难的一部分,为child计算MeasureSpec。该方法为每个child的每个维度(宽、高)计算正确的MeasureSpec。目标就是把当前viewgroup的MeasureSpec和child的LayoutParams结合起来,生成最合理的结果。比如,当前ViewGroup知道自己的准确大小,因为MeasureSpec的mode为EXACTLY,而child希望能够match_parent,这时就会为child生成一个mode为EXACTLY,大小为ViewGroup大小的MeasureSpec。 */public static int getChildMeasureSpec(int spec, int padding, int childDimension);

根据当前自身的状况,以及特定子View的尺寸参数,为特定子View计算一个合理的限制信息。

源代码:
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;        }        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }
伪代码:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {        获取限制信息中的尺寸和模式。        switch (限制信息中的模式) {            case 当前容器的父容器,给当前容器设置了一个精确的尺寸:                if (子View申请固定的尺寸) {                    你就用你自己申请的尺寸值就行了;                } else if (子View希望和父容器一样大) {                    你就用父容器的尺寸值就行了;                } else if (子View希望包裹内容) {                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;                }                     break;            case 当前容器的父容器,给当前容器设置了一个最大尺寸:                if (子View申请固定的尺寸) {                    你就用你自己申请的尺寸值就行了;                } else if (子View希望和父容器一样大) {                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;                } else if (子View希望包裹内容) {                    你最大尺寸值为父容器的尺寸值,但是你还是要尽可能小的测量自己的尺寸,包裹你的内容就足够了;                }                     break;            case 当前容器的父容器,对当前容器的尺寸不限制:                if (子View申请固定的尺寸) {                    你就用你自己申请的尺寸值就行了;                } else if (子View希望和父容器一样大) {                    父容器对子View尺寸不做限制。                } else if (子View希望包裹内容) {                    父容器对子View尺寸不做限制。                }                    break;        } return 对子View尺寸的限制信息;    }

当自定义View的时候,也需要处理measure过程,主要有两种情况。

1、继承自View的子类。

需要覆写onMeasure来正确测量自己。最后都需要调用setMeasuredDimension来保存测量结果

一般来说,自定义View的measure过程伪代码为:

int mode = MeasureSpec.getMode(measureSpec);int size = MeasureSpec.getSize(measureSpec);int viewSize = 0;swith (mode) {    case MeasureSpec.EXACTLY:        viewSize = size; //当前View尺寸设置为父布局尺寸        break;    case MeasureSpec.AT_MOST:        viewSize = Math.min(size, getContentSize()); //当前View尺寸为内容尺寸和父布局尺寸当中的最小值        break;    case MeasureSpec.UNSPECIFIED:        viewSize = getContentSize(); //内容有多大,就设置尺寸为多大        break;    default:        break;}setMeasuredDimension(viewSize);

2、继承自ViewGroup的子类。

不但需要覆写onMeasure来正确测量自己,可能还要覆写一系列measureChild方法,来正确的测量子view,比如ScrollView。或者干脆放弃父类实现的measureChild规则,自己重新实现一套测量子view的规则,比如RelativeLayout。最后都需要调用setMeasuredDimension来保存测量结果。

一般来说,自定义ViewGroup的measure过程的伪代码为:

//ViewGroup开始测量自己的尺寸viewGroup.onMeasure();//ViewGroup为每个child计算测量限制信息(MeasureSpec)viewGroup.getChildMeasureSpec();//把上一步生成的限制信息,传递给每个子View,然后子View开始measure自己的尺寸child.measure();//子View测量完成后,ViewGroup就可以获取每个子View测量后的尺寸child.getChildMeasuredSize();//ViewGroup根据自己自身状况,比如Padding等,计算自己的尺寸viewGroup.calculateSelfSize();//ViewGroup保存自己的尺寸viewGroupsetMeasuredDimension();

案例分析

很多开发人员都遇到过这种需求,就是ScrollView内部嵌套ListView,而该ListView数据条数是不确定的,所以需要设置为包裹内容,然后就会发现ListView就会显示第一行出来。然后就会百度到一条解决方案,继承ListView,覆写onMeasure方法。

 @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);        super.onMeasure(widthMeasureSpec, expandSpec);    }

问题是解决了,但是很多开发人员并不知道为什么。

下面会从ScrollView和ListView的measure过程来分析一下。

1、为什么会出现上述问题?

备注:截取部分问题相关代码,并不是完整代码。

看看ListView的onMeasure:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        final View child = obtainView(0, mIsScrap);        childHeight = child.getMeasuredHeight();        if (heightMode == MeasureSpec.UNSPECIFIED) {            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;            if (heightMode == MeasureSpec.AT_MOST) {                // TODO: after first layout we should maybe start at the first visible position, not 0                 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);            }            setMeasuredDimension(widthSize, heightSize);            mWidthMeasureSpec = widthMeasureSpec;        }    }

当MeasureSpec mode为UNSPECIFIED的时候,只测量第一个item打的高度,跟问题描述相符,所以我们猜测可能是因为ScrollView传递了一个UNSPECIFIED限制给ListView。

再来看ScrollView的onMeasure代码:

 @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }

调用了父类的onMeasure:

看看FrameLayout的onMeasure:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (mMeasureAllChildren || child.getVisibility() != GONE) {                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);            }        }    }

调用了measureChildWithMargins,但是因为ScrollView覆写了该方法,所以看看ScrollView的measureChildWithMargins方法:

@Override    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,                                            int parentHeightMeasureSpec, int heightUsed) {        final int childHeightMeasureSpec =                 MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec),                         MeasureSpec.UNSPECIFIED);        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

果然,它向ListView的onMeasure传递了一个UNSPECIFIED的限制。

为什么呢,想想,因为ScrollView,本来就是可以在竖直方向滚动的布局,所以,它对它所有的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,因为我本来就是需要竖直滑动的,它的本意就是如此,所以它对子View高度不做任何限制。

2、为什么这种解决方法可以解决这个问题?

看看ListView的onMeasure:

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        final View child = obtainView(0, mIsScrap);        childHeight = child.getMeasuredHeight();        if (heightMode == MeasureSpec.UNSPECIFIED) {            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;            if (heightMode == MeasureSpec.AT_MOST) {                // TODO: after first layout we should maybe start at the first visible position, not 0                 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);            }            setMeasuredDimension(widthSize, heightSize);            mWidthMeasureSpec = widthMeasureSpec;        }    }

只要让heightMode == MeasureSpec.AT_MOST,它就会测量它的完整高度,所以第一个数据,限制mode的值就确定下来了。第二个数据就是尺寸上限,如果给个200,那么当ListView数据过多的时候,该ListView最大高度就是200了,还是不能完全显示内容,怎么办?那么就给个最大值吧,最大值是多少呢,Integer.MAX_VALUE?

先看一下MeasureSpec的代码说明:

        private static final int MODE_SHIFT = 30;        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;

他用最高两位存储mode,用其他剩余未存储size。所以Integer.MAX_VALUE >> 2,就是限制信息所能携带的最大尺寸数据。所以最后就需要用这两个值做成一个限制信息,传递给ListView的height维度。

也就是如下代码:

   @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);        super.onMeasure(widthMeasureSpec, expandSpec);    }

自己动手

下面我们自己写一个自定义的ViewGroup,让它内部的每一个子View都垂直排布,并且让每一个子View的左边界都距离上一个子View的左边界一定的距离。并且支持wrap_content。大概看起来如下图所示:

这里写图片描述

实际运行效果如下图所示:

这里写图片描述

代码如下:

public class VerticalOffsetLayout extends ViewGroup {    private static final int OFFSET = 100;    public VerticalOffsetLayout(Context context) {        super(context);    }    public VerticalOffsetLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int width = 0;        int height = 0;        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View child = getChildAt(i);            ViewGroup.LayoutParams lp = child.getLayoutParams();            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);            child.measure(childWidthSpec, childHeightSpec);        }        switch (widthMode) {            case MeasureSpec.EXACTLY:                width = widthSize;                break;            case MeasureSpec.AT_MOST:            case MeasureSpec.UNSPECIFIED:                for (int i = 0; i < childCount; i++) {                    View child = getChildAt(i);                    int widthAddOffset = i * OFFSET + child.getMeasuredWidth();                    width = Math.max(width, widthAddOffset);                }                break;            default:                break;        }        switch (heightMode) {            case MeasureSpec.EXACTLY:                height = heightSize;                break;            case MeasureSpec.AT_MOST:            case MeasureSpec.UNSPECIFIED:                for (int i = 0; i < childCount; i++) {                    View child = getChildAt(i);                    height = height + child.getMeasuredHeight();                }                break;            default:                break;        }        setMeasuredDimension(width, height);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int left = 0;        int right = 0;        int top = 0;        int bottom = 0;        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View child = getChildAt(i);            left = i * OFFSET;            right = left + child.getMeasuredWidth();            bottom = top + child.getMeasuredHeight();            child.layout(left, top, right, bottom);            top += child.getMeasuredHeight();        }    }}

参考

  • Android View 测量流程(Measure)完全解析

  • android绘制view的过程之一———计算view大小(measure)

  • Android View框架的measure机制

0 0
原创粉丝点击