FlowLayout(流式布局)的实现

文章目录

  • 前言
  • 代码实现(Flowlayout)
  • 使用方法
    • 1. 编写 item 布局
    • 2. 编写 Adapter
    • 3. 主活动中使用 FlowLayout
  • 参考

前言

最近在做的项目决定用流式布局来展示历史记录,刚好自己也想学习自定义 ViewGroup,所以就参考了其他的一些文章,写了一个 FlowLayout(流式布局),效果如下:

FlowLayout(流式布局)的实现_第1张图片

代码实现(Flowlayout)

只有一个 FlowLayout 类,完整代码如下:

/**
 * @author Feng Zhaohao
 * Created on 2019/11/10
 */
public class FlowLayout extends ViewGroup {

    private static final String TAG = "FlowLayout";

    private List<Rect> mChildrenPositionList = new ArrayList<>();   // 记录各子 View 的位置
    private int mMaxLines = Integer.MAX_VALUE;      // 最多显示的行数,默认无限制
    private int mVisibleItemCount;       // 可见的 item 数

    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 清除之前的位置
        mChildrenPositionList.clear();
        // 测量所有子元素(这样 child.getMeasuredXXX 才能获取到值)
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int[] a = helper(widthSize);
        int measuredHeight = 0;
        // EXACTLY 模式:对应指定大小和 match_parent
        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        }
        // AT_MOST 模式,对应 wrap_content
        else if (heightMode == MeasureSpec.AT_MOST) {
            measuredHeight = a[0];
        }
        int measuredWidth = 0;
        if (widthMode == MeasureSpec.EXACTLY) {
            measuredWidth = widthSize;
        }
        else if (widthMode == MeasureSpec.AT_MOST) {
            measuredWidth = a[1];
        }

        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    /**
     * 在 wrap_content 情况下,得到布局的测量高度和测量宽度
     * 返回值是一个有两个元素的数组 a,a[0] 代表测量高度,a[1] 代表测量宽度
     */
    private int[] helper(int widthSize) {
        boolean isOneRow = true;    // 是否是单行
        int width = getPaddingLeft();   // 记录当前行已有的宽度
        int height = getPaddingTop();   // 记录当前行已有的高度
        int maxHeight = 0;      // 记录当前行的最大高度
        int currLine = 1;       // 记录当前行数

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 获取当前子元素的 margin
            LayoutParams params = child.getLayoutParams();
            MarginLayoutParams mlp;
            if (params instanceof MarginLayoutParams) {
                mlp = (MarginLayoutParams) params;
            } else {
                mlp = new MarginLayoutParams(params);
            }
            // 记录子元素所占宽度和高度
            int childWidth = mlp.leftMargin + child.getMeasuredWidth() + mlp.rightMargin;
            int childHeight = mlp.topMargin + child.getMeasuredHeight() + mlp.bottomMargin;
            maxHeight = Math.max(maxHeight, childHeight);

            // 判断是否要换行
            if (width + childWidth + getPaddingRight() > widthSize) {
                // 加上该行的最大高度
                height += maxHeight;
                // 重置 width 和 maxHeight
                width = getPaddingLeft();
                maxHeight = childHeight;
                isOneRow = false;
                currLine++;
                if (currLine > mMaxLines) {
                    break;
                }
            }
            // 存储该子元素的位置,在 onLayout 时设置
            Rect rect = new Rect(width + mlp.leftMargin,
                    height + mlp.topMargin,
                    width + childWidth - mlp.rightMargin,
                    height + childHeight - mlp.bottomMargin);
            mChildrenPositionList.add(rect);

            // 加上该子元素的宽度
            width += childWidth;
        }

        int[] res = new int[2];
        res[0] = height + maxHeight + getPaddingBottom();
        res[1] = isOneRow? width + getPaddingRight() : widthSize;

        return res;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 布置子 View 的位置
        int n = Math.min(getChildCount(), mChildrenPositionList.size());
        for (int i = 0; i < n; i++) {
            View child = getChildAt(i);
            Rect rect = mChildrenPositionList.get(i);
            child.layout(rect.left, rect.top, rect.right, rect.bottom);
        }
        mVisibleItemCount = n;
    }

    /**
     * 设置 Adapter
     */
    public void setAdapter(Adapter adapter) {
        // 移除之前的视图
        removeAllViews();
        // 添加 item
        int n = adapter.getItemCount();
        for (int i = 0; i < n; i++) {
            ViewHolder holder = adapter.onCreateViewHolder(this);
            adapter.onBindViewHolder(holder, i);
            View child = holder.itemView;
            addView(child);
        }
    }

    /**
     * 设置最多显示的行数
     */
    public void setMaxLines(int maxLines) {
        mMaxLines = maxLines;
    }

    /**
     * 获取显示的 item 数
     */
    public int getVisibleItemCount() {
        return mVisibleItemCount;
    }

    public abstract static class Adapter<VH extends ViewHolder> {

        public abstract VH onCreateViewHolder(ViewGroup parent);

        public abstract void onBindViewHolder(VH holder, int position);

        public abstract int getItemCount();

    }

    public abstract static class ViewHolder {
        public final View itemView;

        public ViewHolder(View itemView) {
            if (itemView == null) {
                throw new IllegalArgumentException("itemView may not be null");
            }
            this.itemView = itemView;
        }
    }
}

