I remember when I first came into contact with custom View, the onMeasure process was the most difficult to understand among the three major processes of View measurement, layout and drawing. Compared with onLayout and onDraw, which only focus on the logic of the current View, onMeasure usually has to deal with the relationship between parent View and child View, which makes onMeasure more difficult to understand and practice. I didn’t understand it very well at the time, but it can be done. Later, with the improvement of experience, I gradually got to know the process of onMeasure, and found that many online statements about onMeasure were wrong or inaccurate.

Where is MeasureSpec from

Let’s look at the onMeasure method of the View:

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

We all know that we override the onMeasure method when customizing the View, and that we handle the two parameters of the method, widthMeasureSpec and heightMeasureSpec. MeasureSpec is a 32-bit integer with the first two bits of mode and the last 30 bits of size. But it is often a list of knowledge, and do not clear the relationship and logic. Therefore, in this article, I want to talk about onMeasure in a different way and try to avoid listing the knowledge points. Instead, I will write clearly the logic of onMeasure and why it is done from my own understanding.

First, where did widthMeasure and heightMeasure come from? Who passed these two parameters into the onMeasure method of the custom View?

To understand this, you need to know who called the onMeasure method of the View. The onMeasure method is called in the View measure method as follows:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {... onMeasure(widthMeasureSpec, heightMeasureSpec); . }Copy the code

As you can see, the measureSpec in the onMeasure method comes from the measureSpec in the measure method, so who calls the View’s measure method? The answer is its parent View. That is, the measure method is called from the ViewGroup type. In fact, you can also see in the note of the measure method:

This is called to find out how big a view should be. The parent
supplies constraint information in the width and height parameters.
@param widthMeasureSpec Horizontal space requirements as imposed by the
    parent
@param heightMeasureSpec Vertical space requirements as imposed by the
    parent
Copy the code

WidthMeasureSpec and heightMeasureSpec are constraints imposed on child views by the combined size of the parent and child views.

For example, in the LinearLayout onMeasure method, there is a chain of calls like this:

onMeasure -> measureVertical -> measureChildBeforeLayout -> measureChildWithMargins

In the measureVertical method, the child View is traversed and the measureChildBeforeLayout method is called

final int count = getVirtualChildCount();
for (int i = 0; i < count; ++i) {
  ...
  measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
}
Copy the code

MeasureChildWithMargins is called in the measureChildBeforeLayout method. This method calls the child View’s measure method, which is in the ViewGroup base class:

// ViewGroup.java
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
				// Calculate the measureSpec passed to the child View
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
				// Call the measure method of the child View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
Copy the code

The last sentence of this method is to call the Measure method of the LinearLayout’s child View and pass the computed measureSpec into the method. Note that the measureSpec is passed in by the parent View in the onMeasure process, but the measureSpec itself belongs to the child View and contains only the size information of the child View. But in the process of getting the size information of the child View, need to use the parent View.

How to calculate MeasureSpec

As we know earlier, the child View uses the measureSpec in the onMeasure process that is given to it by the parent View. How does the parent View compute the measureSpec?

In the measureChildWithMargins method, there are:

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
Copy the code

As the name implies, the child View’s measureSpec is calculated by the getChildMeasureSpec method. The method is signed as follows:

public static int getChildMeasureSpec(int spec, int padding, int childDimension)
Copy the code
  • specThis parameter refers to the parent ViewmeasureSpecWhich is to call the subviewmeasureMethod’s parent View. Of course, you might ask, parent ViewmeasureSpecWhere did it come from? The answer is the parent View of the parent View, so it’s a chain.
  • paddingThis parameter is the sum of the padding of the parent View and the margin of the child View, which is not important here.
  • childDimensionThis refers to the width or height of our View. That’s what we set up in XMLandroid:layout_heightorandroid:layout_widht.childDimensionIt could be a specific value, for example24dpB: Sure, yesmeasureThe process has been converted to the correspondingpx)MATCH_PARENTorWRAP_CONTENT, the corresponding values of the two are -1 and -2 respectively, which belong to the marker values.

