My knowledge of the Android system, welcomed the Star at https://github.com/daishengda2018/AndroidKnowledgeSystem

OnMeasure and onLayout can be said to be the core of custom View, but many developers fail to understand their meanings and functions, as well as the relationship and difference between onMeasure and XML size. Nor is it possible to tell the essential difference between getMeasureWidth and getWidth. This paper will guide you to deeply understand the definition, process, specific use methods and details of onMeasure and onLayout through theoretical and practical methods.

Custom View: onMeasure, onLayout

The role of layout process

  • Determine the size and position of each View
  • What it does: Support drawing and touching ranges
    • Draw: Know where to draw
    • Touch return: Know where the user clicked

Layout flow

Looked from the overall

  • Measurement process: The measure method of each level of sub-views is recursively called from the root View to measure them.
  • Layout process: recursively call the layout method of each sub-view from the root View, and pass the position and size of the sub-view obtained from the measurement process to the sub-view, which is saved by the sub-view.

Look from the individual

For each View:

  1. Before running, the developer writes down the expected size of the View in the XML file as needed

  2. At runtime, the parent View will be in onMeaure(), based on the developer’s XML requirements for the child View, and its actual available space, the specific size requirements for the child View

  3. The child View calculates its own expectations in its onMeasure based on the expectations specified in the XML and its own characteristics (referring to the View definer’s declaration in onMeasrue)

    If it is a ViewGroup, it will also call the measure () of each child View in the onMeasure.

  4. After the parent View calculates the desired size of the child View, the parent View obtains the actual size and position of the child View

  5. The child View stores the actual size and position passed in by the parent View in its layout() method

    In the case of a ViewGroup, the onLayout() calls the layout() of each word of the View and passes its size to them

Why do we need two processes?

The reason a

A measure might be measured more than once, for example, if there are three child views in a ViewGroup whose width is warp_Content, A’s width is match_parent, B and C’s are warp_content, and the ViewGroup’s width is not fixed. How do you measure it?

Take the LinearLayout as an example: The size of the LinearLayout is also not determined in the first measurement, so it is impossible to determine how big the match_parent of A is. At this time, the LinearLayout will directly measure 0 for A, and then measure the width of B and C. Because the size of B and C is the content of the package, the width of the LinearLayout can be determined after measurement: that is, the width of the longest B.

At this time, the second measurement of A is directly set to the same width as the LinearLayout, so that the effect of match_parent is achieved.

If the process of measure and layout is mixed together, useless layout will be carried out during the two measurements, consuming more resources. Therefore, for the sake of performance, the two are separated.

Reason two

Also, the responsibilities of the two are independent of each other and divided into two processes, which can make the process and code more clear.

expand

The situation in the above example only exists in a LinearLayout, and the measurement mechanism for each layout is different. So how does the LinearLayout work if A, B and C are match_parent?

  • The LinearLayout cannot determine its size, so the child View match_parent will be measured to 0

  • The second round of measurement: There is no size, so the LinearLayout will let all the children measure freely (the parent View does not limit the width). Each measurement will be the same width as the widest.

Note:

  • OnMeasure and Measure (), onDraw and draw

    The onXX method schedules the process, while Measure and Draw do the real work. You can see from the source code that measure calls the onMeasure method.

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           / /........................
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                 / /...........................}}Copy the code
  • Why not just pass the size requirements directly to the child View instead of to the parent View?

    For example, in the example above, the child View of the LinearLayout is set to weight.

  • Layout () is rarely used because its changes are not notified to the parent View, which can cause problems such as layout overlap. A proof is provided below in “Walkthrough – Simply Resize an existing View.”

# # onMeasure method

One question to clarify is: When do we need to implement the onMeasure method ourselves?

A: There are three scenarios for specific development:

  • When inheriting an existing View, simply resize it, such as a custom square ImageView, and take the larger value of the width as the side length.
  • Fully perform custom sizing calculations. For example, to implement a View that draws circles, we need to specify a size when the size is warp_content. For example, “Walkthrough – Completely customize the size of the View” in the following section.
  • Customize Layout. At this time, we need to control the size and position of all the internal sub-views by ourselves, so we need to rewriteonMeasure()onLayout()Methods. For example, “Walkthrough – Custom Layout” in the following section

OnLayout method

The onLayout method is the method in the ViewGroup that controls the position of the child views. The process of placing the child View is very simple, just rewrite the onLayout method, and then get the child View instance, call the child View layout method to implement the layout. In practical development, onMeasure measurement method is generally used together. This will be demonstrated in “Walkthrough – Custom Layout” below.

