zoukankan      html  css  js  c++  java
  • 源码解析Android中View的measure量算过程

    Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算、布局、绘图的总体机制可参见博文《 Android中View的布局及绘图机制》。量算是布局和绘图的基础,所以量算是很重要的一个环节。本文将从源码角度解析View的量算过程,这其中会涉及某些关键类以及关键方法。

    对View进行量算的目的是让View的父控件知道View想要多大的尺寸。


    量算过程概述

    如果要进行量算的View是ViewGroup类型,那么ViewGroup会在onMeasure方法内会遍历子View依次进行量算,本文重点说明非ViewGroup的View的量算过程,因为我们一旦了解了非ViewGroup的View的量算过程,ViewGroup的量算理解起来就要简单许多,主要是ViewGroup在其内部对子View再依次执行量算。

    整个应用量算的起点是ViewRootImpl类,从它开始依次对子View进行量算,如果子View是一个ViewGroup,那么又会遍历该ViewGroup的子View依次进行量算。也就是说,量算会从View树的根结点,纵向递归进行,从而实现自上而下对View树进行量算,直至完成对叶子节点View的量算。

    那么到底如何对一个View进行量算呢?Android通过调用View的measure()方法对View进行量算,让该View的父控件知道该View想要多大的尺寸空间。

    具体来说,View的父控件ViewGroup会调用View的measure方法,ViewGroup会将一些宽度和高度的限制条件传递给View的measure方法。

    在View的measure方法会首先从成员变量中读取以前缓存过的量算结果,如果能找到该缓存值,那么就基本完事了,如果没有找到缓存值,那么measure方法会执行onMeasure回调方法,measure方法会将上述的宽度和高度的限制条件依次传递给onMeasure方法。onMeasure方法会完成具体的量算工作,并将量算的结果通过调用View的setMeasuredDimension方法保存到View的成员变量mMeasuredWidth 和mMeasuredHeight中。

    量算完成之后,View的父控件就可以通过调用getMeasuredWidth、getMeasuredState、getMeasuredWidthAndState这三个方法获取View的量算结果。

    以上就是非ViewGroup类型的View量算的总体过程。


    MeasureSpec简介

    上面我们提到ViewGroup在调用View的measure方法时,会传入ViewGroup对View的宽度及高度的限制条件,这是合理的,例如ViewGroup的空间有限,它需要告诉子View要量算的尺寸的上限。

    上面提到的尺寸的限制条件就是MeasureSpec,它可以通过一个Int类型的值来表示的,该Int值会同时包含两种信息:mode和size,即模式和尺寸。我们知道Java中Int类型的值是4个字节的,Android会用第一个高位字节存储mode,然后用剩余的三个字节存储size。

    View有一个静态内部类MeasureSpec,该类有几个静态方法以及静态常量,我们可以用这些方法将mode和size打包成一个Int值或者是从一个Int值中解析出mode和size。

    假设我们已有了一个包含MeasureSpec信息的Int值measureSpec,那么

    通过调用MeasureSpec.getSize(int measureSpec)即可从measureSpec解析出三个字节所包含的尺寸size信息,该方法返回Int类型,也就是说我们得到的size实际上就是对原有的measureSpec的高位字节的8个二进制位都设置为0,该方法的返回值size虽然也是4个字节的Int值,但是已经完全不包含mode信息。

    通过调用MeasureSpec.getMode(int measureSpec)即可从measureSpec解析出高位字节所包含的模式mode信息,该方法返回Int类型,也就是说我们得到的mode实际上对原有的measureSpec的低位的三个字节的24个二进制码都设置为0,该方法的返回值mode虽然也是4个字节的Int值,但是已经完全不包含size信息。

    对于尺寸size,我们很好理解,比如表示某个宽度值或者表示某个高度值。那么mode是什么呢?

    mode的取值有三种,分别是:

    MeasureSpec.AT_MOST,即0x80000000,该值表示View最大可以取其父ViewGroup给其指定的尺寸,例如现在有个Int值widthMeasureSpec,ViewGroup将其传递给了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是200,那么表示View能取的最大的宽度是200。

    MeasureSpec.EXACTLY,即0x40000000,该值表示View必须使用其父ViewGroup指定的尺寸,还是以widthMeasureSpec为例,如果其mode值是EXACTLY,size是200,那么表示View的宽度必须是200,不多不少才行。

    MeasureSpec.UNSPECIFIED,即0x00000000,该值表示View的父ViewGroup没有给View在尺寸上设置限制条件,这种情况下View可以忽略measureSpec中的size,View可以取自己想要的值作为量算的尺寸。

    更多信息可参考API文档 android/view/View.MeasureSpec。


    measure方法

    measure()的方法签名是public final void measure(int widthMeasureSpec, int heightMeasureSpec)

    当View的父控件ViewGroup对View进行量算时,会调用View的measure方法,ViewGroup会传入widthMeasureSpec和heightMeasureSpec,分别表示父控件对View的宽度和高度的一些限制条件。

    measure方法的源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    <code class="hljs" cs="">public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //首先判断当前View的layoutMode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            //LAYOUT_MODE_OPTICAL_BOUNDS是特例情况,比较少见
            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);
        }
     
        //根据widthMeasureSpec和heightMeasureSpec计算key值,我们在下面用key值作为键,缓存我们量算的结果
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
     
        //mMeasureCache是LongSparseLongArray类型的成员变量,
        //其缓存着View在不同widthMeasureSpec、heightMeasureSpec下量算过的结果
        //如果mMeasureCache为空,我们就新new一个对象赋值给mMeasureCache
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
     
        //mOldWidthMeasureSpec和mOldHeightMeasureSpec分别表示上次对View进行量算时的widthMeasureSpec和heightMeasureSpec
        //执行View的measure方法时,View总是先检查一下是不是真的有必要费很大力气去做真正的量算工作
        //mPrivateFlags是一个Int类型的值,其记录了View的各种状态位
        //如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
        //那么表示当前View需要强制进行layout(比如执行了View的forceLayout方法),所以这种情况下要尝试进行量算
        //如果新传入的widthMeasureSpec/heightMeasureSpec与上次量算时的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
        //那么也就是说该View的父ViewGroup对该View的尺寸的限制情况有变化,这种情况下要尝试进行量算
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
     
            //通过按位操作,重置View的状态mPrivateFlags,将其标记为未量算状态
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
     
            //对阿拉伯语、希伯来语等从右到左书写、布局的语言进行特殊处理
            resolveRtlPropertiesIfNeeded();
     
            //在View真正进行量算之前,View还想进一步确认能不能从已有的缓存mMeasureCache中读取缓存过的量算结果
            //如果是强制layout导致的量算,那么将cacheIndex设置为-1,即不从缓存中读取量算结果
            //如果不是强制layout导致的量算,那么我们就用上面根据measureSpec计算出来的key值作为缓存索引cacheIndex。
            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
     
            //sIgnoreMeasureCache是一个boolean类型的成员变量,其值是在View的构造函数中计算的,而且只计算一次
            //一些老的App希望在一次layou过程中,onMeasure方法总是被调用,
            //具体来说其值是通过如下计算的: sIgnoreMeasureCache = targetSdkVersion < KITKAT;
            //也就是说如果targetSdkVersion的API版本低于KITKAT,即API level小于19,那么sIgnoreMeasureCache为true
     
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //如果运行到此处,表示我们没有从缓存中找到量算过的尺寸或者是sIgnoreMeasureCache为true导致我们要忽略缓存结果
                //此处调用onMeasure方法,并把尺寸限制条件widthMeasureSpec和heightMeasureSpec传入进去
                //onMeasure方法中将会进行实际的量算工作,并把量算的结果保存到成员变量中
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                //onMeasure执行完后,通过位操作,重置View的状态mPrivateFlags,将其标记为在layout之前不必再进行量算的状态
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                //如果运行到此处,那么表示当前的条件允许View从缓存成员变量mMeasureCache中读取量算过的结果
                //用上面得到的cacheIndex从缓存mMeasureCache中取出值,不必在调用onMeasure方法进行量算了
                long value = mMeasureCache.valueAt(cacheIndex);
                //一旦我们从缓存中读到值,我们就可以调用setMeasuredDimensionRaw方法将当前量算的结果到成员变量中
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
     
            //如果我们自定义的View重写了onMeasure方法,但是没有调用setMeasuredDimension()方法,
            //那么此处就会抛出异常,提醒开发者在onMeasure方法中调用setMeasuredDimension()方法
            //Android是如何知道我们有没有在onMeasure方法中调用setMeasuredDimension()方法的呢?
            //方法很简单,还是通过解析状态位mPrivateFlags。
            //setMeasuredDimension()方法中会将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET状态,即已量算状态,
            //此处就检查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET状态即可判断setMeasuredDimension是否被调用
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException(View with id  + getId() + :
                        + getClass().getName() + #onMeasure() did not set the
                        +  measured dimension by calling
                        +  setMeasuredDimension());
            }
     
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
     
        //mOldWidthMeasureSpec和mOldHeightMeasureSpec保存着最近一次量算时的MeasureSpec,
        //在量算完成后将这次新传入的MeasureSpec赋值给它们
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
     
        //最后用上面计算出的key作为键,量算结果作为值,将该键值对放入成员变量mMeasureCache中,
        //这样就实现了对本次量算结果的缓存,以便在下次measure方法执行的时候,有可能将其从中直接读出,
        //从而省去实际量算的步骤
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL);
    }</code>

    上面的注释对每行代码都进行了详细的说明,如果大家仔细读了的话,相信能一目了然,这里根据上面的注释简单总结一下measure方法都干了什么事:

    首先,我们要知道并不是只要View的measure方法执行的时候View就一定要傻傻的真的去做量算工作,View也喜欢偷懒,如果View发现没有必要去量算的话,那它就不会真的去做量算的工作。

    具体来说,View先查看是不是要强制量算以及这次measure中传入的MeasureSpec与上次量算的MeasureSpec是否相同,如果不是强制量算或者MeasureSpec与上次的量算的MeasureSpec相同,那么View就不必真的去量算了。

    如果不满足上述条件,View就考虑去做量算工作。但是在量算之前,View还想偷懒,它会以MeasureSpec计算出的key值作为键,去成员变量mMeasureCache中查找是否缓存过对应key的量算结果,如果能找到,那么就简单调用一下setMeasuredDimensionRaw方法,将从缓存中读到的量算结果保存到成员变量mMeasuredWidth和mMeasuredHeight中。

    如果不能从mMeasureCache中读到缓存过的量算结果,那么这次View就真的不能再偷懒了,只能乖乖地调用onMeasure方法去完成实际的量算工作,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec传递给onMeasure方法。关于onMeasure方法,我们会在下面详细介绍。

    不论上面代码走了哪个判断的分支,最终View都会得到量算的结果,并且将结果缓存到成员变量mMeasureCache中,以便下次执行measure方法时能够从其中读取缓存值。

    需要说明的是,View有一个成员变量mPrivateFlags,用以保存View的各种状态位,在量算开始前,会将其设置为未量算状态,在量算完成后会将其设置为已量算状态。


    onMeasure方法

    我们在上面提到,当View在measure方法中发现不得不进行实际的量算工作时,将会调用onMeasure方法,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec作为参数传递给onMeasure方法。View的onMeasure方法不是空方法,它提供了一个默认的具体实现。
    onMeasure方法的代码如下:

    1
    2
    3
    4
    5
    6
    7
    <code class="hljs" cs="">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //onMeasure调用了setMeasuredDimension方法,
        //setMeasuredDimension又需要调用getDefaultSize方法,
        //getDefaultSize又需要调用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }</code>

    我们发现onMeasure方法中会调用setMeasuredDimension方法,setMeasuredDimension又需要调用getDefaultSize方法,getDefaultSize又需要调用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法,即
    setMeasuredDimension -> getDefaultSize -> getSuggestedMinimumWidth/Height

    那我们就先研究getSuggestedMinimumWidth/Height,然后再依次研究getDefaultSize和setMeasuredDimension,这样就能把onMeasure方法搞明白了。其实getSuggestedMinimumWidth和getSuggestedMinimumHeight的实现逻辑基本一样,我们此处只研究getSuggestedMinimumWidth方法即可。


    getSuggestedMinimumWidth方法

    getSuggestedMinimumWidth用于返回View推荐的最小宽度,其代码如下所示:

    1
    2
    3
    4
    5
    <code class="hljs" cs="">protected int getSuggestedMinimumWidth() {
        //如果没有给View设置背景,那么就返回View本身的最小宽度mMinWidth
        //如果给View设置了背景,那么就取View本身最小宽度mMinWidth和背景的最小宽度的最大值
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }</code>

    如果没有给View设置背景,那么就返回View本身的最小宽度mMinWidth

    如果给View设置了背景,那么就取View本身最小宽度mMinWidth和背景的最小宽度的最大值

    那你可能有疑问,View中保存的最小宽度mMinWidth的值是从哪来的呢?实际上有两种办法给View设置最小宽度。

    第一种情况是,mMinWidth是在View的构造函数中被赋值的,View通过读取XML中定义的minWidth的值来设置View的最小宽度mMinWidth,以下代码片段是View构造函数中解析minWidth的部分:

    1
    2
    3
    4
    5
    <code bash="" class="hljs">//遍历到XML中定义的minWith属性
    case R.styleable.View_minWidth:
    //读取XML中定义的属性值作为mMinWidth,如果XML中未定义,则设置为0
    mMinWidth = a.getDimensionPixelSize(attr, 0);
    break;</code>

    第二种情况是调用View的setMinimumWidth方法给View的最小宽度mMinWidth赋值,setMinimumWidth方法的代码如下所示:

    1
    2
    3
    4
    <code class="hljs" cs="">public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }</code>

    这样我们就搞明白了getSuggestedMinimumWidth方法是怎么执行的了,getSuggestedMinimumHeight方法与其逻辑完全一致,只不过是把宽度换成了高度,在此就不再赘述了。


    getDefaultSize

    我们在onMeasure方法中发现,onMeasure会执行以下两行代码:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

    我们已经研究了getSuggestedMinimumWidth/Height,知道其会返回View的最小宽度和高度,现在我们开始研究getDefaultSize方法。

    Android会将View想要的尺寸以及其父控件对其尺寸限制信息measureSpec传递给getDefaultSize方法,该方法要根据这些综合信息计算最终的量算的尺寸。

    其源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <code class="hljs" cs="">public static int getDefaultSize(int size, int measureSpec) {
        //size表示的是View想要的尺寸信息,比如最小宽度或最小高度
        int result = size;
        //从measureSpec中解析出specMode信息
        int specMode = MeasureSpec.getMode(measureSpec);
        //从measureSpec中解析出specSize信息,不要将specSize与上面的size变量搞混
        int specSize = MeasureSpec.getSize(measureSpec);
     
        switch (specMode) {
        //如果mode是UNSPECIFIED,表示View的父ViewGroup没有给View在尺寸上设置限制条件
        case MeasureSpec.UNSPECIFIED:
            //此处当mode是UNSPECIFIED时,View就直接用自己想要的尺寸size作为量算的结果
            result = size;
            break;
        //如果mode是UNSPECIFIED,那么表示View最大可以取其父ViewGroup给其指定的尺寸
        //如果mode是EXACTLY,那么表示View必须使用其父ViewGroup指定的尺寸
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            //此处mode是UNSPECIFIED或EXACTLY时,View就用其父ViewGroup指定的尺寸作为量算的结果
            result = specSize;     
            break;
        }
        return result;
    }</code>

    通过以上代码,我们就会发现View的父ViewGroup传递给View的限制条件measureSpec的作用在该方法中体现的淋漓尽致。

    首先根据measuredSpec解析出对应的specMode和specSize

    当mode是UNSPECIFIED时,View就直接用自己想要的尺寸size作为量算的结果

    当mode是UNSPECIFIED或EXACTLY时,View就用其父ViewGroup指定的尺寸作为量算的结果

    最终,View会根据measuredSpec限制条件,得到最终的量算的尺寸。

    这样在onMeasure方法中,
    当执行getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)时,我们就得到了最终量算到的宽度值;
    当执行getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)时,我们就得到了最终量算到的高度值。


    setMeasuredDimension

    在前面我们研究onMeasure方法时就已经看到setMeasuredDimension会调用getDefaultSize方法,会将已经量算到的宽度值和高度值作为参数传递给setMeasuredDimension方法,我们研究一下该方法。

    其源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <code class="hljs" java="">protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            //layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情况,我们不考虑
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
     
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        //最终调用setMeasuredDimensionRaw方法,将量算结果传入进去
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }</code>

    该方法会在开始判断layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情况,这种特例很少见,我们直接忽略掉。

    setMeasuredDimension方法最后将量算的结果传递给方法setMeasuredDimensionRaw,我们再研究一下setMeasuredDimensionRaw这方法。


    setMeasuredDimensionRaw

    setMeasuredDimensionRaw接收两个参数,分别是已经量算完成的宽度和高度。

    其源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    <code class="hljs" cs="">private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        //将量算完成的宽度measuredWidth保存到View的成员变量mMeasuredWidth中
        mMeasuredWidth = measuredWidth;
        //将量算完成的高度measuredHeight保存到View的成员变量mMeasuredHeight中
        mMeasuredHeight = measuredHeight;
        //最后将View的状态位mPrivateFlags设置为已量算状态
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }</code>

    我们发现,在该方法中做了三件事:

    将量算完成的宽度measuredWidth保存到View的成员变量mMeasuredWidth中

    将量算完成的高度measuredHeight保存到View的成员变量mMeasuredHeight中

    最后将View的状态位mPrivateFlags设置为已量算状态


    量算完成的尺寸的state

    至此,View的量算过程就完成了,但是View的父ViewGroup如何读取到View量算的结果呢?

    为此,View提供了三组方法,分别是:
    1. getMeasuredWidth和getMeasuredHeight方法
    2. getMeasuredWidthAndState和getMeasuredHeightAndState方法
    3. getMeasuredState方法

    有些人可能会纳闷,只要有了第一组方法不就行了吗?后面那两组方法有啥用?

    此处我们要再仔细研究一下View中保存量算结果的成员变量mMeasuredWidth和mMeasuredHeight,下面的讨论我们都只讨论宽度,理解了宽度的处理方式,高度也是完全一样的。

    mMeasuredWidth是一个Int类型的值,其是由4个字节组成的。

    我们先假设mMeasuredWidth只存储了量算完成的宽度信息,而且View的父ViewGroup可以通过相关方法得到该值。但是存在这样一种情况:View在量算时,父ViewGroup给其传递的widthMeasureSpec中的specMode的值是AT_MOST,specSize是100,但是View的最小宽度是200,显然父ViewGroup指定的specSize不能满足View的大小,但是由于specMode的值是AT_MOST,View在getDefaultSize方法中不得不妥协,只能含泪将量算的最终宽度设置为100。然后其父ViewGroup通过某些方法获取到该View的量算宽度为100时,ViewGroup以为子View只需要100就够了,最终给了子View宽度为100的空间,这就导致了在UI界面上View特别窄,用户体验也就不好。

    Android为让其View的父控件获取更多的信息,就在mMeasuredWidth上下了很大功夫,虽然是一个Int值,但是想让它存储更多信息,具体来说就是把mMeasuredWidth分成两部分:

    其高位的第一个字节为第一部分,用于标记量算完的尺寸是不是达到了View想要的宽度,我们称该信息为量算的state信息。 其低位的三个字节为第二部分,用于存储实际的量算到的宽度。

    由此我们可以看出Android真是物尽其用,一个变量能包含两个信息,这个有点类似于measureSpec的道理,但是二者又有不同:

    measureSpec是将限制条件mode从ViewGroup传递给其子View。 mMeasuredWidth、mMeasuredHeight是将带有量算结果的state标志位信息从View传递给其父ViewGroup。

    那么你可能会问,在本文中我们没看到对mMeasuredWidth的高位字节进行特殊处理啊?我们下面看一下View中的resolveSizeAndState方法。


    resolveSizeAndState

    resolveSizeAndState方法与getDefaultSize方法类似,其内部实现的逻辑是一样的,但是又有区别,getDefaultSize仅仅返回最终量算的尺寸信息,但resolveSizeAndState除了返回最终尺寸信息还会有可能返回量算的state标志位信息。

    resolveSizeAndState方法的源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <code class="hljs" java="">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) {
                    //当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,
                    //我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记
                    //这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了,
                    //然后可能分配更大一点的尺寸给子View
                    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);
    }</code>

    当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记,这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了,然后可能分配更大一点的尺寸给子View。

    getDefaultSize方法只是onMeasure方法中获取最终尺寸的默认实现,其返回的信息比resolveSizeAndState要少,那么什么时候才会调用resolveSizeAndState方法呢? 主要有两种情况:

    Android中的许多layout类都调用了resolveSizeAndState方法,比如LinearLayout在量算过程中会调用resolveSizeAndState方法而非getDefaultSize方法。 我们自己在实现自定义的View或ViewGroup时,我们可以重写onMeasure方法,并在该方法内调用resolveSizeAndState方法。


    getMeasuredXXX系列方法

    现在我们再回过头来看以下三组方法:

    getMeasuredWidth和getMeasuredHeight方法
    该组方法只返回量算结果中的的尺寸信息,去掉了高位字节的state信息,以getMeasuredWidth方法为例,其源码如下:

    1
    2
    3
    4
    5
    <code class="hljs" java="">public final int getMeasuredWidth() {
        //MEASURED_SIZE_MASK的值为0x00ffffff,用mMeasuredWidth与掩码MEASURED_SIZE_MASK进行按位与运算,
        //可以将返回值中的高位字节的8个bit位全置为0,从而去掉了高位字节的state信息
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }</code>

    MEASURED_SIZE_MASK的值为0x00ffffff,用mMeasuredWidth与掩码MEASURED_SIZE_MASK进行按位与运算,可以将返回值中的高位字节的8个bit位全置为0,从而去掉了高位字节的state信息

    getMeasuredWidthAndState和getMeasuredHeightAndState方法
    该组方法返回的量算结果中同时包含尺寸和state信息(如果state存在的话),以getMeasuredWidthAndState方法为例,其源码如下所示:

    1
    2
    3
    4
    <code class="hljs" java="">public final int getMeasuredWidthAndState() {
        //该方法直接返回成员变量mMeasuredWidth,因为mMeasuredWidth本身已经包含了尺寸以及可能的state信息
        return mMeasuredWidth;
    }</code>

    该方法直接返回成员变量mMeasuredWidth,因为mMeasuredWidth本身已经包含了尺寸以及可能的state信息

    getMeasuredState方法
    该方法返回的Int值中同时包含宽度量算的state以及高度量算的state,不包含任何的尺寸信息,其源码如下所示:

    1
    2
    3
    4
    5
    6
    7
    <code class="hljs" java="">public final int getMeasuredState() {
        //将宽度量算的state存储在Int值的第一个字节中,即高位首字节
        //将高度量算的state存储在Int值的第三个字节中
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }</code>

    我们简单分析一下以上代码:

    掩码MEASURED_STATE_MASK的值为常量0xff000000,其高位字节的8个bit位全为1,剩余低位字节的三个字节的24个bit位全为0

    MEASURED_HEIGHT_STATE_SHIFT的值为常量16

    当执行(mMeasuredWidth&MEASURED_STATE_MASK)时,将mMeasuredWidth与MEASURED_STATE_MASK进行按位与操作,该表达式的值高位字节保留了量算后宽度的state,过滤掉了其低位三个字节所存储的宽度size

    由于我们已经用高位首字节存储了量算后宽度的state,所以高度的state就不能存储在高位首字节了。Android打算把它存储在第三个字节中。(mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)表示将mMeasuredHeight向右移16位,这样高度的state字节就从原来的第一个字节右移动到了第三个字节,由于高度的state向右移动了,所以其对应的掩码也有相应移动。(MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)表示state的掩码也从第一个字节右移16位到了第三个字节,即掩码从0xff000000变成了0x0000ff00。然后用右移后的state与右移后的掩码执行按位与操作,这样就在第三个字节保留了高度的state信息,并且过滤掉了第1、2、4字节中的信息,即将这三个字节中的24个bit位置为0。

    最后,将我们得到的宽度的state与高度的state进行按位或操作,这样就将宽度和高度的state都保存在一个Int值中:第一个字节存储宽度的state,第三个字节存储高度的state。

  • 相关阅读:
    css 左右两栏 左边固定右边自适应
    Oracle 执行长SQL
    Oracle 游标基础
    ERROR 000464: Cannot get exclusive schema lock. Either being edited or in use by another application
    MyEclipse 批量更新编码集
    使用Windows服务发布WCF服务【转载】
    JavaScript的陷阱【转载】
    JS图片切换带超链接
    新正则表达式
    asp.net垃圾代码之asp.net去掉垃圾代码,优化aspx页面性能的方法
  • 原文地址:https://www.cnblogs.com/xiaorenwu702/p/5185436.html
Copyright © 2011-2022 走看看