Note: It’s easy to confuse MATCH_PARENT and WRAP_CONTENT with measurespec.mode in many articles online. Some articles will say that when the size of a View is set to WRAP_CONTENT, its mode corresponds to AT_MOST. This is not accurate. MATCH_PARENT and WRAP_CONTENT are just child dimensions. it is a different concept from the mode of measureSpec, but it is a childDimension for markup and needs the parent View to give the specific dimensions.

Before diving into the concrete implementation of getChildMeasureSpec, let’s review the MeasureSpec concept.

MeasureSpec’s structure should be familiar to developers: it is a 32-bit Int. The high 2 bits are MODE and the low 30 bits are SIZE. The return value of getChildMeasureSpec is a MeasureSpec, which is then passed as a parameter to the onMeasure method of the child View.

There are three modes:

  • EXACTLY

    Indicates that the SIZE of the current View is the exact value, which is the value of the last 30 bits of SIZE.

  • AT_MOST

    The SIZE of the current View cannot exceed the value of SIZE.

  • UNSPECIFIED

    Indicates that the size of the current View is not limited by the parent View and can be as large as desired. In this case, the value of SIZE doesn’t mean much. As a general rule, the sliding parent layout imposes UNSPECIFIED constraints on child Views, such as ScrollView and RecyclerView. When you slide, you actually let the child View scroll inside of them, which means that their child View is bigger than the parent View, so the parent View should not impose size constraints on the child View.

    Note that the SIZE is calculated using the getChildMeasureSpec method. It could be the SIZE set by the child View in the XML, it could be the SIZE of the parent View, or it could be 0.

Go back to the getChildMeasureSpec method, which has the following code:

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);
    }
Copy the code

The code is long, but what it does can be summed up in one sentence: Calculate the MeasureSpec of the child View (MATCH_PARENT, WRAP_CONTENT) and return the MeasureSpec of the child View (MATCH_PARENT, WRAP_CONTENT). So there are 3 times 3 is equal to 9 cases.

I won’t explain each case here; the comments in the code are clear. Let me mention a few other points:

  • Why subviewMeasureSpecIt needs to be determined by the parent View, okay?

    This is largely due to the existence of MATCH_PARENT and WRAP_CONTENT, as these two dimensions require the parent View to determine. MATCH_PARENT needs to know how big the parent View is to match the size of the parent View; WRAP_CONTENT means that the size of the child View is determined by itself, but this size cannot exceed the size of the parent View.

    If all View dimensions can only be set to a fixed value, then the child View’s MeasureSpec is independent of the parent View. As in the code above, when childDimension >= 0, the child View’s MeasureSpec is always made up of childDimension and measurespec.exactly.

  • AT_MOSTWRAP_CONTENTThe relationship between

    There are many articles online that say that when a View’s size is set to WRAP_CONTENT, its MeasureSpec.MODE is AT_MOST. That’s not accurate. As the MODE of the parent View is UNSPECIFIED, and the child View is set to EITHER WRAP_CONTENT or MATCH_PARENT, the MODE of the child View is UNSPECIIED, not AT_MOST. Second, when the parent View is AT_MOST, the childDimension of the child View is MATCH_PARENT and the MODE of the child View is AT_MOST. So AT_MOST and WARP_CONTENT are not one to one.

    It looks a little messy, but just keep in mind that AT_MOST means that the SIZE of this View is limited to the measurespec.size value. So what’s the exact value? This depends on how you set the size of the View in onMeasure. For a normal view control onMeasure logic, when its measurespec. MODE is AT_MOST, it means its SIZE is the SIZE of the package contents, but it can’t exceed measurespec.size. Set WRAP_CONENT and maxHeight/maxWidth for the View.

    Tips: With the AT_MOST feature, you can achieve useful functions. For example, need a WRAP_CONTENT RecyclerView, its height increases with the number of items, but there is a maximum height limit, beyond this height no longer increase. To achieve such a RecyclerView, in XML to set android:maxHeight is not used. But we can override the onMeasure method by inheriting RecyclerView

    HeightSpecMode = AT_MOST

    public class MyRecyclerView extends RecyclerView {...@Override
        protected void onMeasure(int widthSpec, int heightSpec) {
         		// Create a heightSpec with mode AT_MOST and size as the maximum height you want, and pass it to super
            int newHeightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
            super.onMeasure(widthSpec, newHeightSpec); }}Copy the code
  • UNSPECIFIEDWhen will it be used?

    Many online articles that explain the drawing process are UNSPECIFIED, but they are UNSPECIFIED, and do not make it very clear. UNSPECIFIED, as the name suggests, it does not specify the size. As the MeasureSpec.MODE of a View is UNSPECIFIED, the parent View has no constraints on its size. Android uses UNSPECIFIED controls, only ScrollView, RecyclerView, and other sliding views, because their child View (that is, the sliding content) can be infinitely higher than the parent View (viewport, RecyclerView). ViewPort) is much higher.

    As the ScrollView imposes constraints on the child View, it constructs a MeasureSpec that measures the child View directly:

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                    heightUsed;
        // The MeasureSpec is explicitly UNSPECIFIED and is used to measure the child View
            final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                    Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                    MeasureSpec.UNSPECIFIED);
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    Copy the code

