自定义View中为何match_parent和wrap_content效果一样

2,373 阅读6分钟

原文首发于微信公众号:躬行之(jzman-blog),欢迎关注交流!

今天来分享一个我在自定义 View 中遇到的问题,如果分析有误,还望各位指出,在自定义 View 的过程中一定会遇到一个问题,自定义 View 没有问题,唯独在自定义的 View 中 match_parent 和 wrap_content 效果一致,onMeasure() 方法如下:

/**
 *
 * 测量View的宽度和高度,这个方法由 measure方法调用,一般由子类重写该方法以提供更加精确和高效的测量
 *
 * 规定:当重写该方法的时候。你必须调用setMeasuredDimension(int, int)方法来存放View的测量宽度和高度,
 * 测量失败抛出 IllegalStateException
 *
 * 测量的基类实现默认为背景大小,除非允许更大尺寸的测量规范,子类应该重写该方法以便提供更好地测量
 *
 * 如果该方法被重写,则子类的责任就是确保测量的高度和宽度至少是View的最小宽度和高度
 *
 * @param widthMeasureSpec 父容器施加的水平方向测量规范.
 * @param heightMeasureSpec 父容器施加的垂直方向测量规范.
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //存储测量的宽高
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

看一下 getDefaultSize 方法的具体实现:

//根据指定的测量模式获得对应的大小
public static int getDefaultSize(int size, int measureSpec) {
    //默认大小,其大小与是否设置背景相关
    int result = size;
    //从View的测量规范中获得对应的测量模式和测量大小
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    //根据View的测量模式设置当前View的大小
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    //返回View的尺寸大小
    return result;
}

显然,默认情况下 View 指定的宽高为 match_parent 和 wrap_content 时,也就是测量模式为 AT_MOST 和 EXACTLY 的时候,最终返回的都是从指定 MeasureSpec 中获得的尺寸大小,所以默认情况下设置 View 的宽高为 match_parent 和 wrap_content 时效果是一样的。

onMeasure 中的宽度和高度的 MeasureSpec 是由父 View MeasureSpec 及自身的布局参数 LayoutParamas 来确定,具体体现在 ViewGroup 中的 getChildMeasureSpec() 方法中,源码如下:

/**
 * 测量子View的难点:找出MeasureSpec传递给指定的子View,该方法找出了正确的MeasureSpec用于子View宽度或高度的测量
 *
 * 该方法的目标是结合MeasureSpec和子View的LayoutParams获得最准确结果
 * 如父View的MeasureSpec中指定的测量模式为EXACTLY,子View制定了确切的尺寸大小,则父View会给子View一个确切的大小
 *
 * @param spec 父View指定的宽或高的MeasureSpec
 * @param padding 如果当前View设置了外边距和内边距,表示当前View的内边距和外边距,
 *                如 mPaddingLeft + mPaddingRight + mLeftMargin + mRightMargin
 * @param childDimension 当前View的宽或高
 * @return 子View测量后的对应尺寸大小
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //获得当前测量维度的父View测量模式和测量大小
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //获得子View真正可以可以使用的尺寸大小
    int size = Math.max(0, specSize - padding);
    //子View的测量模式和尺寸大小,还需测量完成
    int resultSize = 0;
    int resultMode = 0;
    //根据父View指定的不同的测量模式对子View进行测量
    switch (specMode) {
        //EXACTLY:父View指定确切的尺寸,如match_parent、100dp
        case MeasureSpec.EXACTLY:
            //如果子View指定了确切的尺寸大小
            if (childDimension >= 0) {
                //指定尺寸大小为子View自己设置的尺寸大小
                resultSize = childDimension;
                //指定子View测量模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;

            //如果子View设置了尺寸大小是:MATCH_PARENT
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //指定子View的尺寸大小为子View可以是使用的最大的尺寸
                resultSize = size;
                //指定子View测量模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;

            //如果子View设置了尺寸大小是:WRAP_CONTENT
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子View的尺寸大小由自己包裹的内容决定,但是不能超过父View指定的尺寸大小
                resultSize = size;
                //指定子View测量模式为EXACTLY
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //AT_MOST:父View允许子View一个能够达到的最大的尺寸大小,如wrap_content
        case MeasureSpec.AT_MOST:
            //如果子View指定了确切的尺寸大小
            if (childDimension >= 0) {
                // 指定子View的测量模式和测量大小,此时可以确定子View的测量模式的尺寸大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //子View想要获得父View指定的最大尺寸,但是父View的尺寸大小不能确定,子View的大小只能由子View自己包裹的内容决定
                //故测量模式指定为 MeasureSpec.AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;

            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // UNSPECIFIED:父View不对子View约束,子View可以设置它想要的大小
        //如果父View指定的测量模式是UNSPECIFIED,其子View自身的尺寸大小如果不确切指定,则
        // 子View尺寸大小有两种取值范围:0或子View所被允许的最大尺寸
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == ViewGroup.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 == ViewGroup.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;
    }
    //返回子View对应的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面只是对 getChildMeasureSpec() 源码的分析,回到问题本身,为什么在自定义 View 中默认情况下为何宽高设置为 match_parent 和 wrap_content 效果一样,下面表格是根据上述代码分析总结如下:

父View的MeasureSpecEXACTLYAT_MOSTUNSPECIFIED
具体尺寸childDimension(EXACTLY)childDimension(EXACTLY)childDimension(EXACTLY)
match_parentsize(EXACTLY)size(AT_MOST)size(UNSPECIFIED)
wrap_contentsize(AT_MOST)size(AT_MOST)size 或 0(UNSPECIFIED)

上面表格内容都是基于 getChildMeasureSpec() 方法的内部实现,默认状态下当子 View 的宽高设置为 match_parent 或 wrap_content 时,其子 View 的实际大小是 size,size 表示为子 View 可以真正可以使用的大小,及除去内边距和外边距之外子 View 可以使用的尺寸大小,当我们自定义 View 时计算坐标的时候使用到的就是尺寸大小 size,虽然最终子 View 的 MeasureSpec 可能不同,但是从某个具体的 MeasureSpec 中解析出来的 size 是一样,所以在自定义 View 中默认情况下宽高设置为 match_parent 和 wrap_content 效果一样,到此这个问题就算找到原因了。

那么如何解决这个问题呢,当然就要在获取自定义 View 的宽高之前重新保存合适的宽高的尺寸大小,这个尺寸大小可以由具体的需求设置一个默认的大小,即当设置自定义 View 的 LayoutParams 设置为 wrap_content 时要设置默认的尺寸,比如下面这样处理:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //wrap_content默认宽高
    Rect mRect = new Rect();
    mLetterPaint.getTextBounds("A", 0, 1, mRect);
    int mDefaultWidth = mRect.width() + dpToPx(mContext, 12);
    int mDefaultHeight = mRect.height() + dpToPx(mContext, 5);

    //重新设置wrap_content时的默认宽高
    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
            getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        //重新保存合适的宽高
        setMeasuredDimension(mDefaultWidth, mHeight);
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mDefaultWidth, heightSize);
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, mDefaultHeight);
    }
    //如果当前View的LayoutParams为wrap_content则获取的宽高就是对应的默认宽高
    int mWidth = getMeasuredWidth();
    int mHeight = getMeasuredHeight();
}

上面是自定义View实现一个日期选择器这篇文章中的相关代码,具体可以点击查看,