In Android development, there will always be requirements of one kind or another. Although the authorities have provided us with a rich implementation of viewgroups and views, there are always times when we can’t meet the needs. What should we do at this point? The first thing you can do is Google to see if there is a wheel. If there are wheels, then congratulations, grab to change it. If you don’t have wheels, what can you do? You have to build your own wheels. In fact, the use of wheels is more to pursue stability and save time, we still need to have a certain understanding of the principle of wheels.

Streaming layout should be used in many scenarios in Android development, such as TAB display, search history display, and so on. Android currently doesn’t have a native ViewGroup for this layout, and it’s easy to find wheels, but today I’d like to implement a custom ViewGroup.

What is a ViewGroup

First we need to figure out what a ViewGroup is and what it does.

A ViewGroup inherits from a View and implements the ViewManager and ViewParent interfaces. According to the official definition, a ViewGroup is a special View that can hold other views, and it implements a series of methods for adding and removing views. The ViewGroup also defines LayoutParams, which affect the position and size of the View in the ViewGroup.

ViewGroup is also an abstract class, so we need to rewrite the onLayout method, which is not enough. ViewGroup itself is only to achieve the ability to accommodate the View, to achieve a ViewGroup we need to complete the measurement of itself, the measurement of the child, the layout of the child and a series of operations.

onMeasure

This is a very important way to implement a custom View, whether we’re doing a custom View or a custom ViewGroup we need to implement it. This method comes from the View, and the ViewGroup itself doesn’t handle this method. This method passes two parameters, widthMeasureSpec and heightMeasureSpec. These two values are actually mixed information, they contain the specific width and height values and width and height pattern. MeasureSpec needs to be mentioned here.

MeasureSpec

MeasureSpec is the inner class of a View, which is a compressed body of the layout information that the parent container passes to the child. The values passed above are actually generated by MeasureSpec’s makeMeasureSpec method:

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

        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

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

MeasureSpec = “MeasureSpec”; MeasureSpec = “MeasureSpec”; MeasureSpec = “MeasureSpec”; What information is known, and how is it confirmed?

There is a getChildMeasureSpec method in the ViewGroup, and the implementation of this method basically answers our question

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;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Copy the code

The code is still a bit long, but the logic isn’t complicated. The spec parameter is ViewGroup information, the padding is ViewGroup leftPadding+rightPadding+childLeftMargin+childRightMargin+usedWidth, ChildDimension is the width and height information specified in the child’s LayoutParams.

Child’s specific MeasureSpec is influenced by the parent container and also depends on its own layout information, as follows:

  • If child’s LayoutParams specifies a fixed width, such as 100dp, then the final size passed by the onMeasure is the specified width and mode is MeasureSpec.EXACTLY
  • If the width and height of child is MATCH_PARENT, the size passed is usually the width and height of the parent container, and the mode is the same as the mode of the parent container.
  • UNSPECIFIED. If the parent’s mode is MeasureSpec.UNSPECIFIED, the mode passed is MeasureSpec. Otherwise, it is MeasureSpec.AT_MOST.

AT_MOST reveals the UNSPECIFIED maximum width. The actual width is UNSPECIFIED. The specMode also reveals the width of the parent container, which you can set to any height.

What should an onMeasure method do

MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec

If it is a custom View, we need to check its width and height according to MeasureSpec passed by the parent container. As MeasureMode is EXACTLY, the width and height of the View is passed as Size, while AT_MOST and UNSPECIFIED are UNSPECIFIED. Once we have calculated a desired width and height, we need to call the setMeasuredDimension method to save the information.

If it is a custom ViewGroup, then we need to do more things, first of all, we also need to confirm the width and height information of the ViewGroup itself, if it is EXACTLY easy to handle, directly set the corresponding size can be. If you want to support WRAP_CONTENT, this might be a bit of a hassle. First we need to think a little bit about how the ViewGroup is laid out for the Child. This is important because different layouts, different placement of children, can affect the actual space taken up.

LinearLayout, for example, supports horizontal and vertical alignment. The measurement logic they need to perform is different. If it is vertical, we need to traverse the child, measure the child, add their height and margin, and finally add their own height, so that the cumulative value is the height of the WRAP_CONTENT. If it is horizontal, children need to be traversed and accumulated, and their widths, margins, etc. The principle is similar.

To summarize, the onMeasure method requires the ViewGroup to measure the child in combination with the parent container’s MeasureSpec, and determine its width and height according to the arrangement of the child.

onLayout

The onLayout method passes five parameters that changed to indicate whether its position or size has changed. The remaining parameters, left,top,right, and bottom, determine its position in the parent container. This is a relative coordinate, the starting point is not the top left corner of the screen.

So what should we do in this method? If we’re customizing a View, we don’t have to worry about this method. Since the View itself has no capacity to hold a Child, if it is a ViewGroup, then we need to perform layout operations for the child. We need to iterate over the children and execute their Layout methods. By calling the Layout method, we can pass left, top, right, and bottom to determine the position of the child in the ViewGroup. Again, this is a relative coordinate that depends on the parent container.

In fact, the onLayout method is called after its own Layout method has been called. Android’s overall layout system is called from top to bottom, transferring layout information and finally confirming the position of each View on the screen.

onDraw

In general, customizing viewgroups does not require overriding this method. This method is used to do some drawing, but if it’s a custom View, then we need to rewrite this method and implement some drawing logic.

The Padding and Margin

