深入理解MeasureSpec

Quibbler 2021-4-19 808

深入理解MeasureSpec


        在View的绘制流程一文中提及到MeasureSpec,但是并没有涉及太多,只是一带而过。MeasureSpecView测量过程中重要的测量依据,这一篇中阅读MeasureSpec源码理解其作用。



1、MeasureSpec介绍

        MeasureSpec完整的名称是measure specification,直译为中文叫测量规格


1.1、测量模式掩码

        在了解View的三种测量模式之前,先弄明白MeasureSpec中两个常量的含义:

    private static final int MODE_SHIFT = 30;
    
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        MeasureSpec中定义了一个静态常量MODE_MASK,用来从 int 值中提取出测量模式:mode。十六进制0x3,也就是0011,左移30位。使用位移操作因为位移计算在计算机中是非常高效的。


        后面计算还会用到MODE_MASK的反码,用来提取出int中存储的View大小:size。



1.2、三种测量模式

        一个View的MeasureSpec由大小和模式组成,View共有三种可能的模式:

        UNSPECIFIED:未确定模式。父View没有对子view施加任何约束,子view可以是任何大小。

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;



        EXACTLY:精确模式。父View已经为子View确定了确切的尺寸, 不管子View要多大,都会给子View这个值。

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;



        AT_MOST:最大模式。子View可以根据需要的大小而定,最大可以达到MeasureSpec中指定的size大小。

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;



1.3、构造MeasureSpec

        MeasureSpec提供了组装MeasureSpec值的静态方法makeMeasureSpec(int size,int mode),传入测量模式mode和View的大小size,通过该方法将modesize包装在一个int中。

        之所以这样做,最主要的原因还是节省内存,避免内存的频繁分配消耗。因为View的测量过程是及其频繁和复杂的,只用一个int使用非常少的内存即可保存和传递View的大小信息。

    public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            //早期API ≤ 17
            return size + mode;
        } else {
            //高2位保存mode,低30位保存size
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

        对于早期应用内部会通过makeSafeMeasureSpec(int size, int mode)方法计算MeasureSpec

    /**
     * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
     * will automatically get a size of 0. Older apps expect this.
     *
     * @hide 仅供内部使用,以与系统小部件和较旧的应用程序兼容
     */
    @UnsupportedAppUsage
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

        至于多早呢,从View的源码中可以找到答案:在API 17及以前的View如果传入的mode为UNSPECIFIED,那么View的大小直接默认为0。

    // Older apps may need this compatibility hack for measurement.
    sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;


1.4、提取size和mode

        MeasureSpec提供了getMode(int measureSpec)方法从包含sizemode的int值中提取出View的测量模式mode

    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

        同样,对int低30位用反码即可获得保存的View的size值。

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }


        最后,MeasureSpec中还有一个adjust(int measureSpec, int delta)方法,用来对MeasureSpec值进行校准,防止size的大小为负。

    static int adjust(int measureSpec, int delta) {
        //先提取出mode和size
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        
        //如果mode是未指定,则不校准
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        
        //将size大小加上校准值delta,但是不能为负,最小也只能为0.
        size += delta;
        if (size < 0) {
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }



2、MeasureSpec的初始化

        View中的MeasureSpec是如何初始化的呢?回顾一下View的绘制流程一文中1.3节的内容。


2.1、根View的MeasureSpec初始化

        在ViewRootImplperformTraversals()方法中开启View的绘制流程:measure -> layout -> draw。只关注测量相关的代码:

    private void performTraversals() {
       ...
	    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
	    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
	    
	     // Ask host how big it wants to be
	    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
    }

        通过getRootMeasureSpec(int windowSize, int rootDimension)方法获取根MeasureSpec的值:

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

        ViewRootImpl中的mWindowAttributes成员变量默认即是WindowManager.LayoutParams类型,这也是根View所使用的属性。

    public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

        而WindowManager.LayoutParams的构造方法,默认Window的宽高都是MATCH_PARENT

    public LayoutParams() {
        super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        type = TYPE_APPLICATION;
        format = PixelFormat.OPAQUE;
    }

        回到getRootMeasureSpec(int windowSize, int rootDimension)方法中,走第一个case:

    ...
    measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)
    ...

        所以根View的大小就是Window窗口的大小,而且是精确值。


2.2、子View的MeasureSpec初始化

        以最常见的容器FrameLayoutonMeasure()方法为例,从根View或者其它View层层传递过来带有modesizeMeasureSpec值:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ...
        }
        ...        
    }

        在测量子View之前,先遍历一遍所有子View:通过ViewGroupmeasureChildWithMargins(...)方法预先得到子View的MeasureSpec

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        
        //计算子View宽、高的MeasureSpec 
        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);
        
        //再测量子View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

        最后通过ViewGroup中的getChildMeasureSpec(spec, padding, childDimension)方法计算出子View的MeasureSpec。完整方法如下:

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

        上面六十多行代码主要是是caseif判断,逻辑简化成下表:



        至此,把View测量中的一个重要概念MeasureSpec搞清楚了。

        记得以前开发的时候用ScrollView嵌套ListView,列表始终显示不全。当时刚入门,不知道怎么搞(〃'▽'〃)。其实是因为MeasureSpecUNSPECIFIED,无法确定列表具体高度导致。



相关资料:

        Android Developers > Docs > MeasureSpec

        安卓开发之ScrollView嵌套ListView的一些问题和解决


不忘初心的阿甘
最新回复 (0)
    • 安卓笔记本
      2
        登录 注册 QQ
返回
仅供学习交流,切勿用于商业用途。如有错误欢迎指出:fluent0418@gmail.com