美文网首页
Android FlowLayout 流式布局

Android FlowLayout 流式布局

作者: 因为我的心 | 来源:发表于2023-04-03 14:01 被阅读0次

1、FlowLayout 流式布局

Android 流式布局控件,实现自动换行,操出范围可以滑动功能,未使用控件复用功能,所以不应该有太多的子控件。

gitHub地址:https://github.com/itrenjunhua/FlowLayout

2、主要包含功能:

  • 流式布局,自动换行
  • 使用Adapter的形势注入子控件
  • 设置子控件之间的间距(水平方向和竖直方向)
  • 竖直方向超出高度可以滑动
  • 给子控件设置点击监听
  • 设置可显示的最大行数,并提供方法判断是否当前所有的子控件都显示完成
  • 可以设置行内水平方向上对齐方式(居左对齐、居右对齐、两端对齐/左右对齐、居中对齐)
  • 提供自动滚动到顶部、滚动到底部、滚动到指定位置和滚动到指定行方法

3、效果图

图片.png

4、 使用

  1. 在布局中使用控件 FlowLayout.java 完整代码

注意:初始化最大行数,必须用xml中的flow_max_row_count,动态设置最大行数不生效。

//默认最大行数设置:   app:flow_max_row_count="2"
 <com.renj.flowlayout.FlowLayout
         android:id="@+id/flow_layout"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="8dp"
         android:background="@color/colorAccent"
         android:paddingStart="12dp"
         android:paddingTop="8dp"
         android:paddingEnd="8dp"
         android:paddingBottom="12dp"
         app:flow_max_row_count="2"
         app:flow_horizontal_gravity="right"
         app:flow_horizontal_spacing="6dp"
         app:flow_vertical_spacing="6dp" />

2、继承 FlowLayoutAdapter ,重写相关方法

package com.youjiakeji.yjkjreader.ui.view.flowlayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

import com.youjiakeji.yjkjreader.R;

import java.util.ArrayList;
import java.util.List;

/**
 * ======================================================================
 * <p>
 * 作者:Renj
 * <p>
 * 创建时间:2020-10-29   09:51
 * <p>
 * 描述:流式布局控件
 * <p>
 * 修订历史:
 * <p>
 * ======================================================================
 */
public class FlowLayout extends ViewGroup {
    /**
     * 居左对齐,默认
     */
    public static final int HORIZONTAL_GRAVITY_LEFT = 0;
    /**
     * 居右对齐
     */
    public static final int HORIZONTAL_GRAVITY_RIGHT = 1;
    /**
     * 左右对齐
     */
    public static final int HORIZONTAL_GRAVITY_LEFT_RIGHT = 2;
    /**
     * 居中对齐
     */
    public static final int HORIZONTAL_GRAVITY_CENTER = 3;

    private int mViewContentWidth; // 内容显示宽度
    private int mViewContentHeight; // 内容显示高度
    private int mViewReallyHeight;  // 控件实际高度(所有子控件的高度和+paddingTop+paddingBottom)
    private int mHorizontalSpacing; // 水平方向间距
    private int mVerticalSpacing;   // 竖直方向间距

    private int mShowChildViewCount; // 显示的子控件数
    private boolean mChildViewAllShow = true; // 子控件是否已经全部显示了
    private int mTotalShowRowCount; // 总显示行数
    private int mMaxRowCount = Integer.MAX_VALUE; // 最大显示行数
    private List<RowChildViewInfo> mRowChildViewList = new ArrayList<>(); // 所有子控件行信息集合

    private int mMaxScrollY; // 滑动时,最大滑动偏移量
    private Scroller mScroller; // 支持滑动
    private VelocityTracker mVelocityTracker; // ACTION_UP 时测速

    // 每一行的水平方向对齐方式
    private int mHorizontalGravity = HORIZONTAL_GRAVITY_LEFT;

