Handwritten a search flow layout of Taobao and jd.com

Keywords: Android Design Pattern

Some nagging

  • As long as all apps on the market have search function, they are almost inseparable from streaming layout, such as Taobao, JD, xiaohongshu and so on. During the summer vacation, I wrote an app similar to Taobao, which uses this streaming layout.

This is the actual effect of your app

Here are the test results

Inherit ViewGrop to implement custom controls

There are several key points for customizing ViewGrop, among which measurement and placement are the most important.

The first step, of course, is to inherit the ViewGroup

public class FlowLayout extends ViewGroup {

}

Rewrite constructor

Inheriting ViewGrop requires some construction methods, all of which are written and call their own different construction methods to achieve the purpose of unifying parameter entries. So is Google's TextView. Here, getXXX is equivalent to obtaining the defined quantity in the layout file. If there is no definition, set the default value in the method.

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

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

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //Get properties written in XML code
        //xml can set some sub control properties such as margin, color, click effect, font color, font size, etc
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
        mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
        mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
        if(mTextMaxLength!=-1&&mTextMaxLength<=0){
            throw new IllegalArgumentException("max length must not less than 0");
        }
        mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
        if(mMaxLine!=-1&&mMaxLine<=0){
            throw new IllegalArgumentException("max line must not less than 0");
        }
        mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
        mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
        mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
        Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "\n" +
                "mVerticalMargin=" + mVerticalMargin + "\n" +
                "mTextMaxLength=" + mTextMaxLength + "\n" +
                "mTextColor=" + mTextColor + "\n" +
                "mBorderColor=" + mBorderColor + "\n" +
                "mBorderRadius=" + mBorderRadius);
        a.recycle();
    }

Create attrs.xml under the value package and write the attributes you want

    <declare-styleable name="FlowLayout">
        <attr name="itemHorizontalMargin" format="dimension"></attr>
        <attr name="itemVerticalMargin" format="dimension"></attr>
        <attr name="textMaxLength" format="integer"></attr>
        <attr name="textColor" format="color"></attr>
        <attr name="textBorderColor" format="color|reference"></attr>
        <attr name="borderRadius" format="dimension"></attr>
        <attr name="maxLine" format="integer"></attr>
    </declare-styleable>

Provide external interface

The data is transmitted through the set method, and a linked list needs to be maintained internally. The generics here can be customized and passed to an entity class. For simplicity, only text is shown here.

    public void setTextList(List<String> list) {
        mData.clear();
        mData.addAll(list);
        setUpChildren();
    }

setUpChildren() is mainly used to update the text displayed in TextView and provide click events. A for loop traverses all data, then creates a TextView and adds it to ViewGrop.

    private void setUpChildren() {
    	//Remove all child views in ViewGrop
        removeAllViews();
        for (String mDatum : mData) {
            TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.flow_item, this, false);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
            Log.d(TAG,"mDatum.length()---------------->"+mDatum.length());
            String finalMDatum = mDatum;
            textView.setText(mDatum);
            textView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.OnItemClick(v, finalMDatum);
                    }
                }
            });
            //Add child View
            addView(textView);
        }
    }

Internally maintain a click event

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void OnItemClick(View v, String text);
    }

measure

The measurement notes have made it clear.

  //Collection of all rows
    private List<List<View>> lines = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, " in onMeasure");
        int childCount = getChildCount();
        Log.d(TAG, " childCount ========>" + childCount);
        if (childCount == 0) {
            return;
        }
        lines.clear();
        //A collection of all views in a row
        List<View> line = new ArrayList<>();
        //Lines holds a reference to line, and subsequent operations will be directly added to lines
        lines.add(line);

        //The value of the parent control of the control
        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
        int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
        int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != VISIBLE) {
                //If it is not visible, proceed to the next cycle
                continue;
            }
            //Measuring children
            measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);

            //Judge whether to continue adding rows according to the xml custom attributes
            if(mMaxLine!=-1&&lines.size()>mMaxLine){
                return;
            }
            if (line.size() == 0) {
                //Add a child first
                line.add(child);
            } else {
                //Before adding the second child, you need to judge whether it can be added
                boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
                Log.d(TAG, "onMeasure: canBeAdd------------->" + canBeAdd);
                if (canBeAdd) {
                    //Can add
                    line.add(child);
                } else {
                    //Cannot add, reopen a memory
                    //The same is true here. lines are added to line s in advance
                    line = new ArrayList<>();
                    lines.add(line);
                    //The current child needs to be added to the next line
                    i--;
                }
            }
        }

    /**
     * Determine whether children can be added
     *
     * @param line
     * @param child
     * @param parentWidth
     * @return
     */
    private boolean checkChildCanBeAdd(List<View> line, View child, int parentWidth) {
        //Expected at least one TextView in line
        //First add an externally defined paddingleft value
        int totalSize = getPaddingLeft();
        //Add another TextView width from the outside
        totalSize += child.getMeasuredWidth();
        for (View view : line) {
            //Here, calculate the width of all existing textviews in line
            //True width of a TextView = (margin value set externally (spacing between two textviews) + its own original TextView width)
            totalSize += view.getMeasuredWidth();
            totalSize += (int) mHorizontalMargin;
        }
        //Finally, you need to add the right margin
        totalSize += getPaddingRight();
        //Returns whether the calculated total width totalSize is less than the father's width
        return totalSize <= parentWidth;
    }

