Custom View Practice Paper (2) - Custom ViewGroup

Keywords: Android xml encoding Java

1. Introduction

Previous Chapter: Custom View Practice Paper (1) - Customize Single View
We implemented a custom single View, so let's look at the custom ViewGroup in this chapter.

2. Customize ViewGroup

Custom ViewGroups can also be divided into two categories, one inheriting the existing ViewGroups in the system (for example, LinearLayout) and the other directly inheriting the ViewGroups. Let's look at them separately.

2.1 Inheritance system already has ViewGroup

This way you can extend the functionality of the existing ViewGroup s on your system, most often by combining Views.Form a new layout by combining different views to achieve the effect of multiple reuses.For example, the navigation bar at the bottom of WeChat is composed of an ImageView above and a TextView below.As follows:

Here's a simple implementation of this layout, which combines an ImageView with a TextView, and then customizes the properties to modify the icon and title:

2.1.1 Defines the xml layout of the combined View

First, let's define the layout of this View, which is an ImageView above and a TextView below, wrapped outside with LinearLayout.Very simple, named navigation_button.xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

2.1.2 Write Java code to combine View s

Next, we'll write Java code that combines View s, and since the layout above uses LinearLayout to wrap, our classes here also inherit from LinearLayout.

public class NavigationButton extends LinearLayout {//Inherited from LinearLayout

    private ImageView mIconIv;
    private TextView mTitleTv;

    public NavigationButton(Context context) {
        super(context);
        init(context);
    }

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

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

    //Initialization
    public void init(Context context) {
        //Load Layout
        LayoutInflater.from(context).inflate(R.layout.navigation_button, this, true);
        mIconIv = findViewById(R.id.iv_icon);
        mTitleTv = findViewById(R.id.tv_title);
    }

    //Provide an interface to modify the title
    public void setText(String text) {
        mTitleTv.setText(mText);
    }
}

Overrides the three construction methods and loads the layout file in them, providing an interface for setting the name of the title.

2.1.3 Custom Properties

For ease of use, we usually customize attributes, which define two attributes: icon and title.Define attrs_navigation_button.xml in the values directory:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="NavigationButton">
        <attr name="icon" format="reference"/>
        <attr name="text" format="string"/>
    </declare-styleable>
</resources>

2.1.4 Resolving custom attributes

Modify NavigationButton above:

public class NavigationButton extends LinearLayout {

    private ImageView mIconIv;
    private TextView mTitleTv;
    private Drawable mIcon;
    private CharSequence mText;

    public NavigationButton(Context context) {
        super(context);
        init(context);
    }

    public NavigationButton(Context context, AttributeSet attrs) {
        super(context, attrs);

        //Resolve Custom Properties
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NavigationButton);
        mIcon = typedArray.getDrawable(R.styleable.NavigationButton_icon);
        mText = typedArray.getText(R.styleable.NavigationButton_text);
        //Recycle resources as soon as they are available
        typedArray.recycle();

        init(context);

    }

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

    public void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.navigation_button, this, true);
        mIconIv = findViewById(R.id.iv_icon);
        mTitleTv = findViewById(R.id.tv_title);

        //Set related properties
        mIconIv.setImageDrawable(mIcon);
        mTitleTv.setText(mText);
    }

    public void setText(String text) {
        mTitleTv.setText(mText);
    }
}

2.1.5 Use Composite View

<?xml version="1.0" encoding="utf-8"?>
<!--Must be added schemas Claim to use custom attributes-->
<!--Add is xmlns:app="http://schemas.android.com/apk/res-auto"-->
<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:background="#fff"
    android:gravity="bottom"
    android:orientation="horizontal">

    <com.april.view.NavigationButton
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        app:icon="@drawable/chats_green"
        app:text="WeChat">
    </com.april.view.NavigationButton>

    <com.april.view.NavigationButton
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        app:icon="@drawable/contacts"
        app:text="Mail list">
    </com.april.view.NavigationButton>

    <com.april.view.NavigationButton
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        app:icon="@drawable/discover"
        app:text="find">
    </com.april.view.NavigationButton>

    <com.april.view.NavigationButton
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        app:icon="@drawable/about_me"
        app:text="I">
    </com.april.view.NavigationButton>

</LinearLayout>

The results are:

2.1.7 Summary

