Android学习笔记之MeasureSpec

什么是MeasureSpec

Android系统在绘制View的时候,过程是十分复杂的,其中频繁的使用到了MeasureSpec。那么MeasureSpec是什么?有什么用?简单点说,它是一个int值的中间变量,用来存储View的尺寸规格。再说细点,在测量过程中,系统会将View的LayoutParams根据父容器所施加的约束规则转换成对应的MeasureSpec。

MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测试模式,而SpecSize是指在某中测试模式下的规格大小。源码如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    //高两位11
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //高两位00
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //高两位01
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //高两位10
    public static final int AT_MOST     = 2 << MODE_SHIFT;
   
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
   
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //取高2位得模式
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        //取低30位得规格大小
        return (measureSpec & ~MODE_MASK);
    }
}

什么是约束规则

上面解释MeasureSpec时,提及到父容器的约束规则,可能会让我们联想到 match_parent 和 wrap_content,但这里说的约束规则并不完全是这样。具体来说,约束规则指的是MeasureSpec中的三种模式,即SpecMode,它是通过与 match_parent 和 wrap_content 相关的逻辑判断最后决定下来的。每一种模式的含义如下:

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测试状态。
  • EXACTLY:表示View的规格为确切值,即SpecSize所指定的值。它对应于match_parent和具体的数值这两种模式。
  • AT_MOST:表示View的规格不能超过某个值,具体是什么值要看不同View的具体实现。它对应于wrap_content。

MeasureSpec和LayoutParams的关系

上面提到, 在测量过程中,系统会将View的LayoutParams根据父容器所施加的约束规则转换成对应的MeasureSpec。这里需要注意的是,对于顶级View(DecorView)来说,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定;对于普通View来说,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。

对DecorView来说,创建MeasureSpec的源码如下:(其中desireWindowWidth和desireWindowHeight是屏幕的尺寸)

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:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

通过上述代码可以得出以下结论:

  • 如果DecorView的LayoutParams中的宽/高参数为match_parent,那么它的MeasureSpec的模式为精确模式,尺寸为窗口大小
  • 如果DecorView的LayoutParams中的宽/高参数为wrap_content,那么它的MeasureSpec的模式为最大模式,最大尺寸为窗口大小
  • 如果DecorView的LayoutParams中的宽/高参数为具体值(例如100dp),那么它的MeasureSpec的模式为精确模式,尺寸为指定的具体值(100dp)

对普通View来说,View的measure过程是由它的父容器ViewGroup调用的,如下:

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);
}

可以看到,父容器获取子元素的布局参数之后,通过getChildMeasureSpec方法获取子元素的宽高MeasureSpec,然后调用子元素的measure方法。接着我们来看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);
}

上述代码不难理解,总的思路是按照父容器的SpecMode分为三个分支,然后在每个分之中结合子元素本身的LayoutParams来确定子元素的MeasureSpec。通过上述代码可以得出以下结论:

  • 当子View采用固定宽/高时,不管父容器的MeasureSpec是什么模式,View的MeasureSpec都是精确模式并且大小为LayoutParams中指定的具体值
  • 当子View的宽/高是 match_parent 时,如果父容器的模式是精确模式,那么子View也是精确模式并且大小是父容器的剩余空间;如果父容器的模式是最大模式,那么子View也是最大模式并且大小不会超过父容器的剩余空间
  • 当子View的宽/高是 wrap_content 时,不管父容器是最大模式还是精确模式,子View的模式总是最大模式并且大小不会超过父容器的剩余空间

PS:上面的总结没有考虑UNSPECIFIED模式,是因为该模式主要用于系统内部多次Measure的情形,通常情况下我们不需要关注它。

猜你喜欢

转载自blog.csdn.net/Ein3614/article/details/82942827
今日推荐