深入理解MeasureSpec

QuibblerQuibbler 2021-04-19 约 27 分钟 1922 次阅读

深入理解MeasureSpec

在View的绘制流程一文中提及到MeasureSpec,但是并没有涉及太多,只是一带而过。MeasureSpec是View测量过程中重要的测量依据,这一篇中阅读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,通过该方法将mode和size包装在一个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)方法从包含size和mode的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初始化

在ViewRootImpl的performTraversals()方法中开启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初始化

以最常见的容器FrameLayout的onMeasure()方法为例,从根View或者其它View层层传递过来带有mode和size的MeasureSpec值:

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:通过ViewGroup的measureChildWithMargins(...)方法预先得到子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);
    }

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


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

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


相关资料:

         Docs > MeasureSpec">Android Developers > Docs > MeasureSpec

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


相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w