There is such a place in the Book of Heaven and Dragon Slaughter: Zhang Sanfeng demonstrated the taiji sword created by himself to Zhang Wuji, and then asked him to remember the moves. Zhang Wuji said he remembered half of it. Zhang Sanfeng made it slowly again and asked him how much he remembered. Zhang Wuji said he only remembered a few moves. Zhang Sanfeng finally demonstrated again, Zhang Wuji wanted to say, this time all forgotten. Zhang Sanfeng was very satisfied, so he let Zhang Wuji and the eight-armed Sword to compete.

First of all, this is a long, long, long article. The purpose is to clarify the View measurement mechanism in Android once and for all. It’s kind of like being true to yourself. It took me several days to write, and I wanted to give up several times. The reason why I wanted to give up was not that I did not make clear myself, but that I felt that my narrative vein was out of order, and that I could not make readers understand clearly. I was afraid that I would lead readers into the gutter, and I was afraid that I would make people feel wordy and idle. But in the end, I decided to stick to it, because in the process of repeatedly struggling – > unwilling – > exploring – > arguing – > questioning, I completed the sublimation of myself, to understand some long-standing confusion. Therefore, the biggest purpose of this article is to give myself as some learning record, if I have the honor to help you solve some puzzles, then I feel relieved. If there is something wrong, you are also welcome to point out criticism.

1. Is the parent of a View necessarily a ViewGroup?

  1. OnMeasure () is often poorly understood when Android customizes views. Sometimes I get it, sometimes I don’t.

  2. OnMeasure () is often poorly understood when Android customizes views. Sometimes I get it, sometimes I don’t.

  3. When you set a View’s layout_width to wrap_content or match_parent in XML instead of 50dp, why does the View have a normal size?

  4. Currently, you know the three measurement modes of Android: MeasureSpec.EXACTLY, Measure.AT_MOST, and Measure.UNSPECIFIED. But you can’t quite grasp them.

  5. Not only do you have no problem with custom views, but you also have no problem with custom ViewGroups. You understand the three measurement modes given by Android, but you still haven’t had time to think about the three measurement modes themselves.

  6. You may not have thought about what the outermost View of your Activity is.

  7. You may know that the outermost View of your Activity is called a DecorView. See how it relates to PhoneWindow and activity.setContentView (). But you don’t know who measured the DecorView.

All right, here we go. Please take a deep breath, take it easy!

Can’t ignore the custom View topic

Android application layer development can’t get around the topic of custom View, which is officially called Widget in Android, so the View in this article is actually the same meaning as Widget. There are a variety of open source libraries available on Github right now, but as a thoughtful developer, it’s not advisable to reinvent the wheel, but the wheel is made. When it comes to new UI effects, if the existing Widget doesn’t get the job done, you should think about customizing a View. We more or less know that in Android, the View drawing process has three steps: measurement, layout and drawing, which correspond to three apis: onMeasure(), onLayout() and onDraw(). – onMeasure() – Layout onLayout() – Draw onDraw()

There’s no way to say this three stages, the stage is the most important, is relative, the difficulty of measuring phase for most developers than other two, processing details are also much more, a custom View, correct measurement is the first step, as it is today the topic of this article is to discuss the measurement mechanism in the View and details.

Measuring a View is measuring a rectangle

Thanks to people’s imagination, there are various Views on the Android platform. There are Button, TextView, ListView and other components of the system, but also more developers to customize the View.

These are the widgets that come with The Android operating system. They are used to interact with different functions and display their effects, but there is another side to the interface for developers.

Take another look at all the widgets

Everything in the world has certain rules, or barriers that cannot be broken through. For a View, it’s essentially a rectangle, a square area, spread out a canvas, and then take all the resources and run wild under the existing rules.

So, the first step in customizing a View is to say in your head – we are now going to define a rectangle!

Since it is a rectangle, it must have definite width, height and position coordinates. Width and height are obtained in the measurement stage, and then in the layout stage, the rectangle is laid out according to the actual needs to determine the location information, and then the visual effect is handed over to the drawing process. It is the painter, which we are very relieved.

For example, when the government does urban planning, real estate developers tell the government the land area they want, and the government’s comprehensive policies and actual situation of land area divide the land area for real estate developers, which is just a circle on the map. Property developers are given clear geographical information and then build their own towers or mansions in the designated area. And the custom View is to get this similar to the government planning area parameters, but in the real world, the government planning to real estate developers of the land is not necessarily rectangular, but in Android View to get the area must be rectangular.

Ok, so we know that what we’re measuring is length and width, and our goal is length and width.

View Basic method for setting dimensions

In the following process, I will use a series of detailed experiments to illustrate the problem. If you feel tedious, you can skip this section directly. Let’s take a look at defining the size of a Widget when using it in Android. Let’s say we want to use a Button on the screen.

<Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>Copy the code

A button appears on the screen.



Let’s fix the width and height.

<Button
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:text="test"/>Copy the code



In another case, the width of the button is determined by the parent container

<Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="test"/>
Copy the code

The above steps are used in daily development to set the size of a View using the layout_width and layout_height properties. In XML, there are three possible values for these two attributes.

  1. Match_parent means that the value on this dimension is the same as the parent window
  2. Wrap_content indicates that the value on this dimension is determined by the content of the View itself
  3. A number like 5dp means that the View gives the exact value on this dimension.

