Android 流式布局(FlowLayout)实现深度解析

一、流式布局核心概念

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();
}

通过以上实现,我们完成了一个功能完善的流式布局控件,支持动态内容、点击事件、自定义间距和多种高级特性,可以灵活应用于各种需要自动换行布局的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值