    // 适配器对象
    private FlowLayoutAdapter mFlowLayoutAdapter;
    // 子控件点击监听
    private OnItemClickListener mOnItemClickListener;
    // 子控件布局完成监听
    private OnChildLayoutFinishListener mOnChildLayoutFinishListener;

    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);
        init(context, attrs);
    }

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

    private void init(Context context, AttributeSet attrs) {
        mScroller = new Scroller(context);
        mVelocityTracker = VelocityTracker.obtain();

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mMaxRowCount = typedArray.getInteger(R.styleable.FlowLayout_flow_max_row_count, Integer.MAX_VALUE);
        mHorizontalGravity = typedArray.getInteger(R.styleable.FlowLayout_flow_horizontal_gravity, HORIZONTAL_GRAVITY_LEFT);
        mHorizontalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_flow_horizontal_spacing, 0);
        mVerticalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_flow_vertical_spacing, 0);
        typedArray.recycle();
    }

    /**
     * 设置适配器
     *
     * @param flowLayoutAdapter {@link FlowLayoutAdapter} 子类对象
     */
    public void setAdapter(FlowLayoutAdapter flowLayoutAdapter) {
        if (flowLayoutAdapter != null) {
            this.mFlowLayoutAdapter = flowLayoutAdapter;
            flowLayoutAdapter.setFlowLayout(this);
            requestLayout();
        }
    }

    /**
     * 获取适配器,由方法 {@link #setAdapter(FlowLayoutAdapter)} 设置的
     *
     * @return 返回设置的适配器
     */
    public FlowLayoutAdapter getFlowLayoutAdapter() {
        return mFlowLayoutAdapter;
    }

    /**
     * 设置子控件布局完成监听
     *
     * @param onChildLayoutFinishListener {@link OnChildLayoutFinishListener}
     */
    public void setOnChildLayoutFinishListener(OnChildLayoutFinishListener onChildLayoutFinishListener) {
        this.mOnChildLayoutFinishListener = onChildLayoutFinishListener;
    }

    /**
     * 移除子控件布局完成监听
     */
    public void removeOnLayoutFinishListener() {
        this.mOnChildLayoutFinishListener = null;
    }

    /**
     * 设置子控件点击监听
     *
     * @param onItemClickListener {@link OnItemClickListener}
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.mOnItemClickListener = onItemClickListener;
    }

    /**
     * 设置最大显示行数
     *
     * @param maxRowCount 最大显示行数  小于0表示全部显示
     */
    public void setMaxRowCount(int maxRowCount) {
        if ((maxRowCount == mMaxRowCount)
                || (maxRowCount < 0 && mMaxRowCount < 0)
                || (mChildViewAllShow && maxRowCount > mTotalShowRowCount)
                || (mChildViewAllShow && maxRowCount < 0)) {
            return;
        }

        if (maxRowCount < 0) {
            maxRowCount = Integer.MAX_VALUE;
        }
        this.mMaxRowCount = maxRowCount;
        // 先滑动到顶部
        if (getScrollY() > 0) {
            scrollTo(0, 0);
        }
        requestLayout();
    }

    /**
     * 设置水平方向控件对齐方式,默认居左对齐({@link #HORIZONTAL_GRAVITY_LEFT})
     *
     * @param horizontalGravity {@link #HORIZONTAL_GRAVITY_LEFT}、
     *                          {@link #HORIZONTAL_GRAVITY_RIGHT}、
     *                          {@link #HORIZONTAL_GRAVITY_LEFT_RIGHT}、
     *                          {@link #HORIZONTAL_GRAVITY_CENTER}
     */
    public void setHorizontalGravity(int horizontalGravity) {
        if (this.mHorizontalGravity != horizontalGravity) {
            this.mHorizontalGravity = horizontalGravity;
            requestLayout();
        }
    }

    /**
     * 设置子控件之间的间距
     *
     * @param horizontalSpacing 水平方向间距 dp
     * @param verticalSpacing   竖直方向间距 dp
     */
    public void setSpacing(int horizontalSpacing, int verticalSpacing) {
        if (horizontalSpacing < 0 || verticalSpacing < 0) return;

        horizontalSpacing = dip2px(getContext(), horizontalSpacing);
        verticalSpacing = dip2px(getContext(), verticalSpacing);
        if (this.mHorizontalSpacing != horizontalSpacing || this.mVerticalSpacing != verticalSpacing) {
            this.mHorizontalSpacing = horizontalSpacing;
            this.mVerticalSpacing = verticalSpacing;
            requestLayout();
        }
    }

    /**
     * 滚动到顶部
     *
     * @param animation true:使用动画滚动  false:不使用动画
     */
    public void scrollToTop(boolean animation) {
        int scrollY = getScrollY();
        if (scrollY > 0) {
            if (animation) {
                smallScrollToPosition(scrollY, -scrollY);
            } else {
                scrollTo(0, 0);
            }
        }
    }

    /**
     * 滚动到底部
     *
     * @param animation true:使用动画滚动  false:不使用动画
     */
    public void scrollToBottom(boolean animation) {
        int scrollY = getScrollY();
        if (mMaxScrollY > scrollY) {
            if (animation) {
                smallScrollToPosition(scrollY, mMaxScrollY - scrollY);
            } else {
                scrollTo(0, mMaxScrollY);
            }
        }
    }

    /**
     * 滚动到指定位置
     *
     * @param animation position:需要滚动到的位置
     * @param animation true:使用动画滚动  false:不使用动画
     */
    public void scrollToPosition(int position, boolean animation) {
        if (position <= 0) {
            scrollToTop(animation);
        } else if (position >= mMaxScrollY) {
            scrollToBottom(animation);
        } else {
            if (animation) {
                int scrollY = getScrollY();
                smallScrollToPosition(scrollY, position - scrollY);
            } else {
                scrollTo(0, position);
            }
        }
    }

    /**
     * 滚动到指定行数
     *
     * @param animation rowNumber:需要滚动到的行数值
     * @param animation true:使用动画滚动  false:不使用动画
     */
    public void scrollToRowNumber(int rowNumber, boolean animation) {
        if (rowNumber <= 0) {
            scrollToTop(animation);
        } else if (rowNumber >= mTotalShowRowCount) {
            scrollToBottom(animation);
        } else {
            int position = 0;
            // 根据行数计算 position
            for (RowChildViewInfo rowChildViewInfo : mRowChildViewList) {
                position += rowChildViewInfo.rowHeight;
                if (rowChildViewInfo.rowNumber == rowNumber) {
                    break;
                }
            }
            scrollToPosition(position, animation);
        }
    }

    /**
     * 竖直方向平滑滚动方法
     *
     * @param startY 开始位置
     * @param dy     移动距离
     */
    private void smallScrollToPosition(int startY, int dy) {
        mScroller.startScroll(0, startY, 0, dy, Math.min(600, Math.max(300, Math.abs(dy))));
        postInvalidate();
    }

    /**
     * 获取显示的行数。<br/>
     * <b>重点注意:不要直接调用,而要在 {@link #setOnChildLayoutFinishListener(OnChildLayoutFinishListener)}
     * 回调中调用才能保证结果的正确性。</b><br/><br/>
     * 注意:当调用 {@link #setMaxRowCount(int)} 方法设置了最大行数时,<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     * 可能小于最大行数:当设置的最大行数 > 全部显示完成所需的行数时;<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     * 或者等于最大行数:当设置的最大行数 <= 全部显示完成所需的行数时。
     *
     * @return 显示的行数
     */
    public int getShowRowCount() {
        return mTotalShowRowCount;
    }

    /**
     * 是否所有的子控件都显示了。<br/>
     * <b>重点注意:不要直接调用,而要在 {@link #setOnChildLayoutFinishListener(OnChildLayoutFinishListener)}
     * 回调中调用才能保证结果的正确性。</b><br/><br/>
     * 注意:当调用 {@link #setMaxRowCount(int)} 方法设置了最大行数时,可能并非所有子控件都显示完全了。
     *
     * @return true:所有子控件都显示出来了   false:还有子控件没有显示出来
     */
    public boolean isChildViewAllShow() {
        return mChildViewAllShow;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        removeAllViews();
        mRowChildViewList.clear();
        mShowChildViewCount = 0;
        mTotalShowRowCount = 0;

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

        if (mFlowLayoutAdapter == null || mMaxRowCount == 0) {
            // 确定高度
            if (heightMode == MeasureSpec.EXACTLY) {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
            } else {
                heightMeasureSpec = MeasureSpec.makeMeasureSpec(getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
            }
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        // 内容显示宽度和高度
        mViewContentWidth = widthSize - getPaddingLeft() - getPaddingRight();
        mViewContentHeight = heightSize - getPaddingTop() - getPaddingBottom();

        // 所有孩子控件都完全显示需要的高度,默认加上顶部的 padding 值
        int flowLayoutReallyHeight = getPaddingTop();
        int childCount = mFlowLayoutAdapter.getItemCount();
        if (childCount > 0) {
            // 当前行已使用的宽度
            int currentRowWidth = 0;
            // 当前子控件所需要的水平方向间距,因为间距数 = 水平方向一行子控件个数 - 1(最后一个没有间距)
            // 本来是每行除最后一个之外,每个后面有间距;这里转换一下,变成除了第一个之外,后面每个在前面加一个间距
            int currentHorizontalSpacing = 0;
            // 当前行的高度,以一行中最大高度的子控件高度为行高
            int currentRowMaxHeight = 0;
            // 总行数
            mTotalShowRowCount = 1;
            // 显示的子控件数
            mShowChildViewCount = 0;

            List<ChildViewInfo> mChildViewList = new ArrayList<>();
            for (int i = 0; i < childCount; i++) {
                View childView = mFlowLayoutAdapter.createView(getContext(), this, i);
                if (childView.getVisibility() == View.GONE) {
                    continue;
                }

                addView(childView);

                measureChild(childView, widthMeasureSpec, heightMeasureSpec);

                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
                int childViewLeftMargin = 0;
                int childViewTopMargin = 0;
                int childViewRightMargin = 0;
                int childViewBottomMargin = 0;
                if (marginLayoutParams != null) {
                    childViewLeftMargin = marginLayoutParams.leftMargin;
                    childViewTopMargin = marginLayoutParams.topMargin;
                    childViewRightMargin = marginLayoutParams.rightMargin;
                    childViewBottomMargin = marginLayoutParams.bottomMargin;
                }

                int childViewWidth = childView.getMeasuredWidth();
                int childViewHeight = childView.getMeasuredHeight();
                // 计算当前行已使用的宽度
                currentRowWidth += childViewWidth + childViewLeftMargin + childViewRightMargin + currentHorizontalSpacing;
                // 取一行最大高度为行高
                currentRowMaxHeight = Math.max(childViewHeight + childViewTopMargin + childViewBottomMargin, currentRowMaxHeight);
                // 换行
                if (currentRowWidth > mViewContentWidth) {
                    // 增加上一行高度
                    flowLayoutReallyHeight += currentRowMaxHeight;
                    // 组合成新的行对象信息
                    RowChildViewInfo rowChildViewInfo = new RowChildViewInfo();
                    rowChildViewInfo.rowChildViews = mChildViewList;
                    rowChildViewInfo.rowNumber = mTotalShowRowCount;
                    rowChildViewInfo.rowHeight = currentRowMaxHeight;
                    rowChildViewInfo.currentRowUsedWidth = currentRowWidth - (childViewWidth + childViewLeftMargin + childViewRightMargin + currentHorizontalSpacing);
                    mRowChildViewList.add(rowChildViewInfo);

                    // 换行时设置为0,因为间距数 = 水平方向一行子控件个数 - 1 (最后一个没有间距)
                    // 本来是每行除最后一个之外,每个后面有间距;这里转换一下,变成除了第一个之外,后面每个在前面加一个间距
                    currentHorizontalSpacing = 0;
                    // 计算新行已使用宽度
                    currentRowWidth = childViewWidth + childViewLeftMargin + childViewRightMargin + currentHorizontalSpacing;
                    // 新行高度
                    currentRowMaxHeight = childViewHeight + childViewTopMargin + childViewBottomMargin;
                    // 新行子控件集合
                    mChildViewList = new ArrayList<>();
                    // 总行数加1
                    mTotalShowRowCount += 1;
                    // 显示最大行数控制
                    if (mTotalShowRowCount > mMaxRowCount) {
                        // 超过最大行数的部分,减掉
                        mTotalShowRowCount -= 1;
                        currentRowMaxHeight = 0;
                        break;
                    }

                    // 增加竖直方向上的间距
                    flowLayoutReallyHeight += mVerticalSpacing;
                }

                // 确定当前子控件所在的位置
                ChildViewInfo childViewInfo = new ChildViewInfo(childView, mTotalShowRowCount, i);
                childViewInfo.right = currentRowWidth - childViewRightMargin + getPaddingLeft();
                childViewInfo.left = childViewInfo.right - childViewWidth;
                childViewInfo.top = flowLayoutReallyHeight + childViewTopMargin;
                childViewInfo.bottom = childViewInfo.top + childViewHeight;
                mChildViewList.add(childViewInfo);
                mShowChildViewCount += 1;
                // 除了每行的第一个,后面的子控件都在前边加上一个水平间距
                currentHorizontalSpacing = mHorizontalSpacing;
            }
            // 加上最后一行高度
            flowLayoutReallyHeight += currentRowMaxHeight;
            // 加上最后一行的行对象信息
            RowChildViewInfo rowChildViewInfo = new RowChildViewInfo();
            rowChildViewInfo.rowChildViews = mChildViewList;
            rowChildViewInfo.rowNumber = mTotalShowRowCount;
            rowChildViewInfo.rowHeight = currentRowMaxHeight;
            rowChildViewInfo.currentRowUsedWidth = currentRowWidth;
            mRowChildViewList.add(rowChildViewInfo);
        }
        // 加上底部 padding 值
        flowLayoutReallyHeight += getPaddingBottom();
        mViewReallyHeight = flowLayoutReallyHeight;

        // 确定高度
        if (heightMode == MeasureSpec.EXACTLY) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
        } else {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(flowLayoutReallyHeight, MeasureSpec.EXACTLY);
        }

        // 滑动时,最大滑动偏移量
        mMaxScrollY = mViewReallyHeight - mViewContentHeight - getPaddingBottom() - getPaddingTop();
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 确定子控件是否已经全部显示了
        mChildViewAllShow = mFlowLayoutAdapter == null ? true : mFlowLayoutAdapter.getItemCount() == mShowChildViewCount;
        if (mRowChildViewList.isEmpty()) {
            if (mOnChildLayoutFinishListener != null)
                mOnChildLayoutFinishListener.onLayoutFinish(this, mShowChildViewCount);
            return;
        }

        // 水平方向不同对齐方式偏移量,默认居左对齐,不偏移
        int offsetX = 0;
        for (final RowChildViewInfo rowChildViewInfo : mRowChildViewList) {
            List<ChildViewInfo> rowChildViews = rowChildViewInfo.rowChildViews;
            if (mHorizontalGravity == HORIZONTAL_GRAVITY_RIGHT) {
                // 居右对齐
                offsetX = mViewContentWidth - rowChildViewInfo.currentRowUsedWidth;
            } else if (mHorizontalGravity == HORIZONTAL_GRAVITY_LEFT_RIGHT) {
                // 左右两端对齐
                if (rowChildViews.size() > 1) {
                    offsetX = (mViewContentWidth - rowChildViewInfo.currentRowUsedWidth) / (rowChildViews.size() - 1);
                } else {
                    offsetX = 0;
                }
            } else if (mHorizontalGravity == HORIZONTAL_GRAVITY_CENTER) {
                // 居中对齐
                offsetX = (mViewContentWidth - rowChildViewInfo.currentRowUsedWidth) / 2;
            }

            for (int i = 0; i < rowChildViews.size(); i++) {
                final ChildViewInfo childViewInfo = rowChildViews.get(i);
                if (mHorizontalGravity == HORIZONTAL_GRAVITY_LEFT_RIGHT) {
                    // 左右两端对齐,需要特殊处理
                    childViewInfo.onLayout(offsetX * i);
                } else {
                    childViewInfo.onLayout(offsetX);
                }
                childViewInfo.addClickListener(mOnItemClickListener, this, mFlowLayoutAdapter);
            }
        }
        if (mOnChildLayoutFinishListener != null)
            mOnChildLayoutFinishListener.onLayoutFinish(this, mShowChildViewCount);
    }

    private float mInterceptDownX;
    // 获取TouchSlop值
    float mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mInterceptDownX = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                float mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mInterceptDownX);
                // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    private int mTouchEventLastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mViewReallyHeight > mViewContentHeight + getPaddingTop() + getPaddingBottom()) {
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(event);
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mTouchEventLastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    }
                    int dy = mTouchEventLastY - y;
                    // 向上滑动
                    if (dy < 0) {
                        if (getScrollY() == 0) {
                            return super.onTouchEvent(event);
                        }
                        if (getScrollY() + dy < 0) {
                            scrollTo(0, 0);
                            return true;
                        }
                    } else {
                        // 向下滑动
                        if (getScrollY() == mMaxScrollY) {
                            return super.onTouchEvent(event);
                        }
                        if (getScrollY() + dy > mMaxScrollY) {
                            scrollTo(0, mMaxScrollY);
                            return true;
                        }
                    }
                    scrollBy(0, dy);
                    mTouchEventLastY = y;
                    break;
                case MotionEvent.ACTION_UP:
                    mVelocityTracker.computeCurrentVelocity(1000);
                    int initialVelocity = (int) mVelocityTracker.getYVelocity();
                    if (Math.abs(initialVelocity) > 200) {
                        // 由于坐标轴正方向问题,要加负号。
                        mScroller.fling(0, getScrollY(), 0, -initialVelocity, 0, 0, 0, 10000);
                    }

                    if (mVelocityTracker != null) {
                        mVelocityTracker.clear();
                        mVelocityTracker.recycle();
                        mVelocityTracker = null;
                    }
                    break;
            }
            postInvalidate();
            return true;
        } else {
            return super.onTouchEvent(event);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int currY = mScroller.getCurrY();
            // 快速滑动边界判断
            if (currY > mMaxScrollY) {
                currY = mMaxScrollY;
                scrollTo(0, currY);
                mScroller.abortAnimation();
            }
            scrollTo(0, currY);
            postInvalidate();
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    private int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 每一行信息
     */
    private static class RowChildViewInfo {
        private int currentRowUsedWidth; // 行使用宽度
        private int rowNumber; // 行号
        private int rowHeight; // 行高
        private List<ChildViewInfo> rowChildViews; // 行内子控件列表
    }

    /**
     * 子控件信息
     */
    private static class ChildViewInfo {
        private View childView;
        private int left;
        private int top;
        private int right;
        private int bottom;

        private int rowNumber; // 所在行位置
        private int position;  // 在父控件中的位置

        private ChildViewInfo(View childView, int rowNumber, int position) {
            this.childView = childView;
            this.rowNumber = rowNumber;
            this.position = position;
        }

        private void onLayout(int offsetX) {
            childView.layout(left + offsetX, top, right + offsetX, bottom);
        }

        private void addClickListener(final OnItemClickListener onItemClickListener,
                                      final FlowLayout flowLayout,
                                      final FlowLayoutAdapter flowLayoutAdapter) {
            childView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.onItemClick(flowLayout, flowLayoutAdapter,
                                rowNumber, position);
                    }
                }
            });
        }
    }

    /**
     * 子控件点击监听
     */
    public interface OnItemClickListener {
        /**
         * 点击子控件回调方法
         *
         * @param flowLayout {@link FlowLayout} 控件对象
         * @param adapter    {@link FlowLayoutAdapter} 对象
         * @param rowNumber  所在行,行号从 1 开始
         * @param position   在父控件的位置,位置从 0 开始
         */
        void onItemClick(FlowLayout flowLayout, FlowLayoutAdapter adapter, int rowNumber, int position);
    }

    /**
     * 孩子控件布局完成监听
     */
    public interface OnChildLayoutFinishListener {
        /**
         * @param flowLayout     {@link FlowLayout} 控件对象
         * @param showChildCount 当前完成布局的孩子数(显示的孩子数)
         */
        void onLayoutFinish(FlowLayout flowLayout, int showChildCount);
    }
}