Experiment 1

Let’s take it a step further and now find a parent container for Button to observe. The parent container background is identified by a specific color.

<RelativeLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>
Copy the code



You can see that the RelativeLayout wraps around the Button. Let’s do another situation.

Experiment 2

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>Copy the code

Experiment 3

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>Copy the code

Experiment 4

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="1000dp"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>Copy the code

Something unpleasant happened. Button wanted 1000 dp, and RelativeLayout still gave it its own limited range. For example, shanshui Manor asked the Guangming Development Zone government for 10,000 mu of land, but the government said there was not so much land, up to 2,000 mu.

Button is a View, and RelativeLayout is a ViewGroup. So for a View, it is equivalent to landscape manor, and ViewGroup is similar to the role of government. View people, their colorful constitute a beautiful Android world, ViewGroup has its own planning, the so-called planning is to put the overall situation first, as far as possible to coordinate the location of each member within the jurisdiction.

Landscape manor needs to negotiate and communicate with the government to take land to build buildings, and to customize a View also needs to negotiate with the ViewGroup in which it is located.

So what is their protocol?

MeasureSpec protocol between View and ViewGroup MeasureSpec

We have a custom View, and onMeasure () is a key method. It is also the focus of this paper.

public class TestView extends View {
    public TestView(Context context) {
        super(context);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); }}Copy the code

OnMeasure () has two parameters widthMeasureSpec and heightMeasureSpec. What are they? It seems to have something to do with width and height.

They do relate to width and height, and to understand them you need to start with a class. MeasureSpec.

MeasureSpec

MeasureSpec is a static class in View.java

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;


    public static final int UNSPECIFIED = 0 << MODE_SHIFT;


    public static final int EXACTLY     = 1 << MODE_SHIFT;


    public static final int AT_MOST     = 2 << MODE_SHIFT;


    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return(size & ~MODE_MASK) | (mode & MODE_MASK); }}...@MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }


    public static int getSize(int measureSpec) {
        return(measureSpec & ~MODE_MASK); }... }Copy the code

MeasureSpec doesn’t have much code; it has the three most important static constants and the three most important static methods.

MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST

MeasureSpec.makeMeasureSpec()
MeasureSpec.getMode()
MeasureSpec.getSize(a)Copy the code

MeasureSpec represents the measurement rule, which is implemented with an int value. We know that an int has 32 bits. MeasureSpec uses the upper two bits to represent the measurement Mode and the lower 30 bits to represent the Size.

Combine Mode and Size into a measureSpec value using the makeMeasureSpec() method. A measureSpec value can be reversely resolved to its Mode and Size by getMode() and getSize().

The three measurement modes of MeasureSpec are described below.

MeasureSpec.UNSPECIFIED

This mode is unconstrained, the child tells the parent container that it can be as wide and as tall as it wants, you don’t limit me. Developers rarely need to deal with this, either in a ScrollView or an AdapterView. So we can ignore it. The examples in this article will mostly skip that.

MeasureSpec.EXACTLY

This pattern indicates that a child element can be given an exact value.


<Button
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:text="test"/>
Copy the code

When layout_width or layout_height is match_parent or an explicit value such as 100dp, the measurement mode on this dimension is MeasureSpec.EXACTLY. Why does match_parent have an exact value? It’s reasonable to assume that a child View wants to be the same width or height as the parent ViewGroup, and a ViewGroup can obviously decide its own width and height, so when its child View asks for match_parent, It can set its width and height down.

MeasureSpec.AT_MOST

In this mode, the child View wants to be as wide or high as it wants to be. The ViewGroup of course has to respect its requirements, but it also has to do with the premise that you can’t exceed the maximum I can provide, that is, it can’t expect to exceed the recommended width of its parent class. When a View’s layout_width or layout_height is wrap_content, its measurement mode is MeasureSpec.AT_MOST.

With the above measurement patterns in mind, it’s time to write an example to test some ideas.

The custom View

My goal is to define a text box with black text in the middle and a red background.

We can easily code it. First, we define the properties it needs, and then we write the Java code for it. attrs.xml


       
<resources>
    <declare-styleable name="TestView">
        <attr name="android:text" />
        <attr name="android:textSize" />
    </declare-styleable>
</resources>Copy the code

TestView.java

public class TestView extends View {

    private  int mTextSize;
    TextPaint mPaint;
    private String mText;

    public TestView(Context context) {
        this(context,null);
    }

    public TestView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
        mText = ta.getString(R.styleable.TestView_android_text);
        mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);

        ta.recycle();

        mPaint = new TextPaint();
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(mTextSize);
        mPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

        canvas.drawColor(Color.RED);
        if (TextUtils.isEmpty(mText)) {
            return; } canvas.drawText(mText,cx,cy,mPaint); }}Copy the code

Layout file


       
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.frank.measuredemo.MainActivity">

    <com.frank.measuredemo.TestView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="test"/>

</RelativeLayout>
Copy the code

The effect

We can see that in the TestView code for the custom View, we are not doing any measurement work because we are not copying its onMeasure() method at all. However, it does its job, and given explicit values for the layout_width and layout_height properties, it should display normally. Let’s change the numbers again.