put

Placement is also a simple algorithm. It is certainly not difficult for you who have done a lot of algorithms to understand.
Look at the above figure directly. Note that the vertical height should be added with paddingTop at the beginning. Similarly, the paddingBottom can be added at the bottom. In the previous figure, you only need to calculate the row and collection of this control, so you don't need to add the padding value in the vertical direction.

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "in onLayout-------------->");
        if (lines.size() == 0) {
            return;
        }
        View firstChild = getChildAt(0);
        //The height of a child is defined here
        int aChildHeight = firstChild.getMeasuredHeight();
        //Left initialization 0
        int aStartLeft;
        //top initialization is set to the height of paddingTop
        int aStartTop = getPaddingTop();
        
        for (int i = 0; i < lines.size(); i++) {
            //There must be a left margin at the beginning of the line
            aStartLeft = getPaddingLeft();
            List<View> line = lines.get(i);
            //Traverse a row of view s
            for (View view : line) {
                //Left position = aStartLeft
                //Upper position = aStartTop
                //Right position = width of aStartLeft+view
                //Lower position = height of aStartTop+view
                view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
                        aStartTop + view.getMeasuredHeight());
                //There should be a horizontal margin, where the right position plus this margin is the starting position of the next control
                aStartLeft += (int) mHorizontalMargin;
                //aStartLeft has not been changed
                aStartLeft += view.getMeasuredWidth();
            }
            //Process next line
            //Height = height of child control plus Margin value
            aStartTop += aChildHeight;
            aStartTop += (int) mVerticalMargin;
        }
    }

usage method

xml code

        <com.lw.flow.FlowLayout
            android:id="@+id/flowLayout"
            android:layout_width="match_parent"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:paddingTop="10dp"
            android:layout_height="match_parent">
        </com.lw.flow.FlowLayout>

Here, other attributes are written into the code

In Activity:

        flowLayout = findViewById(R.id.flowLayout);
        List<String> list = new ArrayList<>();
        list.add("This is the key");
        list.add("iPad");
        list.add("Android");
        list.add("Digital video camera");
        list.add("headset");
        list.add("mouse");
        list.add("keyboard");

        for (int i = 0; i < 5; i++) {
            list.add("keyword" + i);
        }
        flowLayout.setTextList(list);
  
        flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
            @Override
            public void OnItemClick(View v, String text) {
                Toast.makeText(getApplicationContext(),"Clicked:"+text,Toast.LENGTH_SHORT).show();
            }
        });

The effects are as follows:

After integration into your own business, you can have the following effects:

  • After serializing the linked list and caching it in SharedPreference, it can be saved locally.

  • Click a text here, and the text will automatically jump to the first one, which is also easy to write. Just invert the linked list directly in Collections.reverse(lists),

Complete code

package com.lw.tiketunion.ui.custom;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.InputFilter;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.lw.tiketunion.R;
import com.lw.tiketunion.base.App;
import com.lw.tiketunion.utils.LogUtils;
import com.lw.tiketunion.utils.SizeUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * author: LiuWei
 * Email: 1244204021@qq.com
 * Date: 2021/8/10 16:23
 * Description:The code of FlowLayout
 */
public class FlowLayout extends ViewGroup {
    private static final String TAG = "FlowLayout";
    private static final int DEFAULT_MAX_LINE = -1;
    private List<String> mData = new ArrayList<>();

