Layout process of Android custom View

preface

In the previous article: Measure process of Android custom View , we analyzed the Measure process. This time, we will lift the mysterious veil of the Layout process connecting the preceding and the following,
Through this article, you will learn:

1. Simple analogy about Layout
2. A simple Demo
3. View Layout procedure
4. ViewGroup Layout procedure
5. Analysis of common methods of View/ViewGroup
6. Why is Layout a connecting link

Simple analogy about Layout

In the metaphor of the last article, we said:

Lao Wang allocated specific fertile land areas to his three sons, the king (the king's son: Xiaowang), the second king and the third king, and the three sons (Xiaowang) also confirmed their own needed fertile land areas. This is the Measure process
Now that we know the size of the fertile land allocated to our children and grandchildren, which one will they be allocated, side, middle or other positions? To whom first?
Lao Wang wants to come according to the chronological order of arriving at the house (corresponding to the addView order). The king is his eldest son and is assigned to him first, so he sets aside 3 mu of land to the king from the far left. Now it's the second king's turn. Since the king has allocated 3 mu on the left, the 5 mu of land for the second king can only be divided from the right of the king, and the rest will be distributed to the third king. This is the ViewGroup onLayout process.
The king got the boundary of the fertile field designated by Lao Wang and recorded the coordinates of the boundary (left, upper, right and lower). This is the View Layout process
Then the king told his son Xiao Wang: your father is a little selfish. I can't give you all the 5 mu of land inherited from my grandfather. I'll leave some for the elderly. This is the setup: padding process
If the two kings thought at the beginning of the measurement: I don't want to be too close to the fields of the king and the three kings, then Lao Wang will leave a gap between the lands of the king, the three kings and the two kings. This is the setup: margin procedure

A simple Demo

Customize ViewGroup

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int usedWidth = 0;
        int maxHeight = 0;
        int childState = 0;

        //Survey sub layout
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
            measureChildWithMargins(childView, widthMeasureSpec, usedWidth, heightMeasureSpec, 0);
            usedWidth += layoutParams.leftMargin + layoutParams.rightMargin + childView.getMeasuredWidth();
            maxHeight = Math.max(maxHeight, layoutParams.topMargin + layoutParams.bottomMargin + childView.getMeasuredHeight());
            childState = combineMeasuredStates(childState, childView.getMeasuredState());
        }

        //Count the sub layout level and record the size value
        usedWidth += getPaddingLeft() + getPaddingRight();
        maxHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(resolveSizeAndState(usedWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //Location information passed in by parent layout
        int parentLeft = getPaddingLeft();
        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;

        //Traversal sub layout
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
            left = parentLeft + layoutParams.leftMargin;
            right = left + childView.getMeasuredWidth();
            top = getPaddingTop() + layoutParams.topMargin;
            bottom = top + childView.getMeasuredHeight();
            //Sub layout placement
            childView.layout(left, top, right, bottom);
            //Horizontal placement
            parentLeft += right;
        }
    }

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

    //Custom LayoutParams
    static class MyLayoutParam extends MarginLayoutParams {
        public MyLayoutParam(Context c, AttributeSet attrs) {
            super(c, attrs);
        }
    }

The ViewGroup overrides the onMeasure(xx) and onLayout(xx) methods:

  • onMeasure(xx) measures the sub layout size and determines its own size according to the sub layout calculation results
  • onLayout(xx) placement of sub layout

At the same time, when the layout execution ends, clear PFLAG_FORCE_LAYOUT tag, which affects whether the Measure process needs to execute onMeasure.

Custom View

public class MyView extends View {

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.GREEN);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int defaultSize = 100;
        setMeasuredDimension(resolveSize(defaultSize, widthMeasureSpec), resolveSize(defaultSize, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }
}

The View overrides the onMeasure(xx) and onLayout(xx) methods:

  • onMeasure(xx) measure its own size and record the size value
  • onLayout(xx) did nothing

Add a sub layout for MyViewGroup

<?xml version="1.0" encoding="utf-8"?>
<com.fish.myapplication.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_gravity="center_vertical"
    android:background="#000000"
    android:paddingLeft="10dp"
    tools:context=".MainActivity">
    <com.fish.myapplication.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </com.fish.myapplication.MyView>

    <Button
        android:layout_marginLeft="10dp"
        android:text="hello Button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </Button>
</com.fish.myapplication.MyViewGroup>

MyView and Button controls are added to MyViewGroup. The final running effect is as follows:

It can be seen that the sub Layout in MyViewGroup is placed horizontally. We focus on the Layout process. In fact, in MyViewGroup, we only override the onLayout(xx) method, and MyView also overrides the onLayout(xx) method.
Next, analyze the View Layout process.

View Layout procedure

View.layout(xx)

Similar to the Measure process, the bridge between ViewGroup onLayout(xx) and View onLayout(xx) is View layout(xx).

#View.java
    public void layout(int l, int t, int r, int b) {
        //PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT may be set during measure
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //Record the current coordinate value
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //If the coordinate value of the new (given by the parent layout) is inconsistent with the current coordinate value, it is considered to have changed
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //Coordinate changes or re layout is required
       //PFLAG_LAYOUT_REQUIRED is the flag set after the end of Measure
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //Call the onLayout method to pass in the coordinates of the parent layout
            onLayout(changed, l, t, r, b);
            ...

            //Empty request layout tag
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //The onLayoutChange callback to listen to is set through addOnLayoutChangeListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        final boolean wasLayoutValid = isLayoutValid();
        //Clear the forced layout flag, which determines whether onMeasure is required during measure;
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        ...
    }

    public static boolean isLayoutModeOptical(Object o) {
        //Set shadow, light and other attributes
        //Only ViewGroup has this property
        //Set android:layoutMode="opticalBounds" or android:layoutMode="clipBounds"
        //Returns true. The above properties are not set by default
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        //If shadow and light attributes are set
        //The reserved size is obtained
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        //Change the coordinate value again and call setFrame(xx)
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

As you can see, the setFrame(xx) method is called in the end.

#View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        //If the current coordinate value is inconsistent with the new coordinate value, reset it
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            //Record pflag_ Draw flag bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            //Record new and old width and height
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //Are the new and old width and height the same
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //It's different. Go to inValidate and finally execute the Draw process
            invalidate(sizeChanged);

            //Record new coordinate values
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //Set coordinate values to RenderNode
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            //The tag has already been laid out
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            
            if (sizeChanged) {
                //Call sizeChange. In this method, we can get the wide and high values of View
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            ...
        }
        return changed;
    }

For the Measure process, the dimension value is recorded in onMeasure(xx), while for the Layout process, the coordinate value is recorded in layout(xx). Specifically, in setFrame(xx), this method focuses on two aspects:

1. Record the new coordinate values into the member variables mLeft, mTop, mRight and mbobottom
2. Record the new coordinate values in RenderNode. When the Draw process is called, the Canvas drawing starting point is the position in RenderNode

View.onLayout(xx)

#View.java
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

View.onLayout(xx) is an empty implementation
From the layout(xx) and onLayout(xx) declarations, we can see that both methods can be overridden. Next, let's see whether ViewGroup has overridden them.

ViewGroup Layout procedure

ViewGroup.layout(xx)

#ViewGroup.java
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            //There is no delay, or the animation is not changing coordinates
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            //The parent method is actually View.layout(xx)
            super.layout(l, t, r, b);
        } else {
            //If it is delayed, set the flag bit. After the animation is completed, restart the layout process according to the flag bit requestLayout
            mLayoutCalledWhileSuppressed = true;
        }
    }

Although ViewGroup.layout(xx) rewrites layout(xx), it only makes a simple judgment, and finally calls View.layout(xx).

ViewGroup.onLayout(xx)

#ViewGroup.java
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

This will change onLayout into an abstract method after rewriting, that is, classes inherited from ViewGroup must override onLayout(xx) method.
We take FrameLayout as an example to analyze what onLayout(xx) does.

FrameLayout.onLayout(xx)

