深入理解MeasureSpec

作者: Android天之骄子 | 来源:发表于2017-08-29 20:04 被阅读0次

    前言

    上一篇DecorView添加到Window过程的源码分析我们找到了UI绘制流程的起始点,也就是在ViewRootImplperformTraversals()依次执行performMeasureperformLayoutperformDraw,那么这个MeasureSpec又是什么呢?它是View的一个内部类,从名字我们可以看出,这是一个测量规格,它决定了View的测量过程。

    MeasureSpec

        /**
         * A MeasureSpec encapsulates the layout requirements passed from parent to child.
         * Each MeasureSpec represents a requirement for either the width or the height.
         * A MeasureSpec is comprised of a size and a mode. There are three possible
        */
          * MeasureSpec封装从父对象传递给孩子的布局要求。
          * 每个MeasureSpec表示宽度或高度的要求。
          * MeasureSpec由尺寸和模式组成。 有三种模式:
        public static class MeasureSpec {
            private static final int MODE_SHIFT = 30;
            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
            public static final int EXACTLY     = 1 << MODE_SHIFT;
            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)
                }
            }
    }
    

    从MeasureSpec类的定义我们知道,它封装了对子View的布局要求,由尺寸和模式组成,其实MeasureSpec代表一个32位的int值,高2位表示SpecMode,低30位表示SpecSize,而SpecSize是指在某种SpecMode下的规格大小,从源码我们看出它内部定义了很多常量,从api17以后开始采用位运算,因为位运算的效率最高,我们看下三种模式

    • UNSPECIFIED = 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000父容器不对子View有任何限制,子View要多大给多大,有系统内部调用,我们不需要研究,例如ScrollView
    • EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize,对应于LayoutParams中的match_parent和设置的固定值
    • AT_MOST =2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content
      size & ~MODE_MASK是获得SpecSize,mode & MODE_MASK是获得SpecMode,之后再或运算即可得到MeasureSpec。看下图具体运算
    位运算与.png
    位运算与非.png

    我们在使用View时是直接设置LayoutParams,但是在View测量的时候,系统会将LayoutParams在父容器的约束下进行相对应的MeasureSpec,然后在根据这个MeasureSpec来确定View的测量后的宽高,由此可见,MeasureSpec不是LayoutParams唯一决定的,还需要父容器一起来决定,在进一步决定View的宽高。但是顶级View,也就是上文我们分析到的DecorView和普通的View的MeasureSpec计算有些区别,对于DecorView,其MeasureSpec是由屏幕的尺寸和LayoutParams决定的,而DecorView的默认LayoutParams就是match_parent(在初始化DecorView时可知),对于普通View来说,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams决定。在performTraversals()方法中有如下一段

        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, 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来说就是走第一个case,对于普通View来说,也就是我们Activity显示布局的根View是一个ViewGroup,我们再来看下ViewGroup的measureChildWithMargins()方法

    ViewGroup.java#measureChildWithMargins()

        protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            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);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    可以发现在调用子View的measure()之前会先通过getChildMeasureSpec()方法来得到子View的MeasureSpec,通过分析,我们明显可以发现子View的MeasureSpec的创建与父容器的MeasureSpec和自身的LayoutParams有关,我们再来看下getChildMeasureSpec()

    ViewGroup.java#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,在通过子View自身设置的LayoutParams来进一步决定子View的SpecMode,最后在通过MeasureSpec.makeMeasureSpec(resultSize, resultMode);返回子View的MeasureSpec然后再去measure().getChildMeasureSpec()这个方法很清晰的展示出子View创建MeasureSpec的过程,下面我们通过表格在说明一下

    普通View的MeasureSpec创建规则
    • View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
    • View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间
    • View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
      只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小。

    总结

    好了,至此为止,MeasureSpec含义模式以及创建规则就基本说完了,下一篇开始我们分析onMeasure()等调用过程和相关方法。

    推荐

    DecorView添加到Window过程的源码分析
    AppCompatActivity的setContentView()源码分析
    Activity的setContentView()源码分析


    相关文章

      网友评论

        本文标题:深入理解MeasureSpec

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