3. OnMeasure process

Now that we’ve seen how the onMeasure method’s input, MeasureSpec, is derived, let’s shift the focus to the onMeasure process.

MeasureSpec (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec) (MeasureSpec)

MeasureSpec acts more as a constraint on the measurement of child views by the parent View than the actual size of the View. This constraint must be known when a subview is going to make a measurement. The size of the child View depends on the onMeasure logic. You can ignore this constraint entirely and set the size of the View in the onMeasure setMeasureDimension method, but you will not do so unless you are UNSPEECIFIED as a parent View.

Let’s take a look at the simplest implementation of the onMeasure method in view.java:

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

As you can see, in the onMeasure of the View, the size is determined by getDefaultSize, and then setMeasureDimension is used to set the width and height of the View.

The getDefaultSize method is as follows:

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;
    }
Copy the code

The logic is easy to understand. When specMode is UNSPECIFIED, using getSuggestedMinimumWidth/getSuggestedMinimumHeight return size; When specMode is AT_MOST or EXACTLY, use specSize, which is measurespec.size.

If the parent View’s specMode is EXACTLY or AT_MOST, then the child View’s specMode is also AT_MOST. When the View’s specMode is AT_MOST, it returns its specSize, just as EXACTLY. This means that the View class sets WRAP_CONTENT in the XML to the same effect as MATCH_PARENT. This makes sense, of course, because the View class is simply a rectangle and has no “content.”

This is why WARP_CONTENT has no effect if we do not copy onMeasure when we are customizing the View. So if you want a custom View to have a WRAP_CONTENT effect, you need to override onMeasure and handle the case where specMode is AT_MOST (when the parent View is a slideable View, WRAP_CONTENT may also correspond to UNSPECIFIED specMode, so it is best to handle this case as well.)

For example, take the TextView onMeasure method. When we set WRAP_CONTENT to a TextView, we naturally want the width of the TextView to wrap around the text inside it. Take a look at its onMeasure implementation:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    intwidthSize = MeasureSpec.getSize(widthMeasureSpec); .intwidth; .if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
        / / if widthMode = = MeasureSpec. AT_MOST | | MeasureSpec. UNSPECIFIED
            if(mLayout ! =null && mEllipsize == null) { des = desired(mLayout); }... width = des; . . setMeasuredDimension(width, height); }Copy the code

As you can see, when widthMode == measurespec.at_most, the desired(Layout Layout) method calculates the width. The desired method is implemented as follows:

private static int desired(Layout layout) {
        int n = layout.getLineCount();
        CharSequence text = layout.getText();
        float max = 0;

        // if any line was wrapped, we can't use it.
        // but it's ok for the last line not to have a newline

        for (int i = 0; i < n - 1; i++) {
            if (text.charAt(layout.getLineEnd(i) - 1) != '\n') {
                return -1; }}for (int i = 0; i < n; i++) {
            max = Math.max(max, layout.getLineWidth(i));
        }

        return (int) Math.ceil(max);
    }