<com.frank.measuredemo.TestView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:text="test"/>
Copy the code

Change the value of layout_width to match_parent, so its width is determined by the parent class, but again it works fine.

We already know that the above two cases correspond to MeasureSpec.EXACTLY, in which TestView itself does not need to be handled.

So one might ask, what happens if layout_width or layout_height is wrap_content?

We continue to test and observe.

<com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:text="test"/>
Copy the code



The effect is the same as before, the width is the same as its ViewGroup. Let’s see.

<com.frank.measuredemo.TestView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="test"/>
Copy the code

The width is normal, but the height is the same as ViewGroup. Let’s do another case

<com.frank.measuredemo.TestView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="test"/>
Copy the code

This time you can see that the width and height are the same as the ViewGroup.

But this is not what I want!

The wrAP_content model is MeasureSpec.AT_MOST, so the first requirement is that the size is determined by the View itself and cannot exceed the value recommended by the ViewGroup.

If TestView sets wrap_content on its width and height, it means that its size is determined by its content, which in this case is the string in its middle. Obviously the above does not meet the requirements, so obviously we need to handle the measurement ourselves. 1. For MeasureSpec.EXACTLY, we leave the ViewGroup’s suggested value as the final width and height. 2. For MeasureSpec.AT_MOST, we calculate the width and height according to our content, but not more than the ViewGroup suggested value.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW represents the width of the final setting, whereas resultH represents the height of the final setting */
        int resultW = widthSize;
        int resultH = heightSize;

        int contentW = 0;
        int contentH = 0;

        /** AT_MOST mode, TestView determines the value, but cannot exceed the value recommended by ViewGroup ** /
        if ( widthMode == MeasureSpec.AT_MOST ) {

            if(! TextUtils.isEmpty(mText)){ contentW = (int) mPaint.measureText(mText); contentW += getPaddingLeft() + getPaddingRight(); resultW = contentW < widthSize ? contentW : widthSize; }}if ( heightMode == MeasureSpec.AT_MOST ) {
            if (!TextUtils.isEmpty(mText)){
                contentH = mTextSize;
                contentH += getPaddingTop() + getPaddingBottom();
                resultH = contentH < widthSize ? contentH : heightSize;
            }
        }

        // This function must be set, otherwise an error will be reported
        setMeasuredDimension(resultW,resultH);

}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
    int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

    metrics = mPaint.getFontMetrics();
    cy += metrics.descent;

    canvas.drawColor(Color.RED);
    if (TextUtils.isEmpty(mText)) {
        return;
    }
    canvas.drawText(mText,cx,cy,mPaint);

}
Copy the code

The code is not that hard. We can verify it.

<com.frank.measuredemo.TestView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingLeft="2dp"
    android:paddingRight="2dp"
    android:paddingTop="2dp"
    android:textSize="24sp"
    android:text="test"/>
Copy the code



And you can see that’s what we’re looking for. It now ADAPTS to the MeasureSpec.AT_MOST schema. Let’s test another case

<com.frank.measuredemo.TestView
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:paddingLeft="2dp"
    android:paddingRight="2dp"
    android:paddingTop="2dp"
    android:textSize="48sp"
    android:text="test"/>

Copy the code



The same is true in MeasureSpec.EXACTLY mode.

Now, we have mastered the custom View measurement method, it is also very simple.

But it’s not over yet. We just validated a custom View, but it’s a little bit different for a ViewGroup.

View and ViewGroup, chicken and egg

A ViewGroup is a subclass of a View, but a ViewGroup’s mission is to load and organize views. It is just like a hen is a chicken. The hen lays eggs to hatch the chicks. When the chicks grow up, if the hen lays eggs, then do the eggs give birth to the chicken or the chicken gives birth to the egg?

Now that we’ve mastered the measurement of a custom View, let’s code to test the measurement realization of a custom ViewGroup.

If we want to create a ViewGroup, let’s call it TestViewGroup and lay out the child elements diagonally.

As mentioned earlier, a ViewGroup is essentially a View, but it has the additional obligation of layout child elements. Since it is a View, then a custom ViewGroup also needs to start from the measurement, the key problem is how to accurately get the ViewGroup size information?

We still need to discuss it carefully.

  1. When TestViewGroup is MeasureSpec.EXACTLY on a dimension, the size can be as suggested by the parent container. Remember that a ViewGroup has its own parent, and in its parent container, it’s just a View.
  2. When the TestViewGroup dimension is MeasureSpec.AT_MOST, the TestViewGroup needs to calculate the dimension itself. From the above information, the size of the TestViewGroup is very simple, which is to use its padding + the size of each child element (including the width and height of the child element + the marging of the child element) on a dimension to get a possible size number. This Size value is then compared to the recommended Size given by the parent container of TestViewGroup, resulting in the lowest possible value.
  3. When the TestViewGroup dimension is MeasureSpec.AT_MOST, it is important to get the exact size of the child element because the size of the child element is computed. Fortunately, Android provides a ready-made API.
  4. Once TestViewGroup has been measured successfully, the layout is needed. Custom views basically do not deal with this part, but custom ViewGroup, this part is indispensable. But this article is not about layout techniques, just to tell you that layout is actually relatively simple, nothing more than to determine the coordinates of the child elements and then layout.

Now, we can actually code it.

public class TestViewGroup extends ViewGroup {


    public TestViewGroup(Context context) {
        this(context,null);
    }

    public TestViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        // Only care about the margin information of the child element, so use MarginLayoutParams
        return new MarginLayoutParams(getContext(),attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW represents the width of the final setting, whereas resultH represents the height of the final setting */
        int resultW = widthSize;
        int resultH = heightSize;

        /** Take your padding into account */
        int contentW = getPaddingLeft() + getPaddingRight();
        int contentH = getPaddingTop() + getPaddingBottom();

        /** Measure the size of the child element, this step is necessary */
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        MarginLayoutParams layoutParams = null;

        for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt(i); layoutParams = (MarginLayoutParams) child.getLayoutParams();// When a child element is not visible, it does not participate in the layout, so its size does not need to be counted
            if ( child.getVisibility() == View.GONE ) {
                continue;
            }

            contentW += child.getMeasuredWidth()
                    + layoutParams.leftMargin + layoutParams.rightMargin;

            contentH += child.getMeasuredHeight()
                    + layoutParams.topMargin + layoutParams.bottomMargin;
        }

        /** The AT_MOST mode is the focus of TestViewGroup. The TestViewGroup determines the size of the child element, but cannot exceed the value recommended by the ViewGroup
        if ( widthMode == MeasureSpec.AT_MOST ) {
            resultW = contentW < widthSize ? contentW : widthSize;
        }

        if ( heightMode == MeasureSpec.AT_MOST ) {
            resultH = contentH < heightSize ? contentH : heightSize;
        }

        // This function must be set, otherwise an error will be reported
        setMeasuredDimension(resultW,resultH);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int topStart = getPaddingTop();
        int leftStart = getPaddingLeft();
        int childW = 0;
        int childH = 0;
        MarginLayoutParams layoutParams = null;
        for ( int i = 0; i < getChildCount(); i++ ) { View child = getChildAt(i); layoutParams = (MarginLayoutParams) child.getLayoutParams();// When a child element is not visible, it does not participate in the layout, so its size does not need to be counted
            if ( child.getVisibility() == View.GONE ) {
                continue; } childW = child.getMeasuredWidth(); childH = child.getMeasuredHeight(); leftStart += layoutParams.leftMargin; topStart += layoutParams.topMargin; child.layout(leftStart,topStart, leftStart + childW, topStart + childH); leftStart += childW + layoutParams.rightMargin; topStart += childH + layoutParams.bottomMargin; }}}Copy the code

We then test it by adding it to the XML layout file.

<com.frank.measuredemo.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    <com.frank.measuredemo.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:background="#00ff40"
        android:text="test"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test"/>
</com.frank.measuredemo.TestViewGroup>Copy the code

So what are the practical effects?

Try it out again by adding a fixed width and height to TestViewGroup.

<com.frank.measuredemo.TestViewGroup
    android:layout_width="350dp"
    android:layout_height="600dp"
    android:background="#c3c3c3">
    <com.frank.measuredemo.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:background="#00ff40"
        android:text="test"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test"/>
</com.frank.measuredemo.TestViewGroup>
Copy the code

The results are as follows:

Since then, we have also learned the basic steps of customizing a ViewGroup and have been able to handle the various measurement modes of a ViewGroup.

However, in the real job development process, the requirements are not constant, the content I mentioned above is just the basic rules, people can be comfortable with all kinds of situations when they are skilled in mind.

TestViewGroup is used as a demonstration example only to illustrate the measurement rules and basic custom routines. For starters in Android development, read more code. The key is to copy other people’s excellent custom views or Viewgroups.

In my opinion, trying to implement a streaming label control by yourself is a great improvement in the ability to customize viewgroups, because only when you practice yourself, you will think, in the process of thinking and experimenting you will deeply understand the use of measurement mechanism.

Customizing a streaming tag control is another topic, and maybe I’ll do another one, but I want you to get your hands on it. Below is a screenshot of an instance of my custom ViewGroup.


Yangyang Sasa wrote so much content, in fact, basically has finished, impatient students can directly jump to the following summary. However, for students who have the spirit of study, it is not enough. It’s not over yet.

Question 1: Who is actually measuring the View?

Question 2: When is it necessary to measure a View?

When we customize TestViewGroup, the onMeasure() method uses an API to measure child elements: measureChildren(). What does this method do? We can go check it out.

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if((child.mViewFlags & VISIBILITY_MASK) ! = GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); }}}protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

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

The code is short and straightforward, calling the child’s measure() method separately. Note that the measurement specification passed to the Child has changed, for example widthMeasureSpec becomes Child widthMeasureSpec. The reason is these two lines of code:

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);
Copy the code

Take another look at the implementation of the getChildMeasureSpec() method.

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
        // The superclass itself is in EXACTLY mode
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // If the child wants its own size, satisfy it and set its measurement mode to EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                // Child wants to be the same size as parent
                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.
                // The child wants to determine its own size, but only if it is not larger than the size suggested by the parent.
                // And set its measurement mode to AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        // The parent class itself is in AT_MOST mode and has a maximum size constraint on the parent class itself
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                // If the child wants its own size, satisfy it and set its measurement mode to EXACTLY
                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.
                // The size of the child is the same as that of the parent
                // Set the child mode to AT_MOST
                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.
                // The child wants to determine its own size, but only if it is not larger than the size suggested by the parent.
                // And set its measurement mode to AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        // The parent class itself is UNSPECIFIED, and the parent itself is expected to be as UNSPECIFIED as it wants, leaving the parent parent unrestricted.
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                // If the child wants its own size, satisfy it and set its measurement mode to EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                // Child wants to be the same size as parent, but parent also needs to be evaluated, so it can only be set to 0, and
                // Set the measurement mode of the Child to UNSPECIFIED
                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
                // The Size of the child should be determined by the parent.
                // The parent value can only be set to 0, and the maximum value of the child is not set
                // Set the measurement mode of the Child to UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

Copy the code

The code is clear. A lot of things are out there. Explains a lot of things, including:

  1. When layout_width or layout_height is set to exact value, it is MeasureSpec.EXACTLY.

  2. Why is layout_width or layout_height set to match_parent not necessarily guaranteed to be MeasureSpec.EXACTLY? It can also be MeasureSpec.AT_MOST or MeasureSpec.UNSPECIFIED.

  3. With Layout_width or layout_height set to WrAP_content, the most likely measurement mode is MeasureSpec.AT_MOST, but it could also be MeasureSpec.UNSPECIFIED.

Moving forward, the measureChild() method of the ViewGroup will eventually call the View.measure() method. We follow up further.


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {


        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final booleanspecChanged = widthMeasureSpec ! = mOldWidthMeasureSpec || heightMeasureSpec ! = mOldHeightMeasureSpec;final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final booleanneedsLayout = specChanged && (sAlwaysRemeasureExactly || ! isSpecExactly || ! matchesSpecSize);if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;



            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {


                // measure ourselves, this should set the measured dimension flag back
                // onMeasure is called here
                onMeasure(widthMeasureSpec, heightMeasureSpec);


                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } 

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) ! = PFLAG_MEASURED_DIMENSION_SET) {throw new IllegalStateException("View with id " + getId() + ":"
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;


    }
Copy the code

When a measure() method is called, if its measureSpec is different from the old one or requestLayout() is called, It triggers the onMeasure() method. Note that finally it checks for a bit of the mPrivateFlags variable, PFLAG_MEASURED_DIMENSION_SET, and throws an exception if this bit is not set to 1. This bit must be called in the onMeasure() method.

Okay, chicken and egg, chicken and egg seems to have a result. OnMeasure () in a ViewGroup calls view.measure () and view.measure () calls view.onmeasure ().

So, at last, we understood.

But what? Can we go any further?

The channel in the Activity, the top View?

Tao gives birth to one, life two, two three, three all things, all things negative Yin and Yang, and thought. — Tao Te Ching

We already know that for both views and viewgroups, measurements start with the measure() method, traversing all the way up the control tree. So, for an Android Activity, what is its top View or top ViewGroup?

Start with the setContentView

We know that setting the setContentView() resource file in onCreate() when laying out an Activity is about as top-level a View as the average developer can think of. For example, if you set a RelativeLayout in activity_main.xml, is that the top-level View of your Activity? Who calls its measure() method to trigger a measurement of the entire control tree?


public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initActionBar();
}

public Window getWindow() {
    return mWindow;
}

/** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as  the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * * 

The only existing implementation of this abstract class is * android.policy.PhoneWindow, which you should instantiate when needing a * Window. Eventually that class will be refactored and a factory method * added for creating Window instances without knowing about a particular * implementation. */

