Android Interface Generation Process: Measure of View

Keywords: Android Windows Attribute xml

review

In the article "The relationship between Windows Manager and Windows" As mentioned, each activity contains a Window, and each Windows contains a DecorView. DecorView is a subclass of FrameLayout and is the root node view. By default, it contains a LinearLayout vertical layout, which is titlebar above and mContentParent below. The titlebar style is modified according to the theme settings during generation. Then our main layout file is added to mContentParent through setContentView. These operations are performed when the handler Launch Activity in Activity calls onCreate, and after execution, the handler Resume Activity in Activity is called, where the onResume operation is executed.

The onResume operation adds the DectorView generated in the previous step to Windows through Windows Manager, and then calls ViewRoot to start measuring, laying out and drawing the View. At the same time, the judgment of "View of sub-threads can not modify UI threads" is made here. Talking about Subthreads again - You can update the UI in non-UI threads As mentioned in the article, if the onCreate neutron thread operates the UI without sleeping, it only modifies the attributes, and does not wait for the judgement to be made in onResume. But if a sleep operation is executed, and the sleep operation waits or exceeds the generated time, it happens to encounter this judgment, then an exception will be thrown.

Note that the width of DecorView is the same as that of the mobile screen, but if we don't have a hidden status bar when we set the theme, then our titlebar will move down a little, which is exactly equal to the height of the status bar.

A brief description of Android's process of drawing view s:

Simple descriptions can be explained as: calculating size, layout coordinates, draw ing to the screen;

Now let's see what each step is like.

  • Step 1: When activity starts, the initialization view process is triggered by the DecorView call View of the Window s object (specifically how to read from xml using the public final void measure(int widthMeasureSpec, int heightMeasureSpec) method of the LayoutInflater.from(context).inflate) object. This method is fi. Nal type, that is, all subclasses can not inherit this method to ensure that the principle of android initialization view remains unchanged. Specific parameter class values will be introduced later.

  • Step 2: View measurement method onMeasure(widthMeasureSpec, heightMeasureSpec), which calculates the real view size. Note: The size of the view is determined by the father view and his own size, not by a single decision. That's why subclasses of ViewGroup re-use this method, such as LinearLayout. Because they have to calculate the size of themselves and their child views. The View base class has its own implementation, just setting the size. In fact, according to the source code, the process of measurement is essentially to convert Match_parent and wrap_content to actual size.

  • Step 3: When the measurement is over, go back to DecorView and calculate the size, then the layout is started and the public final void layout(int l, int t, int r, int b) of the view is called, which is also final type, with the same purpose as the measurement method. The onlayout(int l, int t, int r, int b) method is called inside the layout method, which is abstract ed by two ViewGroups, so when we inherit the ViewGroup, we need to re-use the method. The essence of this method is to calculate the coordinate points of view on the screen by measuring the calculated size.

  • Step 4: Once you have measure d and laid out, you will start drawing on the screen. So you will call the public void draw(Canvas canvas) method of view. At this time, the method is not final. The reason is that the programmer can draw by himself and the ondraw method will be called internally. We often need to rewrite the method.

text

Starting with ViewRootImpl# Perform Traveals

private void performTraversals() {
            ...

        if (!mStopped) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
            }
        } 

        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
        }


        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }

                performDraw();
            }
        } 
        ...
}

The method is very long. Here we simplify it. We can see that there are three main methods in it. They are performance measure, performance Layout and performance Draw. Within these three methods, we call measure, layout and draw to carry out different processes. Let's first look at the method of performance measure (child Width Measure Spec, child HeightMeasure Spec), which passes in two parameters, child Width Measure Spec and child HeightMeasure. What do these two parameters mean? To understand the meaning of these two parameters, we need to understand MeasureSpec first.

Understanding MeasureSpec

MeasureSpec is an internal class of the View class. Let's first look at the official document describing the MeasureSpec class: A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is included of a size and a It means that this class encapsulates the size of a View, including information about the width and height of the View, but it should be noted that MeasureSpec does not mean the measurement width and height of the View, it is different, it is based on MeasueSpec to measure the measurement width and height.