Copy the code

The logic is simple, which is to take the maximum line width of all lines of text.

So the onMeasure comes down to calculating the size and assigning it to setMeasuredDimension based on the parent View’s constraints (widthMeasureSpec and heightMeasureSpec), combined with its own characteristics.

The onMeasuer logic of a ViewGroup is similar to that of a View, except that a ViewGroup usually needs to calculate the size of its subviews before it can determine its own size. For example, the LinearLayout WRAP_CONTENT needs to figure out the height of the child views, add them up to determine how tall it is.

The MeasureSpec is on the top floor

As we know earlier, the measureSpec parameter passed by the child View in the onMeasure method is a constraint computed by the parent View’s measureSpec and the child View’s childDimension. The measureSpec of the parent View is calculated by the parent View of the parent View. So it forms a chain. Where is the head of the chain, and where does the top root View’s input measureSpec come from when it’s onMeasure?

For an Activity, the top-level View is a DecorView, which is referenced as follows:

Activity -> WindowManager -> ViewRootImpl -> DecorView

When an Activity adds a DecorView to a window via WindowManager, it calls the setView method of ViewRootImpl, holding the DecorView as follows:

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {... root =newViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); . }Copy the code

The measurement process is initiated by ViewRootImpl. That is, the top-most DecorView uses measureSpec in the measure process, which is passed in by ViewRootImpl. This process occurs in the ViewRootImple’s performTraversals:

// ViewRootImpl.java
private void performTraversals(a) {
    if(mWidth ! = frame.width() || mHeight ! = frame.height()) { mWidth = frame.width(); mHeight = frame.height(); }...int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to beperformMeasure(childWidthMeasureSpec, childHeightMeasureSpec); . }private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
    	mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
Copy the code

As you can see, the top-most measureSpec is constructed using the getRootMeasureSpec method, which is then passed to the mView (which is the DecorView) to measure in the performMeasure.

In the getRootMeasureSpec method, mWidth/mHeight is typically the width/height of the screen, while rootDimension is the layout_width and layout_height of the DecorView. These two values are determined when Windows Manager calls addView, which is MATCH_PARENT. So eventually got the code MeasureSpec. MakeMeasureSpec (windowSize, MeasureSpec. EXACTLY); The MeasureSpec constructed, that is, the DecorView size constraint is the exact width and height value, and is the screen width and height. This makes sense, because the top View should be the size of the screen.

Five, the summary

  • The View inonMeasureMethodMeasureSpecFrom the parent View, and from the parent ViewMeasureSpecThe parent View from the parent View, it’s a chain. The reason why the child View size is bound by the parent View size is becauseMATCH_PARENTWRAP_CONTENTThe existence of. If the size of the child View can only be set to a fixed DP value, then the parent View constraint on the child View doesn’t make much sense.
  • The son of ViewMeasureSpecIs made up of subviewsdimensionAnd the parent ViewMeasureSpecIt was calculated together. The son of ViewdimensionThat’s what the child View is set in the XMLandroid:layout_height/android:layout_width. Pay attention to,WRAP_CONTENTMATCH_PARENTIs alsodimensionIt’s the size, noMeasureSpec.MODEThey are two concepts, don’t get confused, although there is a relationship between the two.
  • WRAP_CONTENTAT_MOSTIt’s not a one-to-one relationship.UNSPECIFIEDGenerally only for sliding views.
  • althoughMeasureSpecSize constraint information is already contained in the subview, but the subview still needs to be inonMeasureTo determine exactly how big the child View should be. Take the TextView example mentioned above. In general, custom views are handled by yourselfspecModeAT_MOSTBecause the View class itself does not handle this case, it will causeWRAP_CONTENTFailure.
  • At the very top of the chainDecorView MeasureSpecisViewRootImplperformMeasureMethodMeasureSpec. thisMeasureSpecSIZEIs the screen width and height,MODEEXACTLY.