    public static final int DEFAULT_BORDER_RADIUS = SizeUtils.dip2px(App.getContext(), 5);
    public static final int DEFAULT_TEXT_MAX_LENGTH = 5;
    //Spacing of each View in a row
    private static final int DEFAULT_HORIZONTAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);
    //Spacing per row
    private static final int DEFAULT_VERTICAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);

    private final int mTextColor;
    private float mHorizontalMargin;
    private float mVerticalMargin;
    private int mTextMaxLength;
    private int mBorderColor;
    private float mBorderRadius;
    private OnItemClickListener onItemClickListener;
    private int mMaxLine;

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

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

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mHorizontalMargin = a.getDimension(R.styleable.FlowLayout_itemHorizontalMargin, DEFAULT_HORIZONTAL_MARGIN);
        mVerticalMargin = a.getDimension(R.styleable.FlowLayout_itemVerticalMargin, DEFAULT_VERTICAL_MARGIN);
        mTextMaxLength = a.getInt(R.styleable.FlowLayout_textMaxLength, DEFAULT_TEXT_MAX_LENGTH);
        if (mTextMaxLength != -1 && mTextMaxLength <= 0) {
            throw new IllegalArgumentException("max length must not less than 0");
        }
        mMaxLine = a.getInt(R.styleable.FlowLayout_maxLine, DEFAULT_MAX_LINE);
        if (mMaxLine != -1 && mMaxLine <= 0) {
            throw new IllegalArgumentException("max line must not less than 0");
        }
        mTextColor = a.getColor(R.styleable.FlowLayout_textColor, getResources().getColor(R.color.black));
        mBorderColor = a.getColor(R.styleable.FlowLayout_textBorderColor, getResources().getColor(R.color.black));
        mBorderRadius = a.getDimension(R.styleable.FlowLayout_borderRadius, DEFAULT_BORDER_RADIUS);
        Log.d(TAG, "FlowLayout: mHorizontalMargin" + mHorizontalMargin + "\n" +
                "mVerticalMargin=" + mVerticalMargin + "\n" +
                "mTextMaxLength=" + mTextMaxLength + "\n" +
                "mTextColor=" + mTextColor + "\n" +
                "mBorderColor=" + mBorderColor + "\n" +
                "mBorderRadius=" + mBorderRadius);
        a.recycle();
    }

    public void setTextList(List<String> list) {
        mData.clear();
        mData.addAll(list);
        setUpChildren();
    }

    public void deleteAllList() {
        mData.clear();
        removeAllViews();
        TextView textView = new TextView(getContext());
        textView.setText("No history");
        textView.setTextColor(Color.BLACK);
        addView(textView);
        invalidate();
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void OnItemClick(View v, String text);
    }

    private void setUpChildren() {
        removeAllViews();
        for (String mDatum : mData) {
            TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.item_flow, this, false);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(mTextMaxLength)});
            Log.d(TAG, "mDatum.length()---------------->" + mDatum.length());
            String finalMDatum = mDatum;
            textView.setText(mDatum);
            textView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.OnItemClick(v, finalMDatum);
                    }
                }
            });
            addView(textView);
        }
    }

    //Collection of all rows
    private List<List<View>> lines = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, " in onMeasure");
        int childCount = getChildCount();
        Log.d(TAG, " childCount ========>" + childCount);
        if (childCount == 0) {
            return;
        }
        lines.clear();
        //A collection of all views in a row
        List<View> line = new ArrayList<>();
        lines.add(line);

        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);

        int childMeasureSpaceWidth = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
        int childMeasureSpaceHeight = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.AT_MOST);

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != VISIBLE) {
                //If it is not visible, proceed to the next cycle
                continue;
            }
            //Measuring children
            measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);

            if (mMaxLine != -1 && lines.size() > mMaxLine) {
                return;
            }
            if (line.size() == 0) {
                //Add a child first
                line.add(child);
            } else {
                //Before adding the second child, you need to judge whether it can be added
                boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
                Log.d(TAG, "onMeasure: canBeAdd------------->" + canBeAdd);
                if (canBeAdd) {
                    //Can add
                    line.add(child);
                } else {
                    line = new ArrayList<>();
                    lines.add(line);
                    i--;
                }
            }
        }
        int finalParentHeight;
        View child = getChildAt(0);
        int measuredHeight = child.getMeasuredHeight();
        Log.d(TAG, "onMeasure:lines.size()--------> " + lines.size());
        finalParentHeight = lines.size() * (measuredHeight + (int) mVerticalMargin);
        setMeasuredDimension(parentWidth, finalParentHeight);
    }

    /**
     * Determine whether children can be added
     *
     * @param line
     * @param child
     * @param parentWidth
     * @return
     */
    private boolean checkChildCanBeAdd(List<View> line, View child, int parentWidth) {
        int totalSize = getPaddingLeft();
        totalSize += child.getMeasuredWidth();
        for (View view : line) {
            totalSize += view.getMeasuredWidth();
            totalSize += (int) mHorizontalMargin;
        }
        totalSize += getPaddingRight();
        return totalSize <= parentWidth;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "in onLayout-------------->");
        if (lines.size() == 0) {
            return;
        }
        View firstChild = getChildAt(0);
        int aChildHeight = firstChild.getMeasuredHeight();
        int aStartLeft;
        int aStartTop = getPaddingTop();
        for (int i = 0; i < lines.size(); i++) {
            aStartLeft = getPaddingLeft();
            List<View> line = lines.get(i);
            for (View view : line) {
                view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
                        aStartTop + view.getMeasuredHeight());
                aStartLeft += (int) mHorizontalMargin;
                aStartLeft += view.getMeasuredWidth();
            }
            aStartTop += aChildHeight;
            aStartTop += (int) mVerticalMargin;
        }
    }
}

Recommended learning materials:

Posted by socadmin on Fri, 19 Nov 2021 17:28:14 -0800