MeasureSpec's function is to convert View's Layout Params into corresponding MeasureSpec according to the rules imposed by the parent container in the MeasureSpec process, and then determine the width and height of the View according to the MeasureSpec in the onMeasure method.

Let's look at the source code for this class:

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

        /**
          * UNSPECIFIED Pattern:
          * The parent View does not have any restrictions on the child View, as much as the child View needs.
          */ 
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
          * EXACTYLY Pattern:
          * The parent View has measured the exact size required by the child Viwe, at which point the final size of the View is
          * It is the value specified by SpecSize. Corresponding to match_parent and exact numerical model
          */ 
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
          * AT_MOST Pattern:
          * The final size of the child View is the SpecSize value specified by the parent View, and the size of the child View cannot be greater than that value.
          * That is, the corresponding wrap_content pattern
          */ 
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //Package size and mode into a 32-bit int value
        //High 2 bits for SpecMode, measurement mode, and low 30 bits for SpecSize. Specification size in some measurement mode
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        //Unpack the 32-bit MesureSpec, return to SpecMode, measurement mode
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        //Unpack the 32-bit MesureSpec and return it to SpecSize, the size of the spec in some measurement mode
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        //...
    }

As you can see, the idea of this class is fairly clear. For each View, including DecorView, a MeasureSpec is held, and the MeasureSpec saves the size specifications of the View. In the measurement process of View, breadth and height information is saved by making MeasureSpec, and in other processes, patterns and breadth are obtained by getMode or getSize. So the problem arises. As mentioned above, MeasureSpec is influenced by Layout Params and the parent container pattern. So, for DecorView, it is already the top-level view. Without the parent container, how does its MeasureSpec come from?

To solve this problem, we go back to the ViewRootImpl PerformTraveals method and look at the code number 1. We call the getRootMeasureSpec (desired Windows Width, lp. width) method, where desired Windows Width is the size of the screen, and assign the returned results to the child WidthMeasureSpec member variable (HeightMeasure Spec is the same as the same). Therefore, child Width MeasureSpec (child HeightMeasureSpec) should save DecorView's MaestSpec, so let's take a look at the implementation of ViewRootImpl#getRootMeasureSpec method:

/**
 * @param windowSize
 *            The available width or height of the window
 *
 * @param rootDimension
 *            The layout params for one dimension (width or height) of the
 *            window.
 *
 * @return The measure spec to use to measure the root view.
 */
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;
    //Elimination of ____________.

    }
    return measureSpec;
}

The idea is also clear, according to different modes to set MeasureSpec, if it is LayoutParams.MATCH_PARENT mode, it is the size of the window, WRAP_CONTENT mode is the size of uncertainty, but can not exceed the size of the window and so on.

So far, we have obtained a MesureSpec of DecorView, which represents the specification and size of the root View. In the next measurement process, we measure each sub-View layer by layer according to the MesureSpec of the obtained root View. Let's go down to the PerfmMeasure method and see what it does. ViewRootImpl# PerfmMeasure:

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

The method is very simple. We call mView.measure directly, where mView is DecorView. That is to say, we start the measurement process from the top View, then we go directly into the measurement process.

Measurement flow chart

Measurement process of ViewGroup

Since DecorView inherits from FrameLayout, which is an internal class of PhoneWindow, and FrameLayout does not have a measure method, it calls the measure method of the parent View. We look directly at its source code, View measure:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
        ...
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } 
        ...
}

As you can see, it calls the onMeasure method internally, and since DecorView is a subclass of FrameLayout, it actually calls the DecorView#onMeasure method. Inside this method, some judgments have been made, but instead of expanding here, the super.onMeasure method, the FrameLayout#onMeasure method, will be invoked at the end.

