PS: If we cannot predict what the future will be like, then we should focus on doing well in the present.
Today, I want to share a problem I encountered in a custom View. If my analysis is incorrect, I hope you can point it out. During the process of creating a custom View, there is a common issue: the custom View works fine, but when using match_parent and wrap_content, the effect is the same. The onMeasure() method is as follows:
/** * * Measure the width and height of the View. This method is called by the measure method, * and is generally overridden by subclasses to provide more accurate and efficient measurements. * * Note: When overriding this method, you must call the setMeasuredDimension(int, int) * method to store the measured width and height of the View. * If measurement fails, throw IllegalStateException. * * The default implementation of measurement is based on the background size, unless * larger measurement specifications are allowed. Subclasses should override this method * to provide better measurements. * * If this method is overridden, it is the subclass's responsibility to ensure that the * measured height and width are at least the minimum width and height of the View. * * @param widthMeasureSpec The horizontal measurement specification imposed by the parent container. * @param heightMeasureSpec The vertical measurement specification imposed by the parent container. */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Store the measured width and height setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
Let's take a look at the specific implementation of the getDefaultSize method:
// Get the corresponding size based on the specified measurement mode public static int getDefaultSize(int size, int measureSpec) { // Default size, which is related to whether a background is set int result = size; // Get the corresponding measurement mode and size from the View's measurement specification int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); // Set the current View's size based on the View's measurement mode switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } // Return the size of the View return result; }
Clearly, by default, when the width and height of the View are set to match_parent and wrap_content, which means the measurement modes are AT_MOST and EXACTLY, the final returned size is obtained from the specified MeasureSpec. Therefore, by default, setting the width and height of the View to match_parent and wrap_content results in the same effect.
The MeasureSpec for width and height in onMeasure is determined by the parent View's MeasureSpec and its own layout parameters (LayoutParams), which is specifically reflected in the ViewGroup's getChildMeasureSpec() method, as shown in the source code below:
/** * The difficulty of measuring the child View: find the MeasureSpec passed to the specified child View. * This method finds the correct MeasureSpec for measuring the width or height of the child View. * * The goal of this method is to combine the MeasureSpec and the child View's LayoutParams to obtain the most accurate result. * For example, if the measurement mode specified in the parent View's MeasureSpec is EXACTLY, and the child View specifies an exact size, * the parent View will give the child View an exact size. * * @param spec The MeasureSpec specified by the parent View for width or height. * @param padding If the current View has set margins and padding, it represents the current View's padding and margins, * such as mPaddingLeft + mPaddingRight + mLeftMargin + mRightMargin. * @param childDimension The width or height of the current View. * @return The corresponding size of the child View after measurement. */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // Get the measurement mode and size of the parent View for the current measurement dimension int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // Get the actual usable size for the child View int size = Math.max(0, specSize - padding); // The measurement mode and size of the child View still need to be measured int resultSize = 0; int resultMode = 0; // Measure the child View based on the different measurement modes specified by the parent View switch (specMode) { // EXACTLY: The parent View specifies an exact size, such as match_parent, 100dp case MeasureSpec.EXACTLY: // If the child View specifies an exact size if (childDimension >= 0) { // The specified size is the size set by the child View itself resultSize = childDimension; // The measurement mode of the child View is EXACTLY resultMode = MeasureSpec.EXACTLY; // If the child View sets the size to MATCH_PARENT } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { // The specified size of the child View is the maximum size it can use resultSize = size; // The measurement mode of the child View is EXACTLY resultMode = MeasureSpec.EXACTLY; // If the child View sets the size to WRAP_CONTENT } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { // The size of the child View is determined by its own content, but cannot exceed the size specified by the parent View resultSize = size; // The measurement mode of the child View is AT_MOST resultMode = MeasureSpec.AT_MOST; } break; // AT_MOST: The parent View allows the child View a maximum size, such as wrap_content case MeasureSpec.AT_MOST: // If the child View specifies an exact size if (childDimension >= 0) { // Specify the measurement mode and size of the child View, at this point the size of the child View's measurement mode can be determined resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { // The child View wants to obtain the maximum size specified by the parent View, but the size of the parent View cannot be determined, // so the size of the child View can only be determined by its own content. // Therefore, the measurement mode is specified as MeasureSpec.AT_MOST resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { // The child wants to determine its own size. It can't be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // UNSPECIFIED: The parent View does not constrain the child View, the child View can set whatever size it wants // If the measurement mode specified by the parent View is UNSPECIFIED, and the child View's size is not specified exactly, // the child View's size can take two ranges: 0 or the maximum size allowed for the child View. case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // The child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { // The 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) { // The child wants to determine its own size.... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } // Return the corresponding MeasureSpec for the child View return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
The above is just an analysis of the getChildMeasureSpec() source code. Returning to the original question, why is it that by default, when the width and height are set to match_parent and wrap_content, the effect is the same in a custom View? The table below summarizes the analysis based on the above code:
Parent View's MeasureSpec 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 or 0(UNSPECIFIED) The contents of the table above are based on the internal implementation of the getChildMeasureSpec() method. By default, when the width and height of the child View are set to match_parent or wrap_content, the actual size of the child View is size, which represents the size that the child View can actually use, excluding padding and margins. When we create a custom View, the size used for calculating coordinates is this size. Although the final MeasureSpec of the child View may differ, the size parsed from a specific MeasureSpec is the same. Therefore, in a custom View, by default, setting the width and height to match_parent and wrap_content results in the same effect. This concludes the reason for the problem.
So how do we solve this problem? Of course, we need to save the appropriate width and height size before obtaining the custom View's width and height. This size can be set to a default size based on specific requirements. For example, when setting the LayoutParams of the custom View to wrap_content, we should set a default size, as shown in the following handling:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Default width and height for 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); // Reset default width and height when wrap_content if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) { // Re-save appropriate width and height 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); } // If the current View's LayoutParams is wrap_content, the obtained width and height are the corresponding default width and height int mWidth = getMeasuredWidth(); int mHeight = getMeasuredHeight(); }
The above code is from the article Custom View Implementing a Date Picker. You can click to view it.