一、效果图
1.效果一:对应使用方式的方式一(布局中添加子 View 标签效果):
2.效果二:对应使用方式的方式二(代码中添加子 View 标签效果):
两种效果一毛一样,只是为了说明流式布局在布局中添加子 View 和在代码中添加子 View,两种实现方式都是好使的
二、使用方式
1.在布局文件中添加子 View 标签
<com.babycy.flowlayout.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android 开发艺术探索"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="深入理解 Java 虚拟机"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android 源码设计模式解析与实战"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Java 多线程核心编程技术"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="C++ Primer"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="深入理解 Android"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设计模式之禅" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Http 权威指南"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android 系统源代码情景分析"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ES6 标准入门"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="FFmpeg 从入门到精通"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android 应用安全防护和逆向分析"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="JavaScript 高级程序设计"
android:textAllCaps="false" />
</com.babycy.flowlayout.FlowLayout>
2.在代码中添加子 View 标签
public class AutoActivity extends AppCompatActivity {
private FlowLayout mFlowLayout;
private String[] mBooks = {
"Android 开发艺术探索",
"深入理解 Java 虚拟机",
"Android 源码设计模式解析与实战",
"Java 多线程核心编程技术",
"C++ Primer",
"深入理解 Android",
"设计模式之禅",
"Http 权威指南",
"Android 系统源代码情景分析",
"ES6 标准入门",
"FFmpeg 从入门到精通",
"Android 应用安全防护和逆向分析",
"JavaScript 高级程序设计"
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auto);
mFlowLayout = (FlowLayout) findViewById(R.id.fl);
initShowView();
}
private void initShowView() {
LayoutInflater inflater = LayoutInflater.from(this);
for (int i = 0; i < mBooks.length; i++) {
TextView tv = (TextView) inflater.inflate(R.layout.tv_tag, mFlowLayout, false);
tv.setText(mBooks[i]);
tv.setAllCaps(false);
mFlowLayout.addView(tv);
}
}
}
activity_auto.xml(FlowLayout 布局文件)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.babycy.flowlayout.FlowLayout
android:id="@+id/fl"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
tv_tag.xml(FlowLayout 子 View 标签布局文件)
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是一个标签"
android:background="@drawable/tag_bg"
android:layout_margin="5dp"/>
tag_bg.xml(FlowLayout 子 View 标签背景,存放位置:res/drawable)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#e7e7e7"/>
<corners android:radius="30dp"/>
<padding android:left="10dp"
android:top="5dp"
android:right="10dp"
android:bottom="5dp"/>
</shape>
三、实现自定义 View 流式布局
public class FlowLayout extends ViewGroup {
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);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//流式布局 FlowLayout 宽度模式
int wm = MeasureSpec.getMode(widthMeasureSpec);
//流式布局 FlowLayout 宽度大小(精确值或 match_parent)
int ws = MeasureSpec.getSize(widthMeasureSpec);
//流式布局 FlowLayout 高度模式
int hm = MeasureSpec.getMode(heightMeasureSpec);
//流式布局 FlowLayout 高度大小(精确值或 match_parent)
int hs = MeasureSpec.getSize(heightMeasureSpec);
//流式布局 FlowLayout 宽度大小(wrap_content)
int width = 0;
//流式布局 FlowLayout 高度大小(wrap_content)
int height = 0;
//子 View 数量
int count = getChildCount();
//行宽
int lineWidth = 0;
//行高
int lineHeight = 0;
//遍历子 View
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//测量子 View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//在当前行宽 lineWidth 基础上再加当前子 View 宽度 childWidth 之后超出 FlowLayout 宽度
if (childWidth + lineWidth > ws - getPaddingLeft() - getPaddingRight()) {
//换行时(注意:这里只是保存换行之前的宽和高,所以当前行若为最后一行时,则可以判断在当前子 View 是最后一个子 View 时,把最后一行的行宽 lineWidth 和行高 lineHeight 更新到 FlowLayout 宽度 width 和高度 height 里)
//每次在换行时,FlowLayout 宽度 width 取上次保存的宽度 width 和换行前一行的行宽 lineWidth 中的最大值
width = Math.max(width, lineWidth);
//FlowLayout 高度(height)累加行高
height += lineHeight;
//重置行宽为当前子 View 宽度
lineWidth = childWidth;
//重置行高为当前子 View 高度
lineHeight = childHeight;
} else {
//没有换行时
//这里只是记录行宽 lineWidth 和行高 lineHeight,在换行时会被更新在 FlowLayout 宽度 width 和 FlowLayout 高度 height 里
//行宽累加子控件宽度
lineWidth += childWidth;
//行高取上次保存的行高和当前子 View 高度中的最大值
lineHeight = Math.max(childHeight, lineHeight);
}
//当前是最后一个控件时,把最后一行的行宽 lineWidth 和行高 lineHeight 更新到 FlowLayout 宽度 width 和高度 height 里
if (i == count - 1) {
//FlowLayout 宽度 width 取前几行和最后一行行宽中的最大值
width = Math.max(width, lineWidth);
//FlowLayout 高度 height 累加行高
height += lineHeight;
}
//设置 FlowLayout 宽度和高度(记得加上 padding)
setMeasuredDimension(wm == MeasureSpec.EXACTLY ? ws : width + getPaddingLeft() + getPaddingRight(), hm == MeasureSpec.EXACTLY ? hs : height + getPaddingTop() + getPaddingBottom());
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//子 View 数量
int count = getChildCount();
//行宽
int lineWidth = 0;
//行高
int lineHeight = 0;
//子 View 的 top
int top = 0;
//子 View 的 left
int left = 0;
//遍历子 View
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//当前子 View 的宽度
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//当前子 View 的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//换行时
if (childWidth + lineWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
//新一行的第一个子 View 的 top 累加上一行的行高
top += lineHeight;
//新一行的第一个子 View 的 left 重置为 0
left = 0;
//行宽重置为当前子 View 的宽度
lineWidth = childWidth;
//行高重置为当前子 View 的高度
lineHeight = childHeight;
} else {
//行宽累加子 View 的宽度
lineWidth += childWidth;
//行高取上一次存的行高和当前子 View 宽度中的最大值
lineHeight = Math.max(lineHeight, childHeight);
}
//子 View 的 left
int cl = left + lp.leftMargin + getPaddingLeft();
//子 View 的 top
int ct = top + lp.topMargin + getPaddingTop();
//子 View 的 right
int cr = cl + child.getMeasuredWidth();
//子 View 的 bottom
int cb = ct + child.getMeasuredHeight();
//根据 left、top、right、bottom 来摆放当前子 View
child.layout(cl, ct, cr, cb);
//当前行的当前子 View 的 left 累加上一个子 View 的宽度
left += childWidth;
}
}
/**
* 重写 generateLayoutParams()、generateDefaultLayoutParams()
*
* 自定义的 ViewGroup 想要支持子控件的 layout_margin 参数,则必须重载 generateLayoutParams() 函数
* 并且在该函数中返回一个 ViewGroup.MarginLayoutParams 派生类对象,这样才能使用 margin 参数
*
* 默认的 generateLayoutParams() 函数只会提取 layout_width、layout_height 的值
* 只有 MarginLayoutParams() 才具有提取 margin 间距的功能
*/
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}