美文网首页
Android-MeasureSpec真正意义和View的大小控

Android-MeasureSpec真正意义和View的大小控

作者: zzq_nene | 来源:发表于2020-12-18 11:31 被阅读0次

    针对自定义View的尺寸限制,需要直观的考虑以下几个问题:

    • 自定义的View最好不要超过其父控件的大小
    • 自定义的View(如果是ViewGroup)的子控件最好不要超过该View的大小
    • 如果明确了自定义View的尺寸,则按该尺寸来测量

    一、父容器的限制与MeasureSpec

    比如:有一个父容器,假设其宽高是200dp*200dp,那么其子View的宽高可能有下面三种情况:

    情况一:宽高都是match_parent

    这样的情况,那么我们期望的子View的宽高就应该是200dp*200dp

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    
    情况二:宽高都是100dp

    这样的情况,我们期望的子View的宽高都是100dp

    android:layout_width="100dp"
    android:layout_height="100dp"
    
    情况三:宽高都是wrap_content

    这样的情况,我们期望子View的宽高是按照自己需求的尺寸来确定,但是最好不要超过200dp

    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    

    父控件通过其自身的MeasureSpec告诉子View父控件本身的一个述求,父控件传给子View的MeasureSpec其实就是给子View做一个参考,具体子View的宽高还是需要子View自己决定,可以不根据该参考来确定。

    MeasureSpec构成

    MeasureSpec是由size和mode组成,MeasureSpec是一个int类型的值,长度是32位,其高两位是mode的值,低30位是size的值。
    MeasureSpec的mode有三种类型:UNSPECIFIED、EXACTLY、AT_MOST,size就是父控件给子View的一个参考大小。

    • UNSPECIFIED(未指定),父控件对子控件不加任何束缚,子元素可以得到任意想要的大小,这种MeasureSpec一般是由父控件自身的特性决定的。比如ScrollView,它的子View可以随意设置大小,无论多高,都能滚动显示,这个时候,size一般就没什么意义。
    • EXACTLY(完全),父控件为子View指定确切大小,希望子View完全按照自己给定尺寸来处理,跟上面的场景1跟2比较相似,这时的MeasureSpec一般是父控件根据自身的MeasureSpec跟子View的布局参数来确定的。一般这种情况下size>0,有个确定值。
    • AT_MOST(至多),父控件为子元素指定最大参考尺寸,希望子View的尺寸不要超过这个尺寸,跟上面场景3比较相似。这种模式也是父控件根据自身的MeasureSpec跟子View的布局参数来确定的,一般是子View的布局参数采用wrap_content的时候。

    源码确定child的MeasureSpec的构造

        protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
            // 构建子View的MeasureSpec
            // 在getChildMeasureSpec内部计算size的时候会减去padding
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    所有的View都是支持padding,所以在为子View设置参考尺寸的时候,需要去除其padding值,这样得到的大小size才是子View能够放置的区域。

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

    上面代码的关系,总的来看,可以总结为下面这个表:


    image.png

    当子View接收到父控件传递来的MeasureSpec之后,就可以知道父控件对自身的要求是怎么样的,而具体的传递是在自定义View的onMeasure方法中进行,如果是自定义ViewGroup,则需要考虑该自定义View的子View测量。

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

    getSuggestedMinimumHeight方法是根据设置的背景和最小尺寸大小给出的一个建议尺寸

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

    如果自定义View的时候没有重写onMeasure方法,那么自定义View的尺寸在AT_MOST和EXACTLY两种模式下是一样的,都是由其父控件的MeasureSpec给定的参考大小来确定。

    二、自定义尺寸的确定

    在onMeasure方法中测量尺寸是最合理的时机,如果自定义View不是ViewGroup,那么只需要参照父控件传递下来的MeasureSpec结合自身尺寸测量规则,最终调用setMeasuredDimension即可。如果自定义View是一个ViewGroup的话,那么就需要结合考虑ViewGroup中的子View的布局和排版以及子View的宽高尺寸,然后结合子View在该自定义ViewGroup中的布局排版计算该自定义ViewGroup的宽高。
    以FlowLayout流式布局为例,自定义ViewGroup中,FlowLayout流式布局是一个最常用的例子:
    FlowLayout需要知道以下几点:

    • 父容器传递给FlowLayout的MeasureSpec
    • FlowLayout中所有子View的宽高
    • 结合MeasureSpec以及FlowLayout自身需求(比如FlowLayout中的子View的高度是match_parent)
      ViewGroup.java源码中也提供了比较简洁的方法,有两个比较常用的measureChildren跟resolveSize,在之前的分析中我们知道measureChildren会调用getChildMeasureSpec为子View创建MeasureSpec,并通过measureChild测量每个子View的尺寸。那么resolveSize呢,看下面源码,resolveSize(int size, int measureSpec)的两个输入参数,第一个参数:size,是View自身希望获取的尺寸,第二参数:measureSpec,其实父控件传递给View,推荐View获取的尺寸,resolveSize就是综合考量两个参数,最后给一个建议的尺寸:
        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) {
                        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);
        }
    
    • 如果父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对自己没有任何限制,那么尺寸就选择自己需要的尺寸size
    • 如果父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,希望自己能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
    • 如果父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就选择MeasureSpec.getSize(measureSpec),否则用自己想要的尺寸就行了

    其实设置自定义ViewGroup的尺寸,只需要三步:

    • 测量所有子View,获取到所有子View的尺寸
    • 根据自身特点和排版布局,依赖所有子View的尺寸计算自身的尺寸
    • 对比计算得到的尺寸和父容器传递的MeasureSpec的参考尺寸,得到一个合适的值

    三、顶层View的MeasureSpec是谁指定

    传递给子View的MeasureSpec是父容器根据自己的MeasureSpec及子View的布局参数所确定的,那么根MeasureSpec是谁创建的呢?我们用最常用的两种Window来解释一下,Activity与Dialog,DecorView是Activity的根布局,传递给DecorView的MeasureSpec是系统根据Activity或者Dialog的Theme来确定的,也就是说,最初的MeasureSpec是直接根据Window的属性构建的,一般对于Activity来说,根MeasureSpec是EXACTLY+屏幕尺寸,对于Dialog来说,如果不做特殊设定会采用AT_MOST+屏幕尺寸。这里牵扯到WindowManagerService跟ActivityManagerService,感兴趣的可以跟踪一下WindowManager.LayoutParams ,后面也会专门分析一下,比如,实现最简单试的全屏的Dialog就跟这些知识相关。

    相关文章

      网友评论

          本文标题:Android-MeasureSpec真正意义和View的大小控

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