- 一些叨叨
- 继承ViewGrop 实现自定义控件
- 重写构造器
- 提供对外接口
- 测量
- 摆放
- 使用方法
- 完整代码
- 市面上所有的app只要有搜索功能,几乎都离不开流式布局,像淘宝、京东、小红书等等。暑假的时候写了一个类似淘宝的app,就用到了这个流式布局。
这个是自己的app实战效果
下面是测试效果
自定义ViewGrop有几个关键点,其中测量 、摆放最重要。
第一步当然是继承ViewGroup了
public class FlowLayout extends ViewGroup {
}
重写构造器
继承 ViewGrop需要一些构造方法, 全部写调用自身不同的构造方法达到统一参数入口的目的,谷歌的TextView也是这样写的。这里getXXX就相当于在layout文件中获取定义过的量,没有定义就设置方法中的缺省值。
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);
//获取XML代码写的属性
//xml可以设置一些子控件边距、颜色、点击效果、字体颜色、字体大小等等属性
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();
}
在value包下创建attrs.xml,写上自己想要的属性
提供对外接口
数据通过set方法传进来,内部需要维护一个链表。这里的泛型可以自定义,可以传一个实体类,这里简单起见,仅展示文本。
public void setTextList(Listlist) { mData.clear(); mData.addAll(list); setUpChildren(); }
setUpChildren()主要用来更新TextView中展示的文本,以及提供点击事件。一个for循环遍历完所有数据,添加然后创建TextView,添加到ViewGrop中即可。
private void setUpChildren() {
//移除ViewGrop中所有子View
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);
}
}
});
//添加子View
addView(textView);
}
}
内部维护一个点击事件
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void OnItemClick(View v, String text);
}
测量
测量已经注释已经说的很清楚啦。
//所有行的集合
private List> 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();
//一行中所有View的集合
List line = new ArrayList<>();
//lines持有line的引用,后面操作会直接添加到lines里
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) {
//不可见就进行下一个循环
continue;
}
//测量孩子
measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);
//根据xml自定义的属性判断是否需要继续添加行
if(mMaxLine!=-1&&lines.size()>mMaxLine){
return;
}
if (line.size() == 0) {
//先添加一个孩子
line.add(child);
} else {
//第二个孩子添加之前需要判断一下是否可以添加
boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
if (canBeAdd) {
//可以添加
line.add(child);
} else {
//不能添加,重新开一个内存
//这里也是一样,line提前被添加到lines中了
line = new ArrayList<>();
lines.add(line);
//当前的孩子还需要添加到下一行
i--;
}
}
}
private boolean checkChildCanBeAdd(List line, View child, int parentWidth) {
//应为line里一定有至少一个TextView
//先加一个外部定义的paddingleft的值
int totalSize = getPaddingLeft();
//再添加一个外部传来TextView的宽度
totalSize += child.getMeasuredWidth();
for (View view : line) {
//这里计算line里所有已经有的TextView宽度
//一个TextView真实宽度=(外部设置的margin值(两个TextView之间的间距)+自身原本TextView宽度)
totalSize += view.getMeasuredWidth();
totalSize += (int) mHorizontalMargin;
}
//最后需要加上右边距
totalSize += getPaddingRight();
//返回计算好的总宽度totalSize是否小于父亲的宽度
return totalSize <= parentWidth;
}
摆放
摆放也算一个简单的算法了吧,对于做过好多算法的你们来说肯定不难理解。
这里直接也看着上面的图,注意,这里开始的时候垂直高度要加paddingTop,同样底边也可以加一下paddingBottom,前面的图应为只需要计算这个控件在哪一行,哪个集合里,所以不需要加垂直方向的padding值。
@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);
//这里定义好一个child的高度
int aChildHeight = firstChild.getMeasuredHeight();
//左边初始化0
int aStartLeft;
//top初始化设置为paddingTop的高度
int aStartTop = getPaddingTop();
for (int i = 0; i < lines.size(); i++) {
//一行的开始肯定是先有左边距啦
aStartLeft = getPaddingLeft();
List line = lines.get(i);
//遍历一行的view
for (View view : line) {
//左边位置=aStartLeft
//上边位置=aStartTop
//右边位置=aStartLeft+view的宽度
//下边位置=aStartTop+view的高度
view.layout(aStartLeft, aStartTop, aStartLeft + view.getMeasuredWidth(),
aStartTop + view.getMeasuredHeight());
//应为存在水平边距,这里右边位置要加上这个边距才是下一个控件起始位置
aStartLeft += (int) mHorizontalMargin;
//aStartLeft还没改值
aStartLeft += view.getMeasuredWidth();
}
//处理下一行
//高度=子控件高度加上Margin值
aStartTop += aChildHeight;
aStartTop += (int) mVerticalMargin;
}
}
使用方法
xml代码
这里其他属性就自己写到代码里啦
Activity中:
flowLayout = findViewById(R.id.flowLayout);
List list = new ArrayList<>();
list.add("这是个关键");
list.add("iPad");
list.add("Android");
list.add("数码摄像机");
list.add("耳机");
list.add("鼠标");
list.add("键盘");
for (int i = 0; i < 5; i++) {
list.add("关键字" + i);
}
flowLayout.setTextList(list);
flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
@Override
public void OnItemClick(View v, String text) {
Toast.makeText(getApplicationContext(),"点击了:"+text,Toast.LENGTH_SHORT).show();
}
});
效果如下:
整合到自己的业务之后就能有下面的效果啦:
-
将链表序列化后缓存到SharedPreference中,就能本地保存了。
-
这里点击一个文字,该文字自动跳到第一个也很好写,直接Collections.reverse(lists)将链表倒置就行啦,
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;
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private static final int DEFAULT_MAX_LINE = -1;
private List mData = new ArrayList<>();
public static final int DEFAULT_BORDER_RADIUS = SizeUtils.dip2px(App.getContext(), 5);
public static final int DEFAULT_TEXT_MAX_LENGTH = 5;
//一行中每个View的间距
private static final int DEFAULT_HORIZONTAL_MARGIN = SizeUtils.dip2px(App.getContext(), 10);
//每行间距
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 list) {
mData.clear();
mData.addAll(list);
setUpChildren();
}
public void deleteAllList() {
mData.clear();
removeAllViews();
TextView textView = new TextView(getContext());
textView.setText("暂无历史记录");
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);
}
}
//所有行的集合
private List> 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();
//一行中所有View的集合
List 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) {
//不可见就进行下一个循环
continue;
}
//测量孩子
measureChild(child, childMeasureSpaceWidth, childMeasureSpaceHeight);
if (mMaxLine != -1 && lines.size() > mMaxLine) {
return;
}
if (line.size() == 0) {
//先添加一个孩子
line.add(child);
} else {
//第二个孩子添加之前需要判断一下是否可以添加
boolean canBeAdd = checkChildCanBeAdd(line, child, parentWidth);
Log.d(TAG, "onMeasure: canBeAdd-------------》" + canBeAdd);
if (canBeAdd) {
//可以添加
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);
}
private boolean checkChildCanBeAdd(List 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 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;
}
}
}
推荐学习资料:
- 视频:
https://www.bilibili.com/video/BV1oa4y1E7Fb?spm_id_from=333.999.0.0
这个up主自定义控件真的讲解的很好,通俗易懂。 - 其他:
功能更强大的FlowLayout:https://github.com/jhwsx/BlogCodes/tree/master/FlowLayout
参考资料
https://blog.csdn.net/lmj623565791/article/details/38339817
https://mp.weixin.qq.com/s/jNdy0ol-oB2nQugptEK5wQ