public abstract class Window { } public class PhoneWindow extends Window implements MenuBuilder.Callback { } Copy the code

As you can see, call Activity. The setContentView () is called PhoneWindow. The setContentView ().

PhoneWindow.java

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if(cb ! =null&&! isDestroyed()) { cb.onContentChanged(); }}Copy the code

Notice in the code above that the view passed in by setContentView is added to an mContentParent variable, so the above question can be answered. The View passed through setContentView() is not the top-level View of the Activity. Let’s look at mContentParent again.

It’s just a ViewGroup. Let’s focus again on the installDecor() function.

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();

    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
        mDecor.makeOptionalFitsSystemWindows();

        mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
        if(mTitleView ! =null) {
            mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
            if ((getLocalFeatures() & (1<< FEATURE_NO_TITLE)) ! =0) {
                View titleContainer = findViewById(com.android.internal.R.id.title_container);
                if(titleContainer ! =null) {
                    titleContainer.setVisibility(View.GONE);
                } else {
                    mTitleView.setVisibility(View.GONE);
                }
                if (mContentParent instanceof FrameLayout) {
                    ((FrameLayout)mContentParent).setForeground(null); }}else{ mTitleView.setText(mTitle); }}else{ mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); }}}Copy the code

The code was long, and I removed some code that wasn’t relevant to the topic. This method elicits a mDecor variable, which is created through the generateDecor() method. DecorView is an internal class defined by PhoneWindow, which is actually a FrameLayout.

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

}
Copy the code