#FrameLayout.java
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        //Number of sub layouts
        final int count = getChildCount();
        //Foreground padding means that the sub layout should not occupy the position when it is placed
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //Traversal sub layout
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //layout is not required in GONE state
            if (child.getVisibility() != GONE) {
                //Get LayoutParams
                final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
                //Obtain the measurement value previously determined in the Measure process
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                
                int childLeft;
                int childTop;
                //Where is the center of gravity
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                //Layout direction, left to right or right to left, left to right by default
                final int layoutDirection = getLayoutDirection();
                //Horizontally reversed Gravity
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //Gravity in vertical direction
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        //If the sub layout is horizontally centered, its horizontal starting point
                        //Deduct the remaining positions of the parent layout padding
                        //Combine the sub layout width so that the sub layout is centered in the remaining positions
                        //Then take the sub layout margin into account
                        //It can be seen from here that if there is a center and a margin in xml, consider the center first and then the margin
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        //To the right, change the horizontal start coordinate value
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                        //The default is left to right
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                //The vertical direction is similar to the horizontal direction
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                
                //The coordinate position of the child is determined
                //Pass to child
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

When FrameLayout.onLayout(xx) is a sub Layout layout, the starting coordinates are based on FrameLayout, and the position of the previous sub Layout is not recorded. Therefore, the placement positions of sub layouts may overlap, which is also the origin of the Layout characteristics of FrameLayout. In our previous Demo, the placement position of the previous sub Layout is recorded in the horizontal direction, and the next sub Layout can only be placed after it, so the function of horizontal placement is formed.
By analogy, we often say that the position of a child layout in the parent layout is determined by ViewGroup.onLayout(xx).

Analysis of common methods of View/ViewGroup

Above, we analyzed View.layout(xx), View.onLayout(xx), ViewGroup.layout(xx) and ViewGroup.onLayout(xx). What is the relationship between these four?
View.layout(xx)

Record the placement position coordinates in the member variable and set the value for RenderNode

View.onLayout(xx)

Empty implementation

ViewGroup.layout(xx)

Call View.layout(xx)**

ViewGroup.onLayout(xx)

Abstract methods, subclasses must be overridden. When overriding subclasses, you need to calculate the placement position for each sub layout and pass it to the sub layout

Which methods need to be overridden by the View/ViewGroup subclass:

Inherited from ViewGroup, onLayout(xx) must be overridden to calculate the position coordinates for the sub layout
Inherited from View, there is no need to override layout(xx) and onLayout(xx), because it has no sub layout to place

It is shown in Figure:

Why is Layout a connecting link

Through the above description, we find that the methods defined in the Measure process and the Layout process are similar:

measure(xx)<----->layout(xx)
onMeasure(xx)<----->onLayout(xx)

Their routines are similar: measure(xx) and layout(xx) generally do not need to be rewritten. onMeasure(xx) is called in measure(xx), and layout(xx) sets the coordinate value for the caller.
If it is ViewGroup: onMeasure(xx), traverse the sub layouts and measure each sub layout. Finally, summarize the results and set the size measured by yourself; Traverse the sub layout in onLayout(xx) and set the coordinates of each sub layout.
If View: onMeasure(xx), measure itself and store the measured size; onLayout(xx) does not need to do anything.

Carry on

Although the Measure process is more complex than the Layout process, after careful analysis, it will be found that its essence is to set two member variables:

Set mMeasuredWidth and mMeasuredHeight

Although the Layout process is relatively simple, its essence is to set coordinate values

1. Set mLeft, mRight, mTop and mbotton to determine a rectangular area
2. Mrendernode.setlefttoprightbottom (mleft, MTop, mright, mbotom) sets coordinates for RnederNode

Associate the variables set by Measure with the variables set by Layout:

mRight and mbobottom are calculated based on mLeft and mRight combined with mMeasuredWidth and mMeasuredHeight

In addition, the Measure process sets pflag_ LAYOUT_ The required flag tells you that onLayout is needed, and the layout process clears PFLAG_FORCE_LAYOUT to tell the Measure process that onMeasure does not need to be executed.
This is the role of Layout

Qixia

We know that the drawing of View depends on Canvas, which has the limitation of action area. For example, we use:

canvas.drawColor(Color.GREEN);

What is the starting point of Cavas drawing?
For hardware rendering acceleration, it is through the RenderNode coordinates set in the Layout process.
For software drawing:

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        canvas.translate(mLeft - sx, mTop - sy);
    }

Hardware rendering acceleration / software rendering will be analyzed in subsequent articles.

This is the inspiration of Layout
The above is the internal relationship among Measure, Layout and Draw.
Of course, the "inheritance" of Layout also needs to consider the influence of margin, gravity and other parameters. See the Demo at the beginning for specific usage.

Classical problem

getMeasuredWidth()/getMeasuredHeight is different from getWidth/getHeight
Let's take obtaining width as an example to see its methods:

#View.java
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getWidth() {
        return mRight - mLeft;
    }

getMeasuredWidth(): gets the measured width, which belongs to "temporary value"
getWidth(): get the true width of the View
Before the Layout procedure, getWidth() defaults to 0
When can I get the true width and height

1. Override the View.onSizeChanged(xx) method to get
2. Register View.addOnLayoutChangeListener(xx) and get it in onLayoutChange(xx)
3. Override the View.onLayout(xx) method to get

The next chapter will analyze the Draw() process, and we will analyze the truth that "everything is drawn"

Draw process of Android custom View (Part 1)

This article is based on Android 10.0

Tags: Android

Posted on Thu, 07 Oct 2021 01:58:56 -0400 by d.shankar