Because different ViewGroup s have different properties, their onMeasure must be different, so it is impossible to analyze all the onMeasure methods of layout. Therefore, this paper chooses the onMeasure method of FrameLayout for analysis. Readers of other layout methods can analyze themselves. So let's continue to look at this method:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //Get the number of subviews in the current layout
    int count = getChildCount();

    //Determine whether the width of the current layout is match_parent mode or specify an exact size, and if so, set measureMatchParent to false.
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    //Traversing through subviews of all types that are not GONE
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //Measure each subview
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //Find the maximum width and height in the child View, because if FrameLayout is the wrap_content attribute
            //Then its size depends on the largest one in the subview
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            //If FrameLayout is wrap_content mode, add it to mMatchParentChildren
            //A child View with a width or height of match_parent, because the final measurement size of the child View is affected by the final measurement size of FrameLayout
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

    //Preservation of measurement results
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    //Number of matchparents set in subview
    count = mMatchParentChildren.size();
    //The following statement will be executed only if the mode of FrameLayout is wrap_content
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            //Width specification settings for FrameLayout because this affects the measurement of sub-View s
            final int childWidthMeasureSpec;

            /**
              * If the width of the child View is the match_parent attribute, modify the MesureSpec of the current FrameLayout:
              * Modify the width specification of widthMeasureSpec to total width - padding - margin, which means:
              * For sub-Viw, if match_parent is to be used, it can cover the measured width of FrameLayout.
              * The space left after padding and margin are subtracted.
              *
              * In conclusion, you can see the getChildMeasureSpec() method:
              *
              * If the width of the subview is a definite value, such as 50dp, then the width specification of FrameLayout's widthMeasureSpec is modified to:
              * SpecSize For the width of the child View, that is, 50dp, SpecMode is EXACTLY mode
              * 
              * If the width of the child View is the wrap_content attribute, the width specification of the widthMeasureSpec of FrameLayout is modified to:
              * SpecSize Subtract padding from margin for the width of the child View and SpecMode from AT_MOST mode
              */
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
            //Similarly, the same treatment is applied to the height. Here, we omit the following.

            //For this part of the sub-View, the measure ment process needs to be re-executed
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

As can be seen from the onMeasure process of FrameLayout above, it has done quite a lot of work. Here is a brief summary: Firstly, FrameLayout measures every sub-View according to its MeasureSpec, i.e. calling the measureChildWithMargin method, which will be described in detail below; for each measurement. The completed sub-View will find the largest width and height, so the measurement width and height of FrameLayout will be affected by the maximum width and height of the sub-View (wrap_content mode). Then the setMeasureDimension method is called to save the measurement width and height of FrameLayout. Finally, the special case is dealt with, that is, when FrameLayout is wrap_content attribute, if its child View is match_parent attribute, the measurement specification of FrameLayout should be reset, and then the measurement of this part of View should be re-measured.

The setMeasureDimension method mentioned above is used to save the measurement results. In the source code above, the parameters of the method receive the return value of the resolveSizeAndState method. So let's look directly at the View#resolveSizeAndState method:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

It can be seen that the idea of this method is quite clear. When the specModel is EXACTLY, the width and height specifications in MeasureSpec are returned directly as the final measurement width; when AT_MOST is used in specMode, the width specifications and the minimum size of MeasureSpec are taken. (Note: The size here, for FrameLayout, is the measurement width of its largest sub-View).

Summary

So far, with DecorView as the starting point, the measurement process of ViewGroup is analyzed in detail. The size of DecorView is obtained in ViewRootImpl# performance Traversals, and then the measurement process is started in the performance Measure method. There are different ways to implement different layout, but generally in onM. In the easure method, each child View is traversed, and the measurement width and height of the parent container are determined according to the MesureSpec of the ViewGroup and the layoutParams of the child View. Finally, the measurement width and height of the parent container are determined according to the measurement width and height information of all the child views.

Next, let's continue to analyze how measurements are made for a sub-View.

The Measuring Process of View

Remember the measureChildWithMargin method mentioned above in FrameLayout measurements. The main parameters it receives are the child View and the MassureSpec of the parent container, so its function is to measure the child View. So let's look directly at this method, ViewGroup#ChildmeasureWithMargins:

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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 1
}