详细说明请看注释,这里就不多说了。下面看下使用方法:

使用方法

使用方法和 RecyclerView 类似,先编写 item 的布局,然后编写 Adapter,最后给 FlowLayout 设置 Adapter 即可显示数据。

1. 编写 item 布局


<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_light"
    android:textColor="@android:color/white"
    android:textSize="24sp"
    android:id="@+id/tv_test_content"
    android:padding="10dp" />

简单起见,布局只有一个 TextView。

注意:不能在 xml 中设置 margin 属性,需在代码中设置,后面会说明。

2. 编写 Adapter

public class FlowAdapter extends FlowLayout.Adapter<FlowAdapter.FlowViewHolder> {

    private static final String TAG = "FlowAdapter";

    private Context mContext;
    private List<String> mContentList;

    public FlowAdapter(Context mContext, List<String> mContentList) {
        this.mContext = mContext;
        this.mContentList = mContentList;
    }

    @Override
    public FlowViewHolder onCreateViewHolder(ViewGroup parent) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.item_test, parent, false);
        // 给 View 设置 margin
        ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(view.getLayoutParams());
        int margin = BaseUtil.dip2px(mContext, 5);
        mlp.setMargins(margin, margin, margin, margin);
        view.setLayoutParams(mlp);

        return new FlowViewHolder(view);
    }

    @Override
    public void onBindViewHolder(FlowViewHolder holder, int position) {
        holder.content.setText(mContentList.get(position));
    }

    @Override
    public int getItemCount() {
        return mContentList.size();
    }

    class FlowViewHolder extends FlowLayout.ViewHolder {
        TextView content;

        public FlowViewHolder(View itemView) {
            super(itemView);
            content = itemView.findViewById(R.id.tv_test_content);
        }
    }
}

如果想要给子 View 设置 margin,需在 onCreateViewHolder 方法创建 View 时设置:

    // 给 View 设置 margin
    ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(view.getLayoutParams());
    int margin = BaseUtil.dip2px(mContext, 5);
    mlp.setMargins(margin, margin, margin, margin);
    view.setLayoutParams(mlp);

3. 主活动中使用 FlowLayout

    private static final String TAG = "TestActivity";
    private FlowLayout mFlowLayout;
    private List<String> mContentList = new ArrayList<>();
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        initData();
        initView();
    }
    

    private void initData() {
        for (int i = 0; i < 4; i++) {
            mContentList.add("java");
            mContentList.add("kotlin");
            mContentList.add("android");
            mContentList.add("android-studio");
            mContentList.add("app");
        }
    }
    
    @Override
    protected void initView() {
        mFlowLayout = findViewById(R.id.fv_test_flow_layout);
        // 设置 Adapter
        FlowAdapter adapter = new FlowAdapter(this, mContentList);
        mFlowLayout.setAdapter(adapter);
        // 设置最多显示的行数
        mFlowLayout.setMaxLines(3);
        // 获取显示的 item 数
        mFlowLayout.post(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "count = " + mFlowLayout.getVisibleItemCount());
            }
        });
    }

参考

  • Android流式布局(FlowLayout)
  • Android FlowLayout流式布局
  • Android 使用MarginLayoutParams在Java代码中设置View的margin属性

你可能感兴趣的:(android)