Let’s talk about these two concepts and understand how they work and how they work.

  • The Padding isRelative to oneselfIt affects its own rendering and the layout of the child, yesViewIts own properties. If we want this property to work, we need to do something based on the value of this property when drawing and laying outThe offsetWhen measuring, we also need to consider its value and add it to the final measurement result.
  • Margin isRelative to the parent containerIn terms of, it affectsViewinViewGroupIn the layout, it is usually made up ofLayoutParamsIs defined by. When we have this property, we need to take it into account when we measure it, and when we lay it out, we need to offset it according to the property of the response.

Implement a streaming layout

Once you’ve figured it out, it’s a lot easier to write code. The general effect of a streaming layout is that the added views are arranged in a row or column, and if one row or column does not fit, they are moved to the next row. Let’s simply implement a streaming layout to deepen our understanding.

First we need to define a class that inherits from ViewGroup:

public class FlowLayout extends ViewGroup {
    public FlowLayout(Context context) {
        this(context, null);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      // Todo implements measurement logic
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
			// Todo implements child's layout logic}}Copy the code

Since we need to support the Margin property, we also need such a LayoutParams. As MarginLayoutParams is defined in ViewGroup, we create an inner class that inherits this implementation:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source); }}Copy the code

LayoutParams also allows you to define some personalized layout parameters, which is easy to handle here. At the same time, we should pay attention to the following methods:

/**
 * 直接调用 {@link#addView(View View)} is used to generate the default LayoutParams * *@return* /
@Override
protected LayoutParams generateDefaultLayoutParams(a) {
    return new LayoutParams(-2, -2);
}

/ * * * {@link#addView(View Child, ViewGroup.LayoutParams params)@param p
 * @return* /
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
}

/** * if {@link#checkLayoutParams(viewGroup.layoutParams p)} returns false, this method will be called to generate LayoutParams * *@param p
 * @return* /
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    if (p == null) {
        return generateDefaultLayoutParams();
    }
    return new LayoutParams(p);
}

/** * If child is in XML, this method is called to generate the layout parameter *@param attrs
* @return* /
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
Copy the code

I have written comments, mainly used for the user addView when the default layout information generation and detection, if not handled well, may cause a crash what.

Here’s how to measure it:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    Log.d(TAG, "onMeasure");
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthMode == MeasureSpec.EXACTLY) {
        // The horizontal width is fixed
        int lineMaxHeight = 0;// The highest row height of the current row
        int currentLeft = getPaddingLeft();// The starting point of the current child is left
        int currentTop = getPaddingTop();// The starting point of the current child top
      	// Remove paddingLeft and paddingRight to get the available width
        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {// Gone child is not processed
                continue;
            }
            / / the child measurement
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            int decoratedWidth = getDecoratedWidth(child);
            int decoratedHeight = getDecoratedHeight(child);
            if (currentLeft + decoratedWidth > availableWidth) {
                // The width exceeds the newline
                currentLeft = decoratedWidth + getPaddingLeft();
                currentTop += lineMaxHeight;// Height plus the previous maximum height
                lineMaxHeight = decoratedHeight;
            } else {
                // If no line break is required, only the current maximum height is recorded.
                currentLeft += decoratedWidth;
                lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
            }
            if (i == getChildCount() - 1) {
                // The last element we need to add heightcurrentTop += lineMaxHeight; }}// Save width and height information
        setMeasuredDimension(widthSize, currentTop + getPaddingBottom());
    } else if (heightMode == MeasureSpec.EXACTLY) {
        // Todo implements vertical fixed streaming layout

    } else {
        // Todo implements streaming layout with fixed width and height}}Copy the code

The measurement logic is not complicated. Firstly, the width and height mode of ViewGroup is judged. Here, the processing logic of streaming layout with fixed width is realized. We need to iterate over all the children and call the measurement method to determine their width and height. Also note that the child needs to be skipped if it is not visible. Since the width is fixed, we need to figure out our own height. GetDecoratedWidth gets the sum of the child’s own width and its left and right margins. Arrange the children in this way during the traversal. If the line does not fit, the newline logic is performed and the height is accumulated. Finally, the height is obtained and saved.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    Log.d(TAG, "onLayout l :" + l + " t :" + t + " r :" + r + " b :" + b);
    int lineMaxHeight = 0;
    int currentLeft = getPaddingLeft();// The starting point of the current child is left
    int currentTop = getPaddingTop();// The starting point of the current child top
    int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child.getVisibility() == GONE) {// Gone child is not processed
            continue;
        }
        int decoratedWidth = getDecoratedWidth(child);
        int decoratedHeight = getDecoratedHeight(child);
        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
        int childLeft, childTop;
        if (currentLeft + decoratedWidth > availableWidth) {
            // The width exceeds the newline
            currentLeft = decoratedWidth + getPaddingLeft();
            currentTop += lineMaxHeight;// Height plus the previous maximum height
            lineMaxHeight = decoratedHeight;
            childLeft = getPaddingLeft() + +layoutParams.leftMargin;
            childTop = currentTop + layoutParams.topMargin;
        } else {
            // If no line break is required, only the current maximum height is recorded.childLeft = currentLeft + layoutParams.leftMargin; childTop = currentTop + layoutParams.topMargin; currentLeft += decoratedWidth; lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight); } child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); }}Copy the code

In the onLayout method I just implemented the logic under the fixed width. The logic is the same as when measuring. When measuring, we have confirmed the width and height of each child. In this case, we only need to call layout method to perform layout logic for each child.

Finally, run the effect, because it is demo so the style is arbitrary, don’t worry about these details (#^.^#)

This is the general process of customizing ViewGroup. If there is any confusion, you can leave a message. I will answer it carefully.