Combining Views is very simple, simply inheriting an existing ViewGroup from the system, assembling the layout and adding some custom attributes can be used, generally without the need to process measurements, layouts, drawings, and so on.
By combining views to achieve new effects, you can make this View reusable and easier to maintain and modify.
Of course, the above UI has a better way to do it, just for example.

2.2 Inherit ViewGroup class

Inheriting the ViewGroup class can be used to redefine a layout, but this is a complex way to implement the measurement and layout process of the ViewGroup and to handle the measurement and layout of child elements.Composite Views can also be implemented in this way, but the details that need to be addressed are more complex.

We are here to implement a streaming layout. What is a streaming layout?Streaming layout means that the views added to this container are arranged from left to right, and if the width of the current row is not enough to fit into the next View, the View is automatically placed on the next row.As shown in the following figure:

2.2.1 Demand Analysis

Let's start with a simple analysis of this need:

  1. Streaming layouts require each subview to be laid out, that is, left-to-right, and if the current row is not wide enough, start with the next row.
  2. Streaming layouts require measuring and calculating their own width and height.
  3. Streaming layouts require details such as margin and padding.
  4. Streaming layouts need to provide some custom attributes for users to use.For example, you can set line spacing and horizontal spacing, and so on.

2.2.2 Implementation Steps

Based on the above requirements analysis, the implementation steps are as follows:

  1. Custom properties.
  2. Resolve custom attributes and provide some interfaces for setting attributes.
  3. Override onMeasure() to implement its own measurement process.
  4. Rewrite onLayout() to lay out the position of the child View.
  5. Use a custom View.

2.2.3 Custom Properties

Two properties are defined here: row spacing, horizontal spacing, and attrs_flow_layout.xml in the values directory:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FlowLayout">
        <!--Horizontal spacing-->
        <attr name="horizontal_spacing" format="dimension|reference"/>
        <!--Line spacing-->
        <attr name="vertical_spacing" format="dimension|reference"/>
    </declare-styleable>
</resources>

2.2.4 Resolving custom attributes

Resolve custom attributes and provide some interfaces for setting attributes to the outside world:

public class FlowLayout extends ViewGroup {//Inherit ViewGroup

    private int mHorizontalSpacing;//Horizontal spacing
    private int mVerticalSpacing;//Line spacing
    //Default spacing
    public static final int DEFAULT_Horizontal_SPACING = 10;
    public static final int DEFAULT_Vertical_SPACING = 10;

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

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

        //Resolve Custom Properties
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mHorizontalSpacing = typedArray.getDimensionPixelOffset(R.styleable.FlowLayout_horizontal_spacing, DEFAULT_Horizontal_SPACING);
        mVerticalSpacing = typedArray.getDimensionPixelOffset(R.styleable.FlowLayout_vertical_spacing, DEFAULT_Vertical_SPACING);
        //Recycle resources as soon as they are available
        typedArray.recycle();

    }

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

    //Set Horizontal Spacing
    public void setHorizontalSpacing(int pixelSize) {
        mHorizontalSpacing = pixelSize;
        requestLayout();
    }

    //Set Line Spacing
    public void setVerticalSpacing(int pixelSize) {
        mVerticalSpacing = pixelSize;
        requestLayout();
    }
}

2.2.5 Override onMeasure()