From the source code, it calls the getChildMeasureSpec method, which passes in the MassureSpec of the parent container and its own layoutParams attributes to obtain the MassureSpec of the child View, which also confirms the conclusion that the MassureSpec of the child View is decided by the MassureSpec of the free parent container and its own LayoutParams. So let's look at the ViewGroup getChildMeasureSpec method:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    //Size represents the available space for child View: parent container size minus padding
    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:
       //Elimination. Specific self-referencing source code
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
       //Omit... Specific self-reference source code
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

The above method is also very easy to understand, presumably based on different parent container patterns and child View layoutParams to determine the size pattern of child View, and so on. Then, according to the above logic, we list the different MesureSpec of the child View in the case of the combination of MesureSpec of the different parent container and Layout Params of the child View:

When the MesureSpec of the sub-View is obtained, we return to the MesureChildWithMargins method, and then execute the number 1 code: child.measure method, which means that the drawing process has been transferred from ViewGroup to the sub-View. You can see that the parameters passed are MesureSpec of the sub-View we just obtained, and then the View will be called. # Measure, as mentioned above, is no longer discussed here, and then the onMeasure method is called in the measure method. Of course, for different types of Views, the onMeasure method is different, but for different Views, even for custom Views, we will call the View in the overridden onMeasure method.# The onMeasure method, so let's look at its source code:

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

Obviously, the setMeasureDimension method is invoked here. As mentioned above, the method is used to set the measurement width and height. The measurement width and height are obtained from getDefaultSize. Let's continue to look at the method View#getDefaultSize:

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

Well, it's a similar code. Different measurement widths and heights are set according to different modes. Let's look directly at AT_MOST and EXACTLY modes, which directly return specSize. That is to say, the measurement widths and heights of View in these two modes depend directly on specSize specifications. That is to say, for a custom View directly inherited from View, its wrap_content and match_parent attributes have the same effect, so if we want to implement wrap_content of the custom View, we need to override the onMeasure method to process the wrap_content attributes.
Next, let's look at the UNSPECIFIED model, which may be relatively rare. It is commonly used for internal measurement of systems. It returns size directly, not specSize. Where does size come from? Looking up, it comes from getSuggested Minimum Width () or getSuggested Minimum Height (), so let's take a look at the source code. View# getSuggested Minimum Width:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

As can be seen from the above logic, when the View does not set the background, it returns mMinWidth, which corresponds to the android:minWidth attribute; if the background is set, it returns the maximum values in mMinWidth and mBackground.getMinimumWidth. So what about mBackground.getMinimumWidth? In fact, it represents the original width of the background, for example, for a Bitmap, its original width is the size of the picture. At this point, the measurement process of sub-View is also completed.

summary

Here is a brief summary of the whole process: measurement begins with DecorView, and determines the MesureSpec of sub-View according to MesureSpec of ViewGroup and Layout Params of sub-View by continuously traversing the measurement method of sub-View. The measurement width of sub-View is obtained further, and then returned layer by layer, and the measurement width of ViewGroup is saved continuously.

Think about the whole measure ment process with questions

The workflow of the View Framework is: measure each View --> Place each View in its corresponding position --> draw each View.

1. Why does the system have a measure ment process?

When a developer draws a UI, he configures the UI basically by means of an XML layout file. The two group attributes that each View must set are layout_width and layout_height, which represent the size of the current View.

Official document screenshots:

So the values of these two attributes must be specified. The values of these two attributes can only be of three types:

  1. Fixed size, such as 100dp.

  2. Just wrap the contents, wrap_content.

  3. To be as big as the parent layout, match_parent/fill_parent.

Because Android wants to provide a more elegant GUI framework, it provides adaptive sizes, wrap_content and match_parent.

Imagine that if these attributes allow only a fixed size, then the size of each View is determined at the time of drawing, so the measurement process may not be required. However, due to the need to meet the mechanism of adaptive size, a measurement process is needed.

2. What did the measure ment process do?