Let’s return to the generate() method

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}
Copy the code

Now that we know how the DecorView is created, look at the mContentParent creation method generateLayout(). It passes into a DecorView, so it must have some relationship to mDecorView.

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.




    WindowManager.LayoutParams params = getAttributes();



    // Inflate the window decor.



    // Embedded, so no decoration is needed.
    layoutResource = com.android.internal.R.layout.screen_simple;
    // System.out.println("Simple!" );


    mDecor.startChanging();

    View in = mLayoutInflater.inflate(layoutResource, null);

    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }


    mDecor.finishChanging();

    return contentParent;
}

Copy the code

The original code is very long, I removed some of the cumbersome code, and the process becomes clear: the method inflates an XML file, which is then added to the mDecorView. The mContentParent is added to the view.

The XML file is com. The android. Internal. R.l ayout. Screen_simple, we can find it out from the SDK package.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="? android:attr/windowContentOverlay" />
</LinearLayout>
Copy the code

So that’s a LinearLayout, vertical. Two elements, one actionBar and one content. And the ViewStub causes the ActionBar to load only when it needs to.

The Activity has a PhoneWindow object. Inside the PhoneWindow is a DecorView. Inside the DecorView is a LinearLayout. The LinearLayout has the mContentParent with id android: ID/Content. The mContentParent is used to load the View passed in by the Activity via setContentView. So the whole structure came out.