Comprehensive practice

Simply rewrite the size of the existing View to achieve a square ImageView

  • Let’s first demonstrate the problem of rewriting layout
/** * Created by im_dsD on 2019-08-24 */
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {

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

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

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

   @Override
    public void layout(int l, int t, int r, int b) {
        // Use the maximum width to set the edge length
        int width = r - l;
        int height = b - t;
        int size = Math.max(width, height);
        super.layout(l, t, l + size, t + size); }}Copy the code

The code is simple: get the maximum width and height to set the side length of the square View. Look again at the layout file Settings

<?xml version="1.0" encoding="utf-8"? >
<LinearLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">


    <com.example.dsd.demo.ui.custom.measure.SquareImageView
        android:background="@color/colorAccent"
        android:layout_width="200dp"
        android:layout_height="300dp"/>

    <View
        android:background="@android:color/holo_blue_bright"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
</LinearLayout>
Copy the code

The description of the layout file would look something like this if it were a normal View

The desired state is this: the SquareImageView is 300dp wide and high.

Although we use the LinearLayout, we changed the size of the SquareImageView using the Layout () method. The LinearLayout is not aware of this change, so the layout overlapped. As you can see, you should not use the layout() method in general.

  • throughonMeasureMethod to change the size.
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Super. onMeasure has completed the View measurement
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Get the maximum value by comparing the measured results
        int height = getMeasuredHeight();
        int width = getMeasuredWidth();
        int size = Math.max(width, height);
        // Set the result back
        setMeasuredDimension(size, size);
    }

Copy the code

conclusion

In short, changing the size of an existing View can be divided into the following steps

  1. rewriteOnMeasure ()
  2. withgetMeasureWidthgetMeasureHeight()Obtain measurement dimensions
  3. Calculate the final size
  4. withsetMeasuredDimension(width, height)Save the results

Fully customize the size of the View

Here’s an example using a CircleView that draws a circle. The expectation for this View is that the size of the View is determined by an internal circle.

Let’s draw a circle first

/** * Created by im_dsD on 2019-08-15 */
public class CircleView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    /** ** for convenience and simplicity, fixed size */
    private static final float PADDING = DisplayUtils.dp2px(20);
    private static final float RADIUS = DisplayUtils.dp2px(80);

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

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

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas); mPaint.setColor(Color.RED); canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, mPaint); }}Copy the code
    <com.example.dsd.demo.ui.custom.layout.CircleView
        android:background="@android:color/background_dark"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
Copy the code

What happens if you set the size to WRAP_content for the package layout?

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // There is no need to make the view measure itself
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // Calculate the desired size
        int size = (int) ((PADDING + RADIUS) * 2);
        // Get the available size from the parent View
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        // Start counting
        int result = 0;
        switch (widthMode) {
            / / no more than
            case MeasureSpec.AT_MOST:
                // In AT_MOST mode, take the minimum value of both
                if (widthSize < size) {
                    result = widthSize;
                } else {
                    result = size;
                }
                break;
            / / accurate
            case MeasureSpec.EXACTLY:
                // Use as much of the parent View as possible
                result = widthSize;
                break;
            // infinite, no size specified
            case MeasureSpec.UNSPECIFIED:
                // Use the calculated size
                result = size;
                break;
            default:
                result = 0;
                break;
        }
        // Set the size
        setMeasuredDimension(result, result);
    }
Copy the code

OnMeasure (int,int) is a template for onMeasure(int,int).

 // There is no need to make the view measure itself
 // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Copy the code

This template code is already packaged in the Android SDK: ResolveSize (int size, int measureSpec) and resolveSizeAndState(int size, int measureSpec, int childMeasuredState), Two lines of code straight away.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // There is no need to make the view measure itself
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // Calculate the desired size
        int size = (int) ((PADDING + RADIUS) * 2);
        // Specify the desired size
        int width = resolveSize(size, widthMeasureSpec);
        int height = resolveSize(size, heightMeasureSpec);
        // Set the size
        setMeasuredDimension(width, height);
    }

Copy the code

It’s perfectly acceptable to do this, but it’s highly recommended that you write it out yourself a few times to get the meaning, as the interview will ask for details.

ResolveSizeAndState (int, int, int) resolveSizeAndState(int, int, int) ResolveSizeAndState (int, int, int) resolveSizeAndState(int, int, int) resolveSizeAndState(int, int, int) resolveSizeAndState(int, int, int) Or sate is not specified (resolveSize(int, in) is better than resolveSize(int, in)), so it will not work.

