Measure要知道的点

作者: 求闲居士 | 来源:发表于2018-12-19 17:25 被阅读5次

    1.measure和onMeasure

    View中和测量过程相关的方法有三个,measure、onMeasure和setMeasuredDimension。


    image

    1.View与ViewGroup的不同

    • View的measure调用onMeasure.
    • View 中的onMeasure只调用了setMeasuredDimension来设置自身高度。
    • 在ViewGroup中,onMeasure方法并没有被重写法,所以继承ViewGroup自定义ViewGroup一定要重写onMeasure来测量子View,否则不会测量子View。
    • 在自定义View中,onMeasure方法是在一般情况下使用父容器提供的宽高,除了UNSPECIFIED情况下使用最小尺度。继承View的自定义要根据业务onMeasure要选择性重写

    2.View的onMeasure

    2.1 原生的onMeasure

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

    其中获取宽高的主要方法是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;
    }
    

    当父容器传来的MeasureSpec为UNSPECIFIED时,数值取自己的最小值;为AT_MOST和EXACTLY时,取父容器传递的数值,也就是Wrap_Content和Match_Paraent

    2.1.2 setMeasuredDimension

    setMeasuredDimension方法是设置view宽高的方法,也是onMeasure必须要调用的方法

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
    
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    

    setMeasuredDimensionRaw是最终被调用的方法,保存宽高,修改标识符。

    如果在onMeasure中没有调用setMeasuredDimension来设置宽高,在measure中,调用onMeasure后,会通过mPrivateFlags来判断是否设置了宽高,没有设置就会抛出异常。所以说setMeasuredDimension是必须要在onMeasure中被调用

    2.2 自定义View重写onMeasure

    从getDefaultSize方法实现来看,直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。

    使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制(当然,如果你想用自己的方式来满足父 View 的限制也行)。

    public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }
    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) {
                //标记量算完的尺寸没有达到了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);
    }
    

    相比原生的的onMeasure中的getDefaultSize,resolveSize对于Wrap_Content情况有了特别处理,在不大于父容器要求的尺寸下,使用用户自己规定的尺寸。

    上面的result就是保存的measureSpec的mode值,唯一不同的是第31位被用于标示尺寸是不是达到了View想要的宽度,如果不满足,则标为1。下面把2位标示直观表示下:

    UNSPECIFIED 00
    EXACTLY     01
    AT_MOST     10
    result = AT_MOST | MEASURED_STATE_TOO_SMALL  11
    

    resolveSizeAndState的结果在resolveSize进行了截取,只取了数值部分,用来标记量算完的尺寸是不是达到了View想要的宽度的高八位没有保留。

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    public final int getMeasuredWidthAndState() {
        return mMeasuredWidth;
    }
    

    而getMeasuredWidth和getMeasuredWidthAndState的区别也在此,getMeasuredWidth是获取存数值,而getMeasuredWidthAndState还带有高位state。

    线性布局中就用了resolveSizeAndState来获取宽高。自己使用时可配合父容器使用。

    3.ViewGroup的onMeasure

    虽然ViewGroup没有实现omMeasure的过程,但是它提供了两个工具方法:measureChildren()和getChildMeasureSpec()。

    我们都知道父容器会调用子View的measure方法来测量子View,那传入的参数int widthMeasureSpec, int heightMeasureSpec是怎么来的呢?

    3.1 理解MeasureSpec

    系统会将View的LayoutParams根据父容器所施加的规则转成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高。

    MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

    SpecMode:UNSPECIFIED,EXACTLY, AT_MOST(wrap_content)

    UNSPECIFIED 00
    EXACTLY     01
    AT_MOST     10
    
    MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
    

    通过上面这个方法将尺寸和mode拼接成MeasureSpec。

    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    

    可以看出,MODE_MASK就是32位int值,高两位为1,通过与MODE_MASK的与或来获取mode和size再拼接。

    其实View中的MEASURED_SIZE_MASK = ~MODE_MASK,MEASURED_STATE_MASK = MODE_MASK;

    public static final int MEASURED_SIZE_MASK = 0x00ffffff;
    public static final int MEASURED_STATE_MASK = 0xff000000;
    

    3.2 ViewGroup的getChildMeasureSpec

    getChildMeasureSpec就是获取要传递给子View的MeasureSpec的方法,要传给子View的MeasureSpec就是通过这个获取的。它通过
    MeasureSpec与子View的宽度来获取值。

    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;
    

    所以在LayoutParams中,设置了固定长度(有意义)的View宽高是不为负数的。

    3.2.1 子View设置了layout_width(height)
    if (childDimension >= 0) {
        //子类自己决定size
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    }
    

    无论哪种specMode,对于设置了精确长度的子View,取设置的宽高,且mode为EXACTLY。

    3.2.2 layout_width(height)="wrap_content"
    if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子类自己决定size,但不能超过父类
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
    }
    

    除了specMode为UNSPECIFIED,子View为wrap_content时,取得宽高为方法参数中的尺寸(例如LinearLayout中为其父容器测量传递的MeasureSpec),mode为AT_MOST。

    3.2.3 layout_width(height)="match_content"

    不考虑UNSPECIFIED

    switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension == LayoutParams.MATCH_PARENT) {
                // 等于父容器的宽度
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            break;
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View想和父容器尺寸一样,但父容器自己也还没确定尺寸
                resultSize = size;
                // 子View尺寸肯定不能大于父容器
                resultMode = MeasureSpec.AT_MOST;
            }
    

    父容器如果有确定的size,那子View就和父View一样;如果没有确定尺寸,那测量子View的MeasureSpec和父容器一样。

    4 参考

    源码解析Android中View的measure量算过程

    一篇文章理解Android 视图树的测量过程

    相关文章

      网友评论

        本文标题:Measure要知道的点

        本文链接:https://www.haomeiwen.com/subject/leajkqtx.html