preface

Tip: The article is long and consists of a lot of code and debug call diagrams. To avoid wasting your time, let’s start with a very simple piece of code (if you can clearly explain this idea, there’s no point in reading this article) :

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black" />
</LinearLayout>
Copy the code

It’s a very simple layout file, so try to preview what the layout looks like in your mind. If you think the screen is full of black, then this article makes sense to you.

Because the layout preview looks like this:

Black is only a short one, and the problem here is the onMeasure() method of TextView. OK, today’s article lets us have a good look at the process of Measure…

The body of the

From the phenomenon, it is obvious that after the process of measure(), our TextView is determined to be only this high. Therefore, let’s use this case today to study what factors will affect View in the process of measurement.

How do we string the views in our layout

Before we start, let’s not rush into the measurement part. To prepare for this, let’s review a few things about setContentView:

  • The layout file of our setContentView is parsed by the LayoutInflate, which will associate with the DecorView with the DecorView being the parent.
  • In the process visible to the Activity, Window passes in the DecorView by calling addView(), which will add a new ViewRootImpl and add the DecorView to the ViewRootImpl, also as the parent.
  • DecorView -> DecorView -> FrmeLayout ->…
  • 4, then in the addView(DecorView) of ViewRootImpl, the requestLayout() method will be executed to open the process of measure, Layout, draw.

Debug call chain, as follows:

In this case, the requestLayout() cannot be executed because parent is null. The real requestLayout() is the call chain below.

2. How to understand the measurement of View

Firstly, according to the official document, we can make it clear that the process of measure() is top-down.

Must be joking! This is Google Developer Docs… This translation is also speechless.

Some people may not know the measurement process of View, but the onMeasure() method is still a bit of a touch, right? (It doesn’t matter if you don’t know, this article is just a small demo to talk about the key points in the process of measure.) The process of measure can be expressed simply by a serial flow chart:


OK, with that knowledge in hand, we can begin the analysis of the effect at the beginning:

The requestLayout() method is called to the View’s measure(), which in turn is called to its own onMeasure(). Measure() is not a rewritable method, so since measurement is top-down, let’s start with omMeasure() in the outer LinearLayout.

3. OnMeasure () of LinearLayout

OnMeasure () is simpler, but here we need to clarify what the parameters of this method mean:

  • MeasureSpec goes without saying, records the dimensions and measurement mode of the current View
  • Just to be clear, here’s MeasureSpec for the parent View
/ * * *@param widthMeasureSpec horizontal space requirements as imposed by the parent.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else{ measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}Copy the code

The measureChildBeforeLayout() method is used to measure the child View. The measureChildBeforeLayout() method is used to measure the child View. The measureChildBeforeLayout() method is used to measure the child View. This method will eventually reach measureChildWithMargins() in the ViewGroup:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // This method basically does one thing: determine the child View's MeasureSpec from the child View's LayoutParams and the parent View's MeasureSpec
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

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

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
Copy the code

In case you’re wondering, when were the LayoutParams for the subview set?

When is the View’s LayoutParams set?

What time is the View’s LayoutParams set?

It all boils down to one sentence: Parsing the XML Settings in LayoutInflate. What exactly? Go directly to the code:

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // Omit part of the code
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    rInflateChildren(parser, view, attrs, true);
    viewGroup.addView(view, params);
}

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}
Copy the code

The above code description is clear, to put it simply, parsing XML, based on the View’s layout_width, layout_height attributes in the layout to generate the corresponding LayoutParams. The View and LayoutParams are then bound together using the addView() method.

5, child View measure()

The measureChildWithMargins() method determines the child View’s MeasureSpec jointly with the parent View’s MeasureSpec and the child View’s LayoutParams, and then calls the child View’s measure() method.

There are two key points:

  • MeasureSpec generates a child View
  • Execute the measure() method of the child View

MeasureSpec generates a child View