conclusion

Fully customize the size of the View is divided into the following steps:

  1. rewriteOnMeasure ()
  2. Calculate your desired size
  3. withresolveSize()orResolveSizeAndState ()Correction results
  4. withsetMeasuredDimension(width, height)Save the result

A custom Layout

The source address

Take TagLayout as an example to implement a custom Layout step by step. The specific desired effect is shown in the figure below:

rewriteonLayout()

OnLayout () must be implemented when inheriting from a ViewGroup, which means that the placement rules for child views are left to the developer.

/** * Custom Layout Demo ** Created by im_dsD on 2019-08-11 */
public class TagLayout extends ViewGroup {

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

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // All child views are the same size as the TagLayoutchild.layout(l, t, r, b); }}}Copy the code

Let’s see if it works as expected

<?xml version="1.0" encoding="utf-8"? >
<com.example.dsd.demo.ui.custom.layout.TagLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:padding="5dp"
        android:background="#ffee00"
        android:textSize="16sp"
        android:textStyle="bold"
        android:text="Music" />

</com.example.dsd.demo.ui.custom.layout.TagLayout>
Copy the code

It’s exactly what you’d expect. What if you want your TextView to be a quarter the size of your TagLayout?

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(a);i{+ +)View child = getChildAt(i);/ / childViewIs shown asTagLayout1/4
            child.layout(l.t.r / 4.b / 4); }}Copy the code

Effect achieved!! It is obvious that onLayout can be very flexible in controlling the position of the View

What if I tried to have two views arranged diagonally?

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(a);i{+ +)View child = getChildAt(i);
            if (i= =0) {child.layout(0.0, (r - l) / 2, (b - t)  / 2);
            } else {
                child.layout((r - l) / 2, (b - t)  / 2, (r - l), (b - t)); }}}Copy the code

OnLayout method is still very simple, but in the real layout how to obtain the position of the View is difficult! This is where onMeasure comes in!

To calculate

Before writing the specific code, let’s build the general framework. The main idea is to calculate the size and position of the child View in onMeasure() method, including the specific size of the TagLayout, and then place the child View in onLayout() method.

There are three difficulties involved in the calculation process, please refer to the notes for details