Note: Because the code is simplified, the LinearLayout is actually composed of two parts. The lower part is the Content, and the upper part is not necessarily the ActionBar, but also the title, but it doesn’t affect us, we just need to remember the Content.

A DecorView is the root of the entire control tree in the Activity.

Who mapped the top-level View?

Since the DecorView is the starting point of the entire mapping, who mapped it? Who calls its measure() method, which causes a top-to-bottom sizing of the entire control tree?

The view.requestLayout () method can be used to rearrange the interface, so what does requestLayout() do?

Let’s go back to the PhoneWindow setContentView().

public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if(cb ! =null&&! isDestroyed()) { cb.onContentChanged(); }}Copy the code

Let’s look at what happens with mContentParent. AddView (View, params).

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}
Copy the code

It calls requestLayout() just to explore it, so after setContentView, the control tree will be mapped. But here is the conclusion, and we need to verify the process.

public void requestLayout() {
    if(mMeasureCache ! =null) mMeasureCache.clear();

    if(mAttachInfo ! =null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if(viewRoot ! =null && viewRoot.isInLayout()) {
            if(! viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if(mParent ! =null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if(mAttachInfo ! =null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null; }}Copy the code

The View adds two flags PFLAG_FORCE_LAYOUT and PFLAG_INVALIDATED to mPrivateFlags in requestLayout(). Another object, ViewRootImpl, is also introduced. Then call the parent’s requestLayout().

We are thinking that the top-level View is a DecorView, but does it have a parent? If so, what is it? I was puzzled by the fact that the DecorView didn’t have a parent container, so I lost the clue I was following and found it later. Now I can give you a hint. The mParent in a View is actually an interface: ViewParent.

/** * Defines the responsibilities for a class that will be a parent of a View. * This is the API that a view sees when it wants to interact with its parent. * */
public interface ViewParent {
    /** * Called when something has changed which has invalidated the layout of a * child of this view parent. This will schedule a layout pass of the view * tree. */
    public void requestLayout(a); . }Copy the code

The parent of a View must be a ViewGroup. ViewGroup is just one of the implementation classes of the ViewParent interface.

public abstract class ViewGroup extends View implements ViewParent.ViewManager {
}
Copy the code

Let’s take a look at the ViewRootImpl source.


/** * The top of a view hierarchy, implementing the needed protocol between View * and the WindowManager. This is for the most part an internal implementation * detail of {@link WindowManagerGlobal}. * * {@hide} */
@SuppressWarnings({"EmptyCatchBlock"."PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent.View.AttachInfo.Callbacks.HardwareRenderer.HardwareDrawCallbacks {

}Copy the code

You can see that ViewRootImpl is also the implementation class of ViewParent, so theoretically it could also be the parent of a DecorView. Note again that the comment explains that ViewRootImpl is the top of the control tree. For more details, I need to look up Windows ManagerGlobal. All right, let’s do it.


/**
 * Provides low-level communication with the system window manager for
 * operations that are not associated with any particular context.
 *
 * This class is only used internally to implement global functions where
 * the caller already knows the display and relevant compatibility information
 * for the operation.  For most purposes, you should use {@link WindowManager} instead
 * since it is bound to a context.
 *
 * @see WindowManagerImpl
 * @hide* /
public final class WindowManagerGlobal {

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if(! (paramsinstanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        if(parentWindow ! =null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {


            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true); }}throwe; }}}Copy the code

There’s not a lot of code, but the addView() method caught my interest. Here, the ViewRootImpl object is created. And the class comments tell me to look at Windows Manager IMPl.

Feeling deeper and deeper, there is no way, take a deep breath, continue to move forward.

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Display mDisplay;
    private final Window mParentWindow;

    public WindowManagerImpl(Display display) {
        this(display, null);
    }


    @Override
    public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); }}Copy the code

WindowManagerImpl contains a WindowManagerGlobal object, which represents WindowManagerImpl. And WindowManagerImpl is the implementation class of WindowManager. That’s what we use when we write hover Windows.

WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Copy the code

At this point, the thread seems to have run dry. However, it is important to understand that an Activity is also a Window, so you must create an Activity through the WindowManager addView.

Back to activities, there is another PhoneWindow worth exploring. Search the Activity source for where the window is assigned. And there it is.

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config) {
        attachBaseContext(context);

        mFragments.attachActivity(this, mContainer, null);

        mWindow = PolicyManager.makeNewWindow(this);
        mWindow.setCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this); mUiThread = Thread.currentThread(); mMainThread = aThread; mInstrumentation = instr; mToken = token; mIdent = ident; mApplication = application; mIntent = intent; mComponent = intent.getComponent(); mActivityInfo = info; mTitle = title; mParent = parent; mEmbeddedID = id; mLastNonConfigurationInstances = lastNonConfigurationInstances; mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) ! =0);
        if(mParent ! =null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
 }
Copy the code

MWindow is created through PolicyManager. And established a connection with Windows Manager.

public final class PolicyManager {
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";

    private static final IPolicy sPolicy;

    static {
        // Pull in the actual implementation of the policy at run-time
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex); }}// Cannot instantiate this class
    private PolicyManager() {}

    // The static methods to spawn new policy-specific objects
    public static Window makeNewWindow(Context context) {
        returnsPolicy.makeNewWindow(context); }}Copy the code

A Policy object is created by reflection, and its makeNewWindow() method is called to get a Window() object.

public class Policy implements IPolicy {
    private static final String TAG = "PhonePolicy";

    private static final String[] preload_classes = {
        "com.android.internal.policy.impl.PhoneLayoutInflater"."com.android.internal.policy.impl.PhoneWindow"."com.android.internal.policy.impl.PhoneWindow$1"."com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback"."com.android.internal.policy.impl.PhoneWindow$DecorView"."com.android.internal.policy.impl.PhoneWindow$PanelFeatureState"."com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState"};static {
        // For performance reasons, preload some policy specific classes when
        // the policy gets loaded.
        for (String s : preload_classes) {
            try {
                Class.forName(s);
            } catch (ClassNotFoundException ex) {
                Log.e(TAG, "Could not preload class for phone policy: "+ s); }}}public Window makeNewWindow(Context context) {
        return newPhoneWindow(context); }}Copy the code

At this point, we finally know that the mWindow in the Activity is a PhoneWindow.

After going round and round, we seem to be back to square one, but thankfully we know where the PhoneWindow came from in the Activity.

View at the top, who maps it?

So that you don’t forget it, I throw the question out again. This is our ultimate goal, and with that answer in mind, this article is over.

However, for now, the current information, we seem to have no way to know, according to the above steps of thinking, we can not find the answer to this problem. This road is blocked, we need to think of another way. What we are looking for is who called the decorView.measure () method in the first place and ended up redrawing the entire control tree. Now that we have a clue, we have to dig into Activity again.

Talk about Activity

I have read the source code of ActivityManager, WindowManager and PackageManager before. Most of us are most concerned with the Activity lifecycle process.

Yes, we load the layout file in the Activity’s onCreate() method, but only after onResume() does the image appear. So, we can start there.

I only wrote a brief article about the process of Activity, because it is a different topic, and this topic is huge and tedious, so I will choose to ignore many details. Students who are not familiar with it can look through the materials themselves.

The Activity of creating

When we learn Java, we all know that we need to write a class to run, and then contain a main() method, but the Activity does not let us implement main(), the program also still run, this is probably a lot of Android beginners more confused place. In fact, the Activity is running on the Android Framework, the program we write is actually running on the Google engineers built on the program, can be said to be someone else’s program fragment, this is the Framework. Android applications run by creating a class with a main() method called ActivityThread. The name is associated with Activity. Let’s introduce another class, ActivityManagerService, which is one of the system’s most important services for scheduling and handling related requests from application-layer clients.

The origin of the Activity’s onResume()

When an Activity starts for the first time, it must execute onCreate() before the onResume() method.

 private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {· Activity A = performLaunchActivity(r, customIntent);if(a ! =null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;

        handleResumeActivity(r.token, false, r.isForward, ! r.activity.mFinished && ! r.startsNotResumed); }else {
        // If there was an error, for any reason, tell the activity
        // manager to stop us.
        try {
            ActivityManagerNative.getDefault()
                .finishActivity(r.token, Activity.RESULT_CANCELED, null);
        } catch (RemoteException ex) {
            // Ignore}}}Copy the code

You can see that the performLaunchActivity() is followed by handleResumeActivity(). In performLauncherActivity(), you create the Activity and call the onCreate() method. If you’re interested, look up the code yourself.

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
            boolean reallyResume) {


    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if(r ! =null) {
        final Activity a = r.activity;


        if (r.window == null && !a.mFinished && willBeVisible) {
            // Focus on this code
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

            if (a.mVisibleFromClient) {
                a.mWindowAdded = true; wm.addView(decor, l); }}}}Copy the code

You might see ActivityThread calling windowManager.addView () in the handleResumeActivity() method, This method is the final call part earlier in this article has described the WindowManagerGlobal. AddView ().

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {


    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true); }}throwe; }}Copy the code

When we looked at this code earlier, we didn’t understand what it was for. Now I think I know. After the Activity is created, save the DecorView into the ViewRootImpl when onResume() is executed.

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
            if (mView == null) {
                mView = view;

                mAdded = true;

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();


                view.assignParent(this); }}}Copy the code

I streamlined the code and note that ViewRootImpl sets itself to be the parent of a DecorView, which illustrates a problem:

RequestLayout () calls from any View in the control tree will eventually call requestLayout() of the ViewRootImpl associated with the Activity. The ViewRootImpl manipulates the entire control tree. It lives up to its name

ViewRootImpl through setView () method will save DecorView to mView variable, and perform the ViewRootImpl. RequestLayout ().

public void requestLayout() {
    if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}void scheduleTraversals() {
    if(! mTraversalScheduled) { mTraversalScheduled =true;
        mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); scheduleConsumeBatchedInput(); }}Copy the code

MChoreographer is a Choreographer object that you can simply think of as a Handler to choreograph the drawing of Android interfaces. It will eventually invoke the doTraversal() method.

final class TraversalRunnable implements Runnable {
    @Override
    public void run() { doTraversal(); }}void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

        try {
            performTraversals();
        } finally{ Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}}Copy the code

The performTraversals() method of ViewRootImpl is called as a result. This code is very, very, very long. I only excerpted what we needed, please refer to the source code for complete information.

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;


        if (host == null| |! mAdded)return;

        mIsInTraversal = true;


        WindowManager.LayoutParams lp = mWindowAttributes;

        int desiredWindowWidth;
        int desiredWindowHeight;



        WindowManager.LayoutParams params = null;
        if (mWindowAttributesChanged) {
            mWindowAttributesChanged = false;
            surfaceChanged = true;
            params = lp;
        }



        if (mFirst) {


            if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {}else{ DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; }}else{}booleanlayoutRequested = mLayoutRequested && ! mStopped;if (layoutRequested) {

            final Resources res = mView.getContext().getResources();


            // Ask host how big it wants to bewindowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); }}Copy the code

The above is the result of removing a lot of code, mainly to show that performTraversals() first measures the proposed size as the width and height of the screen, and then calls measureHierarchy().

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;

        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
                "Measuring " + host + " in display " + desiredWindowWidth
                + "x" + desiredWindowHeight + "...");

        boolean goodMeasure = false;
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            // On large screens, we don't want to allow dialogs to just
            // stretch to fill the entire width of the screen to display
            // one line of text. First try doing the layout at a smaller
            // size to see if it will fit.
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
            if(baseSize ! =0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                } else {
                    // Didn't fit in that size... try expanding a bit.
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                            + baseSize);
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                        goodMeasure = true; }}}}if(! goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);if(mWidth ! = host.getMeasuredWidth() || mHeight ! = host.getMeasuredHeight()) { windowSizeMayChange =true; }}return windowSizeMayChange;
    }Copy the code

Set different measurement modes based on the LayoutParam value. The getRootMeasureSpec() method is called, followed by the performMeasure() method.

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

You can see that when we set the LayoutParam property to WRAP_content in our DecorView, it measures as MeasureSpec.AT_MOST and everything else as MeasureSpec.EXACTLY, The recommended size is the screen width and height in that direction. Let’s look at the performMeasure() method.

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

    try {

        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    } finally{ Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}Copy the code

MView is a DecorView. The ViewRootImpl calls the DecorView’s measure() method to initiate a measurement of the entire control tree.

The Activity is full-screen, so it can be inferred that it is full-screen. But is that really the case?

DecorView LayoutParams are actually mWindowAttributes viewrootimpl.java in ViewRootImpl

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
Copy the code

WindowManager.java

 public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {

    public LayoutParams() {
        super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = TYPE_APPLICATION; format = PixelFormat.OPAQUE; }}Copy the code

MWindowAttributes width and height measurement mode are match_parent. So the recommended size that the DecorView ultimately measures is the width and height of the screen.

Ok, to recap, ever since we know that DecorView is the outermost View of the Activity, we’ve been trying to figure out who’s mapping it, calling its measure() method. After many iterations, Activity and ActivityThread are dragged out. Finally find the source ViewRootImpl.

conclusion

If you’ve been reading all the way up to this point, I want to say thank you for being here.

The ultimate purpose of this article is not to confuse you. If the length of the article has affected your thinking, I will make a final summary.

  1. The View is measured from the measure() method to the onMeasure().

  2. The onMeasure() of the View receives the parent’s measurement specification suggestion. The measurement specification contains the measurement mode and recommended dimensions.

  3. The measurement mode contains MeasureSpec.EXACTLY, MeasureSpec.AT_MOST, and MeasureSpec.UNSPECIFIED.

  4. As for MeasureSpec.UNSPECIFIED, the Parent’s measurement mode is set directly to the MeasureSpec.EXACTLY mode. MeasureSpec.AT_MOST is a MeasureSpec.AT_MOST is a MeasureSpec.AT_MOST is a MeasureSpec.

  5. The View override onMeasure() method must finally be dimensioned using setMeasuredDimension(), otherwise an exception will be reported.

  6. Customizing a ViewGroup is slightly more difficult than a View, because onMeasure() measures the width and height of child elements, so measure the size of child elements by measureChildren() or measureChild(). We then call getMeasureWidth()/getMeasureHeight() to get the desired size of the child element.

  7. An Activity has a tree of controls, and a DecorView is the top-level View, but the ViewRootImpl object associated with the Activity manipulates them.

The above.