This part of the logic is mainly in the getChildMeasureSpec() method, so we’ll just go after it:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // Omit part of initialization code
    switch (specMode) { 
        case MeasureSpec.EXACTLY: 
            if (childDimension >= 0) { 
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST: 
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED: 
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Copy the code

This part of the code, even Google made the rules, there is nothing to say. It all boils down to this image from Exploring the Art of Android Development:

If the parent View (LinearLayout) is wrap_content, and the child View (TextView) is match_parent, what should the child View measure look like?

With the above analysis, it is easy to come to the answer: parentSize + AT_MOST. So we know that in this scenario, match_parent means that the child View’s width and height is the parent View’s width and height. So what is the width and height of the parent View?

Since the parent View is already the root View, its outer side is a DecorView, and the DecorView’s MeasureSpec is relatively simple, directly based on the Window’s width and height and its own LayoutParams.

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}
Copy the code

The DecorView’s LayoutParams are also very clear, as you should know from setContentView code:

public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
Copy the code

So in this case, the DecorView’s MeasureSpec is the width and height of the screen + EXACTLY, so the parent View (LinearLayout) is clear: parentSize + AT_MOST.

Did you find a problem?

  • The MeasureSpec of the child View (TextView) is parentSize + AT_MOST
  • The MeasureSpec of parent View (LinearLayout) is parentSize + AT_MOST
  • The DecorView’s MeasureSpec is the size + AT_MOST of the screen

The above derivation: the size of the subview should be the size of the screen! The same is true of the result from debug:

But the actual effect of the opening paragraph has negated that answer, so what’s the problem?

ParentSize + AT_MOST (parentSize + AT_MOST) Let’s not forget that we are only getting the MeasureSpec for the child View. There is one more crucial step to getting the MeasureSpec: implement the measure() method for the child View.

5.2. Perform the measure() method of the subview

Let’s take a look at the measure() method of the subview, MeasureChildWithMargins () the measureChildWithMargins() method calculates the child View MeasureSpec based on the parent View’s MeasureSpec and the child View’s LayoutParams and then calls the child View’s measure() :

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // Omit the MeasureSpec process for retrieving the child View
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
Copy the code

We can see that the measure() of the child View is called to the onMeasure() of the child View, and setMeasureDimension() is used to determine the measure width and height of the View.

Here we can probably have a guess: the reason why the final height of the child View (TextView) is not parentSize is most likely because of its own onMeasure() method!

Stepping into the onMeasure() method, we will find that the implementation of TextView’s onMeasure() method is relatively long, so the key logic is mainly extracted here:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Omit part of the code
    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightSize;
        mDesiredHeightAtMeasure = -1;
    } else {
        int desired = getDesiredHeight();

        height = desired;
        mDesiredHeightAtMeasure = desired;

        if(heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); }}// Omit part of the code
    setMeasuredDimension(width, height);
}
Copy the code

Our subview (TextView) has been determined to be AT_MOST, so let’s look at the result directly:

So the focus of our next investigation is the getDesiredHeight() method:

private int getDesiredHeight(a) {
    return Math.max(
        getDesiredHeight(mLayout, true), getDesiredHeight(mHintLayout, mEllipsize ! =null));
}
// In this case, you can use this method in the Layout
layout.getHeight()
// This method is implemented by using the height of one line * the number of lines of all text
public int getHeight(a) {
    return getLineTop(getLineCount());
}
Copy the code

As you can see, in this scenario, the height of the TextView is the height of the Max expected from the mLayout or mHintLayout, and this is also TextView specific logic.

OK, for those of you who have read the code comment above, this should be an Epiphany. The opening black bar is the height of a line of text. And that height is the height of the TextView’s default Paint.

Here is not based on the source code, interested students can follow in to have a look, the following post a few pictures to support this conclusion:

<TextView
    android:layout_width="match_parent"
    android:background="@color/black"
    android:text="11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 1111"
    android:textColor="@color/white"
    android:layout_height="match_parent"/>
Copy the code

<TextView
    android:layout_width="match_parent"
    android:background="@color/black"
    android:textSize="60sp"
    android:layout_height="match_parent"/>
Copy the code

6. Extend problem 1

Here’s a quick question to think about: Can we make the TextView take up the full screen without typing text? The answer is yes, because this article is mainly about understanding this question.

(parentSize + AT_MOST) (onMeasure()) (parentSize + AT_MOST) (onMeasure()) (onMeasure()) (onMeasure())

class TestTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : TextView(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
    }
}
Copy the code

6. Extend question 2

MeasureSpec = parentSize + AT_MOST = parentSize + AT_MOST Because TextView itself is a carbon copy of measure(), the effect of the opening appears. So if we replace the TextView with a View would that make it full screen? The answer is yes:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="# 000000" />
</LinearLayout>
Copy the code

The end of the

This article involves a lot of knowledge. At first, it was not expected that this article would involve so much energy. After all, it is difficult to compress many knowledge points into an article, and I am still a crayon.

Hope this article can bring help to you students, also welcome everyone to leave a message to discuss together or to share to their own friends ~