See the code comment below for a more detailed analysis, and it's important to note that we support margin here, so it's complicated.

    private List<Integer> mHeightLists = new ArrayList<>();//Save maximum height of each row

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        //Because we need to support margin, we need to override the generateLayoutParams method and create the MarginLayoutParams object
        return new MarginLayoutParams(getContext(), attrs);
    }

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

        // Obtaining measurement modes and sizes
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // In the case of warp_content, record width and height
        int warpWidth = 0;
        int warpHeight = 0;

        int widthUsed = getPaddingLeft() + getPaddingRight();//Width of Padding
        int lineWidth = widthUsed;//Record the width of the current line
        int lineHeight = 0;//Record the maximum height of a row

        int childCount = getChildCount();
        //Traverse sub View s for measurements
        for (int i = 0; i < childCount; i++) {

            View child = getChildAt(i);

            //Sub View is GONE skipped
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            //Get a layout parameter that supports margin
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            //Measure the width and height of each child, and the maximum width and height available for each child is widthSize-padding-margin
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

            // The actual width and height occupied by the child
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            //Determine if this line can still fit this child
            if (lineWidth + childWidth <= widthSize) {
                //If you can, add up the width of the line and record the maximum height of the line
                lineWidth += childWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childHeight);
            } else {//If the line cannot be loaded and needs to be wrapped, record the width, height, initial width, initial height of the next line

                //Compare the current line width (the current line width minus the horizontal spacing at the end) to the next line width, maximizing
                warpWidth = Math.max(lineWidth - mHorizontalSpacing, widthUsed + childWidth);
                //Wrap lines to record the initial width of new lines
                lineWidth = widthUsed + childWidth + mHorizontalSpacing;

                //Accumulate current height
                warpHeight += lineHeight + mVerticalSpacing;
                //Save the maximum height of each row, which is used when onLayout
                mHeightLists.add(lineHeight);
                //Record the initial height of the next row and set it to the current row
                lineHeight = childHeight;
            }

            // If it is the last child, compare the maximum width of the current record with the current lineWidth
            if (i == childCount - 1) {
                warpWidth = Math.max(warpWidth, lineWidth - mHorizontalSpacing);
                //Accumulated Height
                warpHeight += lineHeight;
            }
        }
        //Save the corresponding measurement width according to the measurement mode
        //That is, if MeasureSpec.EXACTLY uses the width and height passed in directly from the parent ViewGroup
        //Otherwise, set the width and height calculated for yourself when warp_content
        setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : warpWidth,
                (heightMode == MeasureSpec.EXACTLY) ? heightSize : warpHeight);
    }

2.2.6 Override onLayout()

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int width = getWidth();
        int line = 0;//Current line number
        int widthUsed = getPaddingLeft() + getPaddingRight();//Width of Padding
        int lineWidth = widthUsed;//Record the width of the current line
        int left = getPaddingLeft();
        int top = getPaddingTop();

        int childCount = getChildCount();
        //Traverse all sub View s
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);

            //child is GONE skipped
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            //Get a layout parameter that supports margin
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //Get the measured width of the child
            int childWidth = child.getMeasuredWidth();
//            int childHeight = child.getMeasuredHeight();

            //To determine if this line can still fit the child, add the margin value
            if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin <= width) {
                //Add up the width of the line if you can fit it
                lineWidth += childWidth + mHorizontalSpacing;
            } else {//If it cannot be loaded and needs to be wrapped, record the width of the new line and set the new left, top position
                //Reset left
                left = getPaddingLeft();
                //top accumulates the maximum height and spacing of the current row
                top += mHeightLists.get(line++) + mVerticalSpacing;
                //Start a new line, record width
                lineWidth = widthUsed + childWidth + mHorizontalSpacing;
            }
            //Calculate the left,top,right,bottom of a child
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();
            //Calculate the location of the child
            child.layout(lc, tc, rc, bc);
            //left moves one horizontal space to the right
            left = rc + mHorizontalSpacing;
        }
    }

2.2.7 Use Custom View

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff">

    <com.april.view.FlowLayout
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:background="#0ff"
        android:padding="5dp"
        app:horizontal_spacing="10dp"
        app:vertical_spacing="20dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="Android"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="Source Code Analysis"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="custom View"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="Inheritance system already exists View"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="inherit View class"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="inherit ViewGroup class"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="Inheritance system already exists ViewGroup"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_bg"
            android:text="Custom Properties"/>

    </com.april.view.FlowLayout>
</LinearLayout>

Run the program as follows:

2.2.8 Summary

Inherit the ViewGroup class to implement a completely new layout, typically rewriting onMeasure() and onLayout(), and providing custom properties.However, onDraw() generally does not need to be rewritten unless some splitting line or other requirements are met.

Overall, inheriting the ViewGroup class is the most complex, and writing a good custom ViewGroup requires a lot of detail, such as margin, padding, and so on.

In addition, the FlowLayout above is actually not good enough, there are many details not yet implemented, such as support for gravity, and there may be some bug s, but as an example it is sufficient to inherit the ViewGroup class as a custom View.

If you need to use streaming layouts, the actual google also has a control for us, so you can take a look at Flexbox Layout if you're interested.

Posted by worldofcarp on Fri, 17 May 2019 19:09:36 -0700