View的MeasureSpec确定过程(onMeasure分析

作者: 201e7f3210f4 | 来源:发表于2016-05-07 15:16 被阅读730次

    本文首发在我的个人博客ghui.me欢迎指教

    我们在自定义View时通常会去重写View的onMeasure方法,此方法提供的有一个默认实现:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    

    这个方法有两个int型的参数:widthMeasureSpec,heightMeasureSpec。这篇文章将会分析它们的确定过程。首先看一下MeasureSpec是什么?

    MeasureSpec

    MeasureSpec是View类的一个静态内部类,源码不太多,这里贴出分析之:

    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);
                }
            }
    
            public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
            }
    
            public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
    
            static int adjust(int measureSpec, int delta) {
                final int mode = getMode(measureSpec);
                if (mode == UNSPECIFIED) {
                    // No need to adjust size for UNSPECIFIED mode.
                    return makeMeasureSpec(0, UNSPECIFIED);
                }
                int size = getSize(measureSpec) + delta;
                if (size < 0) {
                    Log.e(VIEW_LOG_TAG,
                     "MeasureSpec.adjust: new size would be negative! (" + size +") spec: " 
                     + toString(measureSpec) + " delta: " + delta);
                    size = 0;
                }
                return makeMeasureSpec(size, mode);
            }
    
            public static String toString(int measureSpec) {
                int mode = getMode(measureSpec);
                int size = getSize(measureSpec);
    
                StringBuilder sb = new StringBuilder("MeasureSpec: ");
    
                if (mode == UNSPECIFIED)
                    sb.append("UNSPECIFIED ");
                else if (mode == EXACTLY)
                    sb.append("EXACTLY ");
                else if (mode == AT_MOST)
                    sb.append("AT_MOST ");
                else
                    sb.append(mode).append(" ");
    
                sb.append(size);
                return sb.toString();
            }
        }
    

    widthMeasureSpec 与 heightMeasureSpec是一个int型的变量,java中int型变量由4个字节(32bit)组成,其中高2位用来封装MeasureMode,MeasureMode一共有3种:

    1. UNSPECIFIED = 0 << MODE_SHIFT; 即: 00000000 00000000 00000000 00000000 父容器不对子View有任何限制
    2. EXACTLY = 1 << MODE_SHIFT; 即: 01000000 00000000 00000000 00000000 父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize
    3. AT_MOST = 2 << MODE_SHIFT; 即: 10000000 00000000 00000000 00000000 父窗口限定了一个最大值给子View即specsize

    低30位用来封装size.

    1. public static int makeMeasureSpec(int size, int mode) 此方法用来根据size与mode组合出特定的measureSpec.可以看到在api17之后是通过二进制 & | 计算的,这样效率更高。
    2. static int adjust(int measureSpec, int delta) 此方法是用来对measurespec中的size进行调整的 和 <inset>相关。
    3. public static String toString(int measureSpec) 此方法大概是用来方便开发者从一个int值中打印mode与size,方便调试。

    过程分析

    MeasureSpec大概就是这样,为了说明MeasureSpec的创建过程,我们写一个CustomLinearLayout里面套一个CustomView,通过debug来分析一下,布局代码大致如下:

    <?xml version="1.0" encoding="utf-8"?>
    <me.ghui.customviewgroup.CustomLinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
        <me.ghui.customviewgroup.CustomView android:layout_width="match_parent"
            android:layout_height="200px"
            android:background="@android:color/holo_blue_light"/>
    </me.ghui.customviewgroup.CustomLinearLayout>
    

    然后我们给CustomLinearLayout,CustomView的onMeasure方法加入一些log,如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e("ghui", "CustomLinearLayout." + "widthMeasureSpec:" 
        + MeasureSpec.toString(widthMeasureSpec)
        + ", heightMeasureSpec: " + MeasureSpec.toString(heightMeasureSpec));
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e("ghui", "CustomView." + "widthMeasureSpec:" 
        + MeasureSpec.toString(widthMeasureSpec)
        + ", heightMeasureSpec: " + MeasureSpec.toString(heightMeasureSpec));
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    

    我们在CustomView的onMeasure方法中加入一个断点,通过android studio观察onMeasure方法的调用栈信息,截图如下:


    从上面可以发现CustomView的onMeasure方法调用过程源起其Parent CustomLinearLayout中的onMeasure.在这条调用链上最后一个ViewGroup的方法是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);
    }
    

    可以看到此方法的最后一行中会调用child.measure方法将计算好的childWidthMeasureSpec 与 childHeightMeasureSpec传给子View,再往上看会发现这两个参数的计算过程被完全封装到了
    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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    从中可以看出子View的MeasureSpec的计算过程被分为了3部分:

    1. parentMeasureMode = EXACTLY

      a. childDimension >=0 即子View设置的有具体的大小(如:100px),这种情况下childSpecSize即为其自身LayoutParams里指定的大小,childSpecMode为EXACTLY

      b. childDimension = MATCH_PARENT 即子View的LayoutParams里设置的为MATCH_PARENT,这种情况下childSpecSize即为parentLeftSize(父容器剩余的空间),childSpecMode为EXACTLY

      c. childDimension = WRAP_CONTENT 即子View的LayoutParams里设置的为WRAP_CONTENT,这种情况下childSpecSize为parentLeftSize(父容器剩余的空间),childSpecMode为AT_MOST

    2. parentMeasureMode = AT_MOST

      a. childDimension >=0 即子View设置的有具体的大小(如:100px),这种情况下childSpecSize即为其自身LayoutParams里指定的大小,childSpecMode为EXACTLY (同 1.a)

      b. childDimension = MATCH_PARENT 即子View的LayoutParams里设置的为MATCH_PARENT,这种情况下childSpecSize即为parentLeftSize(父容器剩余的空间),childSpecMode为AT_MOST (同 1.c)

      c. childDimension = WRAP_CONTENT 即子View的LayoutParams里设置的为WRAP_CONTENT,这种情况下childSpecSize即为parentLeftSize(父容器剩余的空间),childSpecMode为AT_MOST(同1.c)

    3. parentMeasureMode = UNSPECIFIED

      a. childDimension >=0 即子View设置的有具体的大小(如:100px),这种情况下childSpecSize即为其自身LayoutParams里指定的大小,childSpecMode为EXACTLY (同 1.a)

      b. childDimension = MATCH_PARENT 即子View的LayoutParams里设置的为MATCH_PARENT,这种情况下childSpecSize为0,childSpecMode为UNSPECIFIED

      c. childDimension = WRAP_CONTENT 即子View的LayoutParams里设置的为WRAP_CONTENT,这种情况下childSpecSize为0,childSpecMode为UNSPECIFIED(同3.b)

    整理一下可以得到如下表格:


    总结

    1. View的measureSpec由其父容器的measureSpec及自身的LayoutParams共同决定
    2. 若子View为具体的大小(如100px),则不管其父容器的specMode为哪种,子View对应的specMode均为EXACTLY,specSize均为childSize
    3. 若子View的LayoutParams中的宽或高为wrap_content,则不管其父容器的specMode为哪种,子View对应的specMode均为AT_MOST,specSize均为parentLeftSize
    4. 子View的前期measure过程实际上在其父容器的onMeasure中就基本完成了,父容器会把计算好的measureSpec传递给子View,子View在自己的onMeasure中可以得到这些值,子View可以根据这些值设置自己的大小,当然也可以不参考它们。最终在onMeasure方法通过调用
      setMeasuredDimension方法设置view的最终measureSize。但View的真实大小是在Layout阶段才确定下来的,通过child.layout(left,top,right,bottom).View的measure size与 layout size不必相等,但绝大多数情况下是相等的。

    个人微信公众号已开通:CoderGhui ,欢迎关注!

    相关文章

      网友评论

        本文标题:View的MeasureSpec确定过程(onMeasure分析

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