Why match_parent and wrap_content have the same effect in custom View

The original text was first published on the WeChat public account: jzman-blog, welcome to follow and exchange!

Today I will share a problem that I encountered in custom View. If the analysis is wrong, I hope you will point out that you will definitely encounter a problem in the process of customizing View. There is no problem with custom View, only in customization In the View, match_parent and wrap_content have the same effect, and the onMeasure() method is as follows:

/**
 *
 * 测量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));
}

Take a look at the specific implementation of the getDefaultSize method:

//根据指定的测量模式获得对应的大小
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;
}

Obviously, when the width and height specified by the View are match_parent and wrap_content by default, that is, when the measurement mode is AT_MOST and EXACTLY, the final return is the size obtained from the specified MeasureSpec, so the width and height of the View are set by default The effect is the same for match_parent and wrap_content.

The width and height MeasureSpec in onMeasure is determined by the parent View MeasureSpec and its own layout parameter LayoutParamas, which is embodied in the getChildMeasureSpec() method in ViewGroup. The source code is as follows:

/**
 * 测量子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);
}

The above is just an analysis of the source code of getChildMeasureSpec(), back to the question itself, why is the width and height set to match_parent and wrap_content by default in the custom View have the same effect , the following table is based on the above code analysis and summarized as follows:

MeasureSpec of the parent View EXACTLY AT_MOST UNSPECIFIED
specific size childDimension(EXACTLY) childDimension(EXACTLY) childDimension(EXACTLY)
match_parent size(EXACTLY) size(AT_MOST) size(UNSPECIFIED)
wrap_content size(AT_MOST) size(AT_MOST) size 或 0(UNSPECIFIED)

The content of the above table is based on the internal implementation of the getChildMeasureSpec() method. By default, when the width and height of the child View is set to match_parent or wrap_content, the actual size of the child View is size, and size represents the size that the child View can actually use , And the size that can be used by the sub-View except for the inner and outer margins. When we calculate the coordinates when we customize the View, the size is used. Although the MeasureSpec of the final sub-View may be different, but from a certain The parsed size in the specific MeasureSpec is the same, so by default the width and height are set to match_parent and wrap_content in the custom View, and the effect is the same. At this point, even if the cause is found.

So how to solve this problem, of course, it is necessary to re-save the appropriate width and height size before obtaining the width and height of the custom View. This size can be set to a default size according to specific needs, that is, when setting the custom View When LayoutParams is set to wrap_content, the default size should be set, such as the following processing:

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

The above is the relevant code in this article to customize View to implement a date picker. You can click to view it for details.

Guess you like

Origin blog.csdn.net/jzman/article/details/108701521