private List<Rect> mChildRectList = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // There is no need to waste resources by letting the View calculate itself.
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // Difficulty 1: Calculate the size for each child View
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            // Difficulty 2: Calculate the position of each sub-view and save it.
            Rect rect = newRect(? ,? ,? ,?) ; mChildRectList.add(rect); }// Difficulty 3: Calculate the size of the TagLayout according to the size of all the child views
        intmeasureWidth = ? ;intmeasureHeight = ? ; setMeasuredDimension(measureWidth, measureHeight); }@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mChildRectList.size() == 0) {
            return;
        }
        for (int i = 0; i < getChildCount(); i++) {
            if (mChildRectList.size() <= i) {
                return;
            }
            View child = getChildAt(i);
            // Set the child View with the saved locationRect rect = mChildRectList.get(i); child.layout(rect.left, rect.top, rect.right, rect.bottom); }}Copy the code
Difficulty 1: how to calculate the size of the child View.

It mainly involves two points: the developer’s size setting for the child View and the specific available space of the parent View. It’s easy to get the developer’s Settings for the size of the child View:

// Get the developer's Settings for the size of the child View
LayoutParams layoutParams = child.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
Copy the code

To obtain the available space of the parent View (TagLayout), combine two things:

  1. TagLayout’s parent View limits its size
  2. Free space for TagLayout. So let’s use width as an example and just do a little bit of pseudo-code to see how do we calculate the size of a child View
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// TagLayout is a space that has already been used
int widthUseSize = 0;
for (int i = 0; i < getChildCount(); i++) {
	View child = getChildAt(i);
  // Get the developer's Settings for the size of the child View
  LayoutParams layoutParams = child.getLayoutParams();
  int childWidthMode;
  int childWidthSize;
  // Get the available space of the parent View
  switch (layoutParams.width) {
  // If the child View is set to match_parent by the developer
  	case LayoutParams.MATCH_PARENT:
    	switch (widthMode) {
      	case MeasureSpec.EXACTLY:
        // When TagLayout is EXACTLY mode, the area that the child View can fill is the available space of the TagLayout
        case MeasureSpec.AT_MOST:
        // TagLayout has a maximum available space in AT_MOST mode
        // EXACTLY the same pattern
        childWidthMode = MeasureSpec.EXACTLY;
        childWidthSize = widthSize - widthUseSize;
        break;
        case MeasureSpec.UNSPECIFIED:
        // When the TagLayout is UNSPECIFIED, the available space is UNSPECIFIED. Infinite space still want to
        // match_parent is still a paradox, so we also specify the mode of the child View as UNSPECIFIED
        childWidthMode = MeasureSpec.UNSPECIFIED;
        // If size is not used, write 0
        childWidthSize = 0;
        break;
        }
      case LayoutParams.WRAP_CONTENT:
       break;
      default:
      // The specified size
      break;
}
/ / get measureSpec
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
Copy the code

UNSPECIFIED mode? As an example, the width or height mode of the ScrollView that is swiped horizontally or vertically is UNSPECIFIED

The pseudocode only simulates the case where the developer sets the size of the child View to match_parent, but the rest of the case can be analyzed for your own interest. The author will not do too much analysis! The Android SDK already provides the measureChildWithMargins(int, int, int, int) API for measuring sub-views in one sentence.

Difficulty 2: Calculate the position of each sub-view and save it.
Difficulty 3: Calculate the size of TagLayout according to the size of all child Views

MeasureChildWithMargins makes measuring sub Views easy. Solve difficulties 2 and 3 in one breath.

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int lineHeightUsed = 0;
        int lineWidthUsed = 0;
        int widthUsed = 0;
        int heightUsed = 0;
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // Measure the size of the child View. TagLayout's child views can be newline, so set the widthUsed parameter to 0
            // Keep the size of the child View from being squeezed.
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            if(widthMode ! = MeasureSpec.UNSPECIFIED && lineWidthUsed + child.getMeasuredWidth() > widthSize) {// Need a line break
                lineWidthUsed = 0;
                heightUsed += lineHeightUsed;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }
            Rect childBound;
            if (mChildRectList.size() >= i) {
                // Create one if it does not exist
                childBound = new Rect();
                mChildRectList.add(childBound);
            } else {
                childBound = mChildRectList.get(i);
            }
            // Store child location information
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
                            heightUsed + child.getMeasuredHeight());
            // Update location information
            lineWidthUsed += child.getMeasuredWidth();
            // Get the largest size in a row
            lineHeightUsed = Math.max(lineHeightUsed, child.getMeasuredHeight());
            widthUsed = Math.max(lineWidthUsed, widthUsed);
        }

        // The width and height used are the same as the width and height of the TagLayout
        heightUsed += lineHeightUsed;
        setMeasuredDimension(widthUsed, heightUsed);
    }
Copy the code

I’ve finally written the code. Let’s run it.

It collapsed! You can locate yes based on logs

  // Measure the child View
  measureChildWithMargins(child, widthMeasureSpec, widthUsed, 
                                                          heightMeasureSpec, heightUsed);
Copy the code

The measureChildWithMargins method has a type conversion that crashes

protected void measureChildWithMargins(int.int ,int.int) {
	finalMarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); ........................... }Copy the code

The solution is to override generateLayoutParams(AttributeSet) in TagLayout and return a MarginLayoutParams.

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
Copy the code

Run again to reach the final goal!

conclusion

The main steps of custom Layout are as follows:

  1. rewriteonMeasure()
    • To iterate over each child View, usemeasureChildWidthMargins()Measure the View
      • MarginLayoutParams and generateLayoutParams ()
      • Some subviews may require multiple measurements
      • After the measurement is completed, the actual size and position of the sub-view are obtained and temporarily saved
    • After measuring the position and size of all child views, calculate your own size and usesetMeasuredDimension(width, height)save
  2. rewriteonLayout()
    • Iterate through each child View, calling their Layout () method to pass the position and size to them.

Difference between getMeasureWidth and getWidth

GetMeasureXX indicates the measureMeasurexx value measured after the onMeasure method ends, while getXX indicates the layout right-left and bottom-top values. The first difference is the assignment phase. See that getXXX is always 0 until Layout (), while getMeasureXX may not be the final value (onMeasure may be called multiple times), but the final value will always be the same. Using that depends on the context.

Summary: getMeasureXX takes a temporary value, while getXX takes a final value, usually using getXXX in the Draw phase, touch feedback phase, and forced to use getMeasureXX in the onMeasure phase.

All source code addresses for this article

My knowledge of the Android system, welcomed the Star at https://github.com/daishengda2018/AndroidKnowledgeSystem