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:
- Video:
https://www.bilibili.com/video/BV1oa4y1E7Fb?spm_id_from=333.999.0.0
This up main custom control is really well explained and easy to understand. - other:
More powerful FlowLayout: https://github.com/jhwsx/BlogCodes/tree/master/FlowLayout
reference material
https://blog.csdn.net/lmj623565791/article/details/38339817
https://mp.weixin.qq.com/s/jNdy0ol-oB2nQugptEK5wQ