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

2020-12-18  本文已影响0人  zzq_nene

针对自定义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的一个参考大小。

源码确定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需要知道以下几点:

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

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

三、顶层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就跟这些知识相关。

上一篇下一篇

猜你喜欢

热点阅读