Because of the mechanism of adaptive size mentioned above, the real size of View can not be determined when the adaptive size is used to define the size of View. But the View size eventually needs to be mapped to the pixel size on the screen, so the measurement process is to do this, calculate various size values, and get specific pixel values. The measurement process traverses the entire View tree and then measures the actual size of each View in turn. Specifically, each ViewGroup sends a measure command to each sub-View within it, and then measures its size by onMeasure() of the specific sub-View. The final measurements are stored in View's mMeasuredWidth and mMeasuredHeight in pixels.

3. For adaptive size mechanism, how to measure a View tree reasonably?

After traversing the layout file, the system generates the corresponding view tree structure in memory for the layout file. At this time, all the view objects of the whole view tree species have no specific size, because the measure ment process is ultimately to determine the exact size of each view, that is, the exact pixel value. But at the beginning, the values of layout_width and layout_height attributes in View are only adaptive sizes, i.e. match_parent and wrap_content, which are negative numbers in the system, so the system will not treat them as specific size values. So when a View needs to convert its internal match_parent or wrap_content into specific pixel values, it needs to know two information.

  1. For match_parent, what is the current specific pixel value of the parent layout, because match_parent means that the child View wants to be as large as the parent layout.

  2. For wrap_content, sub-View needs to calculate a reasonable minimum that can wrap all content according to its current internal content. But if this minimum is larger than the current parent layout, that's not possible. The parent layout will tell you that I'm only that big, and you shouldn't exceed this size.

Because of the particularity of tree data structure, when we study the process of measure ment, we can only study a simple scene of ViewGroup and two Views. The schematic diagram is as follows:

That is to say, in the measurement process, ViewGroup will make a comprehensive evaluation according to its current situation, combining the size data of the child View, and then inform the child View of the relevant information. When the child View is in onMeasure itself, it needs to consider both its own content size and the limitation of the parent layout. Make information, then evaluate it comprehensively, and measure an optimal result.

4. How does the ViewGroup pass restriction information to the child View?

When it comes to passing restriction information, it's the MeasureSpec class, which runs through the measurement process to pass restriction information from parent layout to child View size measurement. Simply put, this class holds two types of data.

  1. The specific size of the parent layout where the child View is currently located.

  2. The type of restriction that the parent layout imposes on the child View.

Then the types of restriction can be divided into three types:

  1. UNSPECIFIED, not limited. This means that I can give you as much as the sub-View wants, and you can be assured of bold measure ments, regardless of the others. It doesn't matter what size I pass you. (In fact, Android is recommended in the high version, as long as it is in this mode, the size is set to 0)

  2. EXACTLY, accurate. That is to say, according to my current situation, considering the size parameters you specify, you should be this size. In the size attributes of MesureSpec, check it for yourself. You don't care how big your content is, just use this size.

  3. AT_MOST, the most. That is to say, according to my current situation, considering the size parameters you specified, you can measure a size that just can wrap your content without exceeding the size I gave you.

Source code analysis

In the source code of View, the following information about the measure ment process is extracted.

We know that the root node of the whole View tree is DecorView, which is a FrameLayout, so it is a ViewGroup, so the measurement of the whole View tree begins with the measure method of a ViewGroup object.

View:

1,measure

/** To begin measuring the size of a View, the parent will provide constraint information in the parameters. The actual measurement is done in onMeasure(), which calls the onMeasure() method, so only onMeasure can and must be override. */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);

The parent layout calls child.measure in its own onMeasure method, which transfers the measure process to the child View.

2,onMeasure

/** Specific measurement process, measurement view and its content, to determine the measurement width (mMeasured Width mMeasured Height). In this method, set Measured Dimension (int, int) must be invoked to save the width and height of the view measurement. */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

In this method, the child View will measure its own size reasonably according to the restriction information given by the parent layout and its own content size.

3,setMeasuredDimension

/** Preservation of measurement results */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);

When the View measurement is finished, the measurement results are saved in mMeasuredWidth and mMeasuredHeight.

ViewGroup:

1,measureChildren

/** For all child views to measure their own size, you need to consider the current ViewGroup's MesureSpec and Plus. Skip child views with gone status */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

measure all the sub-View sizes and hand the measurement process inside the sub-View.

2,measureChild

/** To measure a single View, you need to consider MesureSpec and Plus of the current ViewGroup. */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

Measure each specific subview.

3,measureChildWithMargins

/** To measure a single View, you need to consider the MesureSpec and Plus, margins of the current ViewGroup. */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();

Measure each specific subview. But information like margin needs to be taken into account.

4,getChildMeasureSpec

/** measureChildren The most difficult part of the process is calculating MeasureSpec for children. This method calculates the correct MeasureSpec for each dimension (width, height) of each child. The goal is to combine the current view group's MesureSpec with child's Layout Params to produce the most reasonable results.
For example, the current ViewGroup knows its exact size, because MeasureSpec's model is EXACTLY, and the child wants to match_parent, then it generates a MeasureSpec whose model is EXACTLY and its size is ViewGroup.
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension);

According to the current situation and the size parameters of the specific sub-View, a reasonable restriction information is calculated for the specific sub-View.

Source 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;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
Pseudo-code:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        Get the size and pattern in the restriction information.
        switch (Patterns in Restricted Information){
            The parent container of the case's current container sets an exact size for the current container:
                if (Sub View Application Fixed Size){
                    You just use the size you applied for.
                } Other if (child View wants to be as big as parent container){
                    You just use the size of the parent container.
                } Other if (sub View wants to wrap content){
                    Your maximum size is the size of the parent container, but you still need to measure your size as small as possible, and wrapping your content is enough.
                } 
                    break;
            The parent container of the current container of case sets a maximum size for the current container:
                if (Sub View Application Fixed Size){
                    You just use the size you applied for.
                } Other if (child View wants to be as big as parent container){
                    Your maximum size is the size of the parent container, but you still need to measure your size as small as possible, and wrapping your content is enough.
                } Other if (sub View wants to wrap content){
                    Your maximum size is the size of the parent container, but you still need to measure your size as small as possible, and wrapping your content is enough.
                } 
                    break;
            The parent container of the current container does not limit the size of the current container:
                if (Sub View Application Fixed Size){
                    You just use the size you applied for.
                } Other if (child View wants to be as big as parent container){
                    The parent container does not restrict the size of the child View.
                } Other if (sub View wants to wrap content){
                    The parent container does not restrict the size of the child View.
                }
                    break;
        } return restriction information on sub-View size;
    }

When customizing View, you also need to deal with the measure ment process. There are two main cases.

1. Subclasses inherited from View.

You need to overwrite onMeasure to measure yourself correctly. Finally, you need to call setMeasuredDimension to save the measurement results.

Generally speaking, the measure procedure pseudocode of the custom View is:

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);

int viewSize = 0;

swith (mode) {
    case MeasureSpec.EXACTLY:
        viewSize = size; //The current View size is set to the parent layout size
        break;
    case MeasureSpec.AT_MOST:
        viewSize = Math.min(size, getContentSize()); //The current View size is the minimum of the content size and the parent layout size
        break;
    case MeasureSpec.UNSPECIFIED:
        viewSize = getContentSize(); //Set the size of the content as large as it is
        break;
    default:
        break;
}

setMeasuredDimension(viewSize);

2. Subclasses inherited from ViewGroup.

Not only do you need to override onMeasure to measure yourself correctly, you may also need to override a series of measureChild methods to measure sub-views correctly, such as ScrollView. Or simply abandon the measureChild rule implemented by the parent class and re-implement a set of rules for measuring child views, such as Relative Layout. Finally, we need to call setMeasured Dimension to save the measurement results.

Generally speaking, the pseudocode for the measure ment process of the custom ViewGroup is:

//ViewGroup starts measuring its own size
viewGroup.onMeasure();
//ViewGroup calculates MeasureSpec for each child
viewGroup.getChildMeasureSpec();
//Pass the restriction information generated in the previous step to each sub-View, and then the sub-View starts measuring its own size.
child.measure();
//After the sub-View measurement is completed, the ViewGroup can obtain the size of each sub-View measurement.
child.getChildMeasuredSize();
//ViewGroup calculates its size based on its own situation, such as Padding, etc.
viewGroup.calculateSelfSize();
//ViewGroup saves its own size
viewGroupsetMeasuredDimension();

case analysis

Many developers have encountered the need to nest a ListView inside ScrollView, and the number of ListView data bars is uncertain, so you need to set it to the package content, and then you will find that the ListView will display the first line. Then Baidu goes to a solution, inherits ListView and overrides onMeasure.

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

The problem is solved, but many developers don't know why.

The following will be analyzed from the measure ment process of ScrollView and ListView.

1. Why do these problems arise?

Note: Intercept part of the problem-related code, not the complete code.

Look at the onMeasure of ListView:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

When MeasureSpec mode is UNSPECIFIED, only the height of the first item is measured, which corresponds to the problem description, so we suspect that ScrollView passed a UNSPECIFIED limit to ListView.

Let's look at ScrollView's onMeasure code again:

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

The onMeasure of the parent class is called:

Look at FrameLayout's onMeasure:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
    }

Called measureChildWithMargins, but since ScrollView overrides this method, look at the measureChildWithMargins method of ScrollView:

@Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 
                                           int parentHeightMeasureSpec, int heightUsed) {
        final int childHeightMeasureSpec = 
                MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec), 
                        MeasureSpec.UNSPECIFIED);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

Sure enough, it passes a UNSPECIFIED restriction to onMeasure of ListView.

Why, think about it, because Scroll View is a layout that can scroll vertically, so its height to all its sub-views is UNSPECIFIED, which means that there is no limit to how high the sub-views are, because I need to slide vertically, and that's what it meant, so its sub-views are high. There are no restrictions.

2. Why can this solution solve this problem?

Look at the onMeasure of ListView:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

Just let heightMode = MeasureSpec. AT_MOST, it will measure its full height, so the first data, limiting the value of mode, is determined. The second data is the upper limit of size. If we give a 200, then when the ListView data is too much, the maximum height of the ListView is 200, or the content can not be fully displayed. What should we do? So give me a maximum, what's the maximum, Integer.MAX_VALUE?

Let's first look at the code description of MeasureSpec:

        private static final int MODE_SHIFT = 30;
        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;

He uses up to two storage mode s, and other unused sizes. So Integer. MAX_VALUE >> 2 is the maximum size data that can be carried by limiting information. So finally, you need to use these two values to make a restriction message and pass it to the height dimension of ListView.

That is the following code:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

Do it yourself

Now let's write a custom View Group, so that every sub-View in it is arranged vertically and the left boundary of each sub-View is a certain distance from the left boundary of a sub-View. It also supports wrap_content. It probably looks as follows:

The actual operation effect is shown as follows:

The code is as follows:

public class VerticalOffsetLayout extends ViewGroup {

    private static final int OFFSET = 100;

    public VerticalOffsetLayout(Context context) {
        super(context);
    }

    public VerticalOffsetLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

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

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

        int width = 0;
        int height = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            ViewGroup.LayoutParams lp = child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
            child.measure(childWidthSpec, childHeightSpec);
        }

        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    int widthAddOffset = i * OFFSET + child.getMeasuredWidth();
                    width = Math.max(width, widthAddOffset);
                }
                break;
            default:
                break;

        }

        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    height = height + child.getMeasuredHeight();
                }
                break;
            default:
                break;

        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;
        int right = 0;
        int top = 0;
        int bottom = 0;

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            left = i * OFFSET;
            right = left + child.getMeasuredWidth();
            bottom = top + child.getMeasuredHeight();

            child.layout(left, top, right, bottom);

            top += child.getMeasuredHeight();
        }
    }
}

Reference resources

Posted by bad_gui on Sat, 13 Jul 2019 10:56:19 -0700