3、 默认的框架FlowLayoutAdapter

public abstract class FlowLayoutAdapter {
    private FlowLayout flowLayout;

    protected abstract View createView(Context context, FlowLayout flowLayout, int position);

    public abstract int getItemCount();

    public abstract Object getItem(int position);

    protected void setFlowLayout(FlowLayout flowLayout) {
        this.flowLayout = flowLayout;
    }

    public void notifyChange() {
        flowLayout.requestLayout();
    }

}
//自定义MainFlowLayoutAdapter  继承FlowLayoutAdapter 
public class MainFlowLayoutAdapter extends FlowLayoutAdapter {
    private List<String> dataList;
    private int mCheckedPosition = -1;

    public MainFlowLayoutAdapter(List<String> datas) {
        this.dataList = datas;
    }


    @Override
    protected View createView(Context context, FlowLayout flowLayout, int position) {
        //布局
        View view = View.inflate(context, R.layout.item_flow_layout, null);
        TextView tvContent = view.findViewById(R.id.tv_content);
        tvContent.setText(dataList.get(position));
        return view;
    }

    @Override
    public int getItemCount() {
        return dataList == null ? 0 : dataList.size();
    }

    @Override
    public Object getItem(int position) {
        return dataList.get(position);
    }

    public int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

4、使用:

  /**
     * 初始化流式布局
     */
    private fun initFlowLayout() {
        val flowLayoutAdapter = MainFlowLayoutAdapter(DataUtils.getDataList(10))
        //设置显示行数
        mViewBind.flowLayout.setAdapter(flowLayoutAdapter)
        mViewBind.flowLayout.setOnItemClickListener { flowLayout, adapter, rowNumber, position ->
            mKeyWord = "${adapter.getItem(position)}"
            mViewBind.edit.setText("${adapter.getItem(position)}")
            MyToash.Log(
                "search",
                "----:${adapter.getItem(position)}"
            )
        }
        mViewBind.tvSearchTitle.setOnClickListener {
            //动态设置有效,默认不生效
            mViewBind.flowLayout.setMaxRowCount(2)
        }
    }

5、代码设置

  • setMaxRowCount(int maxRowCount):设置最大显示行数,maxRowCount:最大显示行数 小于0表示全部显示
  • setHorizontalGravity(int horizontalGravity):设置水平方向控件对齐方式,默认居左对齐,取值:
    FlowLayout.HORIZONTAL_GRAVITY_LEFT :居左对齐,默认 FlowLayout.HORIZONTAL_GRAVITY_RIGHT : 居右对齐 FlowLayout.HORIZONTAL_GRAVITY_LEFT_RIGHT : 左右对齐/两端对齐 FlowLayout.HORIZONTAL_GRAVITY_CENTER : 居中对齐
  • setSpacing(int horizontalSpacing, int verticalSpacing):设置子控件之间的间距
  • scrollToTop(boolean animation):滚动到顶部,参数 true:使用动画滚动 false:不使用动画
  • scrollToBottom(boolean animation):滚动到底部,参数 true:使用动画滚动 false:不使用动画
  • scrollToPosition(int position, boolean animation):滚动到指定位置,参数 animation: true:使用动画滚动 false:不使用动画
  • scrollToRowNumber(int rowNumber, boolean animation):滚动到指定行数,参数 animation: true:使用动画滚动 false:不使用动画
  • setOnChildLayoutFinishListener(OnChildLayoutFinishListener onChildLayoutFinishListener):设置子控件布局完成监听
  • setOnItemClickListener(OnItemClickListener onItemClickListener):设置子控件点击监听
  • isChildViewAllShow():是否所有的子控件都显示了,需要setOnChildLayoutFinishListener(OnChildLayoutFinishListener) 回调中调用保证结果的正确
  • getShowRowCount():获取显示的行数,需要在 setOnChildLayoutFinishListener(OnChildLayoutFinishListener) 回调中调用保证结果的正确
// 设置子控件布局完成监听,在回调中调用 getShowRowCount() 和 isChildViewAllShow() 方法
    private void setFlowLayoutFinishListener() {
        flowLayout.setOnChildLayoutFinishListener((flowLayout, showChildCount) -> {
            Logger.i("子控件布局完成: 显示行数: " + flowLayout.getShowRowCount() + " ;子控件总数:"
                    + flowLayoutAdapter.getItemCount() + " ;显示子控件数: " + showChildCount
                    + " ;全部子控件是否显示完成: " + flowLayout.isChildViewAllShow());
        });
    }

属性控制

<declare-styleable name="FlowLayout">
    <!-- 最大显示行数 -->
    <attr name="flow_max_row_count" format="integer" />
    <!-- 每一行水平方向对齐方式 -->
    <attr name="flow_horizontal_gravity" format="enum">
        <!-- 每一行水平方向对齐方式:左对齐,默认 -->
        <enum name="left" value="0" />
        <!-- 每一行水平方向对齐方式:右对齐 -->
        <enum name="right" value="1" />
        <!-- 每一行水平方向对齐方式:左右对齐 -->
        <enum name="left_right" value="2" />
        <!-- 每一行水平方向对齐方式:居中对齐 -->
        <enum name="center" value="3" />
    </attr>
    <!-- 水平方向间距 -->
    <attr name="flow_horizontal_spacing" format="dimension" />
    <!-- 竖直方向间距 -->
    <attr name="flow_vertical_spacing" format="dimension" />
</declare-styleable>

链接:https://www.jianshu.com/p/55b7ad65abd4

相关文章

网友评论

      本文标题:Android FlowLayout 流式布局

      本文链接:https://www.haomeiwen.com/subject/kxndddtx.html