一、流式布局核心概念
1. 基本特性
-
自动换行:子视图按顺序排列,超出宽度自动换行
-
灵活适配:适应不同屏幕尺寸和方向变化
-
无行列限制:不同于GridLayout,不需要预先定义行列数
-
动态排列:根据内容动态调整布局
2. 应用场景
-
标签云展示
-
商品属性筛选
-
兴趣选择界面
-
搜索关键词历史
二、自定义FlowLayout实现步骤
1. 基础类结构定义
public class FlowLayout extends ViewGroup {
private int mHorizontalSpacing = 16; // 水平间距(dp)
private int mVerticalSpacing = 16; // 垂直间距(dp)
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 将dp转换为px
mHorizontalSpacing = dp2px(context, mHorizontalSpacing);
mVerticalSpacing = dp2px(context, mVerticalSpacing);
// 读取自定义属性
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mHorizontalSpacing = ta.getDimensionPixelSize(
R.styleable.FlowLayout_horizontalSpacing, mHorizontalSpacing);
mVerticalSpacing = ta.getDimensionPixelSize(
R.styleable.FlowLayout_verticalSpacing, mVerticalSpacing);
ta.recycle();
}
}
private int dp2px(Context context, float dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp,
context.getResources().getDisplayMetrics());
}
}
三、核心测量与布局逻辑
1. onMeasure实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 实际需要的宽高
int width = 0;
int height = 0;
// 当前行的宽高
int lineWidth = 0;
int lineHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子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;
// 是否需要换行
if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
width = Math.max(width, lineWidth);
height += lineHeight + mVerticalSpacing;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
lineWidth += childWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
}
// 最后一个元素
if (i == childCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
// 考虑padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
// 处理AT_MOST和EXACTLY模式
width = widthMode == MeasureSpec.EXACTLY ? widthSize : Math.min(width, widthSize);
height = heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(height, heightSize);
setMeasuredDimension(width, height);
}
2. onLayout实现
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
if (childCount == 0) return;
int width = getWidth();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int lineWidth = 0;
int lineHeight = 0;
int currentTop = paddingTop;
int currentLeft = paddingLeft;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 是否需要换行
if (currentLeft + childWidth > width - paddingLeft) {
currentTop += lineHeight + mVerticalSpacing;
currentLeft = paddingLeft;
lineWidth = 0;
lineHeight = 0;
}
// 计算子View位置
int left = currentLeft + lp.leftMargin;
int top = currentTop + lp.topMargin;
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
// 更新行状态
currentLeft += childWidth + mHorizontalSpacing;
lineWidth += childWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
}
}
四、Margin支持与LayoutParams
1. 自定义LayoutParams
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof MarginLayoutParams;
}
五、XML属性定义与使用
1. 定义自定义属性
<!-- res/values/attrs.xml -->
<declare-styleable name="FlowLayout">
<attr name="horizontalSpacing" format="dimension"/>
<attr name="verticalSpacing" format="dimension"/>
</declare-styleable>
2. 在布局中使用
<com.example.customview.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:horizontalSpacing="8dp"
app:verticalSpacing="12dp"
android:padding="16dp">
<!-- 动态或静态添加子View -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签1"
android:background="@drawable/tag_bg"/>
<!-- 更多子View... -->
</com.example.customview.FlowLayout>
六、动态添加子视图
1. 添加文本标签
FlowLayout flowLayout = findViewById(R.id.flow_layout);
String[] tags = {"Android", "Kotlin", "Java", "Flutter", "iOS",
"Python", "React Native", "前端", "后端"};
for (String tag : tags) {
TextView textView = new TextView(this);
textView.setText(tag);
textView.setTextSize(14);
textView.setPadding(dp2px(12), dp2px(6), dp2px(12), dp2px(6));
textView.setBackgroundResource(R.drawable.tag_background);
FlowLayout.LayoutParams params = new FlowLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, dp2px(8), dp2px(8));
flowLayout.addView(textView, params);
}
2. 标签点击处理
flowLayout.setOnChildClickListener(new FlowLayout.OnChildClickListener() {
@Override
public void onChildClick(View view, int position) {
TextView tv = (TextView) view;
Toast.makeText(MainActivity.this,
"点击: " + tv.getText(), Toast.LENGTH_SHORT).show();
}
});
// 在FlowLayout中添加接口
public interface OnChildClickListener {
void onChildClick(View view, int position);
}
// 在onTouchEvent中处理点击事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (isPointInView(event.getX(), event.getY(), child)
&& mListener != null) {
mListener.onChildClick(child, i);
break;
}
}
}
return super.onTouchEvent(event);
}
private boolean isPointInView(float x, float y, View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int viewX = location[0];
int viewY = location[1];
return (x >= viewX && x <= (viewX + view.getWidth()) &&
y >= viewY && y <= (viewY + view.getHeight()));
}
七、性能优化建议
1. 视图回收与复用
// 在Adapter中复用已有视图
public void updateTags(List<String> newTags) {
flowLayout.removeAllViews();
for (String tag : newTags) {
TextView tv = getTagViewFromPool(); // 从缓存池获取
if (tv == null) {
tv = new TextView(getContext());
// 初始化设置...
}
tv.setText(tag);
flowLayout.addView(tv);
}
}
2. 异步加载与分批渲染
// 分批加载大量标签
private void loadTagsInBatches(List<String> allTags, int batchSize) {
int total = allTags.size();
for (int i = 0; i < total; i += batchSize) {
final int start = i;
final int end = Math.min(i + batchSize, total);
post(() -> {
for (int j = start; j < end; j++) {
addTagView(allTags.get(j));
}
});
}
}
3. 布局缓存优化
// 在FlowLayout中
private boolean mMeasurementCacheEnabled = true;
private SparseArray<ViewMeasurement> mMeasurementCache = new SparseArray<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mMeasurementCacheEnabled) {
int key = makeMeasureSpecKey(widthMeasureSpec, heightMeasureSpec);
ViewMeasurement cached = mMeasurementCache.get(key);
if (cached != null) {
setMeasuredDimension(cached.width, cached.height);
return;
}
}
// 正常测量逻辑...
if (mMeasurementCacheEnabled) {
int key = makeMeasureSpecKey(widthMeasureSpec, heightMeasureSpec);
mMeasurementCache.put(key, new ViewMeasurement(
getMeasuredWidth(), getMeasuredHeight()));
}
}
八、高级功能扩展
1. 对齐方式支持
// 在FlowLayout中添加属性
private int mGravity = Gravity.START; // 默认左对齐
// 在onLayout中处理对齐
switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
currentLeft = paddingLeft + (availableWidth - lineWidth) / 2;
break;
case Gravity.END:
currentLeft = paddingLeft + (availableWidth - lineWidth);
break;
// START/LEFT 已经默认处理
}
2. 最大行数限制
private int mMaxLines = Integer.MAX_VALUE;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// ...在测量过程中...
if (lineCount >= mMaxLines) {
// 添加省略号View
if (!hasMoreView) {
addMoreView();
measureChild(moreView, widthMeasureSpec, heightMeasureSpec);
lineWidth += moreView.getMeasuredWidth() + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, moreView.getMeasuredHeight());
}
break;
}
}
3. 动画效果支持
public void addViewWithAnimation(final View child, long delay) {
child.setAlpha(0f);
child.setScaleX(0.5f);
child.setScaleY(0.5f);
super.addView(child);
child.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setStartDelay(delay)
.setDuration(300)
.start();
}
通过以上实现,我们完成了一个功能完善的流式布局控件,支持动态内容、点击事件、自定义间距和多种高级特性,可以灵活应用于各种需要自动换行布局的场景。