天天看点

Android 实现流式布局的几种方式和FlexboxLayout的使用

自定义流式布局FlowLayout(可指定显示行数)

FlowLayout

/**
 * Cerated by xiaoyehai
 * Create date : 2021/1/11 15:02
 * description :自定义流式布局(可指定显示行数)
 */
public class FlowLayout extends LinearLayout {

    /**
     * 默认间距
     */
    public static final int DEFAULT_SPACING = AppUtils.dp2px(10);

    /**
     * 横向间隔
     */
    private int mHorizontalSpacing = DEFAULT_SPACING;

    /**
     * 纵向间隔
     */
    private int mVerticalSpacing = DEFAULT_SPACING;

    /**
     * 是否需要布局,只用于第一次
     */
    boolean mNeedLayout = true;

    /**
     * 每一行是否平分空间:将剩余空间平均分配给每个子控件
     */
    private boolean isAverageInRow = false;

    /**
     * 当前行已用的宽度,由子View宽度加上横向间隔
     */
    private int mUsedWidth = 0;

    /**
     * 行的集合
     */
    private final List<Line> mLines = new ArrayList<>();

    /**
     * 行对象
     */
    private Line mLine = null;

    /**
     * 最大的行数
     */
    private int mMaxLinesCount = Integer.MAX_VALUE;

    public FlowLayout(Context context) {
        this(context, null);
    }

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

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

    /**
     * 设置横向间隔
     *
     * @param spacing
     */
    public void setHorizontalSpacing(int spacing) {
        if (mHorizontalSpacing != spacing) {
            mHorizontalSpacing = spacing;
            requestLayoutInner();
        }
    }

    /**
     * 设置纵向间隔
     *
     * @param spacing
     */
    public void setVerticalSpacing(int spacing) {
        if (mVerticalSpacing != spacing) {
            mVerticalSpacing = spacing;
            requestLayoutInner();
        }
    }

    /**
     * 设置最大行数
     *
     * @param count
     */
    public void setMaxLines(int count) {
        if (mMaxLinesCount != count) {
            mMaxLinesCount = count;
            requestLayoutInner();
        }
    }

    /**
     * 每一行是否平分空间
     *
     * @param isAverageInRow
     */
    public void setIsAverageInRow(boolean isAverageInRow) {
        if (isAverageInRow != isAverageInRow) {
            this.isAverageInRow = isAverageInRow;
            requestLayoutInner();
        }
    }

    private void requestLayoutInner() {
        AppUtils.runOnUIThread(new Runnable() {
            @Override
            public void run() {
                requestLayout();
            }
        });

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取自定义控件宽度
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();

        //获取自定义控件高度
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

        //获取自定义控件的宽高测量模式
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        restoreLine();// 还原数据,以便重新记录

        //获取子控件数量
        final int count = getChildCount();

        //测量每个子控件的大小,决定什么时候需要换行
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }

            //如果父控件是确定模式,子控件就包裹内容,否则子控件模式和父控件一样
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth,
                    modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight,
                    modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight);

            //测量子控件
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            //如果当前行对象为空,初始化一个行对象
            if (mLine == null) {
                mLine = new Line();
            }

            //获取子控件宽度
            int childWidth = child.getMeasuredWidth();

            mUsedWidth += childWidth;//  //当前已使用宽度增加一个子控件

            //是否超出边界
            if (mUsedWidth <= sizeWidth) { //没有超出边界
                mLine.addView(child);// //给当前行添加一个子控件
                mUsedWidth += mHorizontalSpacing;// 加上间隔
                if (mUsedWidth >= sizeWidth) {   //增加水平间距后,超出边界,需要换行
                    if (!newLine()) {
                        //创建行失败,表示已经100行,不能在创建了,结束循环,不再添加
                        break;
                    }
                }
            } else {///超出边界
                if (mLine.getViewCount() == 0) { //1.当前没有控件,一添加控件就超出边界(子控件很长)
                    mLine.addView(child);//强制添加到当前行
                    if (!newLine()) {// 换行
                        break;
                    }

                } else {
                    //2.当前有控件,一添加控件就超出边界
                    //先还行,再添加
                    if (!newLine()) {// 换行
                        break;
                    }
                    // 在新的一行,不管是否超过长度,先加上去,因为这一行一个child都没有,所以必须满足每行至少有一个child
                    mLine.addView(child);
                    mUsedWidth += childWidth + mHorizontalSpacing;
                }
            }
        }

        //保存最后一行到集合
        if (mLine != null && mLine.getViewCount() > 0 && !mLines.contains(mLine)) {
            // 由于前面采用判断长度是否超过最大宽度来决定是否换行,则最后一行可能因为还没达到最大宽度,所以需要验证后加入集合中
            mLines.add(mLine);
        }

        //控件整体宽度
        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);

        // 控件整体高度
        int totalHeight = 0;

        final int linesCount = mLines.size();
        for (int i = 0; i < linesCount; i++) {// 加上所有行的高度
            totalHeight += mLines.get(i).mHeight;
        }

        totalHeight += mVerticalSpacing * (linesCount - 1);// 加上所有间隔的高度
        totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding

        //根据最新宽高测量整体布局的大小
        // 设置布局的宽高,宽度直接采用父view传递过来的最大宽度,而不用考虑子view是否填满宽度,因为该布局的特性就是填满一行后,再换行
        // 高度根据设置的模式来决定采用所有子View的高度之和还是采用父view传递过来的高度
        setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (!mNeedLayout || changed) {// 没有发生改变就不重新布局
            mNeedLayout = false;
            int left = getPaddingLeft();// 获取最初的左上点
            int top = getPaddingTop();

            //遍历所以行对象,设置每行位置
            final int linesCount = mLines.size();
            for (int i = 0; i < linesCount; i++) {
                final Line oneLine = mLines.get(i);
                oneLine.layoutView(left, top);// 布局每一行
                top += oneLine.mHeight + mVerticalSpacing;// 更新top值,为下一行的top赋值
            }
        }
    }

    /**
     * 还原所有数据
     */
    private void restoreLine() {
        mLines.clear();
        mLine = new Line();
        mUsedWidth = 0;
    }

    /**
     * 换行方法
     */
    private boolean newLine() {
        mLines.add(mLine); //把上一行添加到集合
        if (mLines.size() < mMaxLinesCount) {
            //如果可以继续添加行
            mLine = new Line();
            mUsedWidth = 0; //宽度清零
            return true;
        }
        return false;
    }

    // ==========================================================================
    // Inner/Nested Classes
    // ==========================================================================

    /**
     * 代表着一行,封装了一行所占高度,该行子View的集合,以及所有View的宽度总和
     */
    class Line {
        int mWidth = 0;// 该行中所有的子View累加的宽度
        int mHeight = 0;// 该行中所有的子View中高度最高的那个子View的高度

        /**
         * 一行子控件的集合
         */
        List<View> views = new ArrayList<View>();

        /**
         * 添加一个子控件
         *
         * @param view
         */
        public void addView(View view) {// 往该行中添加一个
            views.add(view);
            mWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            mHeight = mHeight < childHeight ? childHeight : mHeight;// 高度等于一行中最高的View
        }

        /**
         * 获取当前行子控件的个数
         *
         * @return
         */
        public int getViewCount() {
            return views.size();
        }

        /**
         * 摆放行对象
         *
         * @param l
         * @param t
         */
        public void layoutView(int l, int t) {// 布局
            int left = l;
            int top = t;
            int count = getViewCount();

            // 总宽度
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();

            // 剩余的宽度,是除了View和间隙的剩余空间
            //将剩余空间平均分配给每个子控件
            int surplusWidth = layoutWidth - mWidth - mHorizontalSpacing * (count - 1);

            if (surplusWidth >= 0) {// 剩余空间
                // 采用float类型数据计算后四舍五入能减少int类型计算带来的误差
                int splitSpacing = (int) (surplusWidth / count + 0.5); //平均每个控件分配的大小

                for (int i = 0; i < count; i++) {

                    final View view = views.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();

                    // 当控件比较矮时,需要居中展示
                    // 计算出每个View的顶点,是由最高的View和该View高度的差值除以2
                    int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);
                    if (topOffset < 0) {
                        topOffset = 0;
                    }
                    // 把剩余空间平均到每个View上
                    if (isAverageInRow) {
                        childWidth = childWidth + splitSpacing;
                    }
                    view.getLayoutParams().width = childWidth;
                    if (isAverageInRow) {
                        if (splitSpacing > 0) {// View的长度改变了,需要重新measure
                            int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
                            int heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
                            view.measure(widthMeasureSpec, heightMeasureSpec);
                        }
                    }
                    // 布局View
                    view.layout(left, top + topOffset, left + childWidth, top + topOffset + childHeight);
                    left += childWidth + mHorizontalSpacing; // 为下一个View的left赋值
                }
            } else {
                if (count == 1) {
                    //没有剩余空间
                    //这个控件很长,占满整行
                    View view = views.get(0);
                    view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());
                } else {
                    // 走到这里来,应该是代码出问题了,目前按照逻辑来看,是不可能走到这一步
                }
            }
        }
    }
}

           

使用

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.FlowLayoutActivity">

    <com.zly.flowlayoutdemo.widget.FlowLayout
        android:id="@+id/flowLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
           

FlowLayoutActivity

public class FlowLayoutActivity extends AppCompatActivity {

    private FlowLayout mFlowLayout;

    private List<String> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flow_layout);

        mFlowLayout = (FlowLayout) findViewById(R.id.flowLayout);

        loadData();

        staticAddView();

        //动态添加View
        //dynamicAddView();
    }

    private void staticAddView() {
        int padding10 = AppUtils.dp2px(10);
        mFlowLayout.setPadding(padding10, padding10, padding10, padding10);
        for (int i = 0; i < mDatas.size(); i++) {
            TextView textView = (TextView) LayoutInflater.from(this).inflate(R.layout.item_search_history, mFlowLayout, false);
            String s = mDatas.get(i);
            textView.setText(s);
            mFlowLayout.addView(textView);
            textView.setOnClickListener(v -> Toast.makeText(FlowLayoutActivity.this, s, Toast.LENGTH_SHORT).show());
        }
        mFlowLayout.setMaxLines(5); //设置最大行数
    }


    private void dynamicAddView() {
        int padding6 = AppUtils.dp2px(6);
        int padding8 = AppUtils.dp2px(8);
        int padding10 = AppUtils.dp2px(10);
        int padding12 = AppUtils.dp2px(12);
        mFlowLayout.setPadding(padding10, padding10, padding10, padding10);
        mFlowLayout.setHorizontalSpacing(padding8); //水平间距
        mFlowLayout.setVerticalSpacing(padding10);  //竖直边距

        for (int i = 0; i < mDatas.size(); i++) {
            String key = mDatas.get(i);
            TextView textView = new TextView(this);
            textView.setText(key);

            textView.setTextColor(Color.parseColor("#333333"));
            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
            textView.setPadding(padding12, padding6, padding12, padding6);
            textView.setGravity(Gravity.CENTER);
            textView.setBackgroundResource(R.drawable.shape_search_lable_bg);
            mFlowLayout.addView(textView);

            textView.setOnClickListener(v -> Toast.makeText(FlowLayoutActivity.this, key, Toast.LENGTH_SHORT).show());
        }

        mFlowLayout.setMaxLines(5); //设置最大行数
    }

    private void loadData() {
        mDatas = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            if (i % 2 == 0) {
                mDatas.add("数据" + i);
            } else {
                mDatas.add("数据数据数据" + i);
            }
        }
    }
}
           

自定义流式布局ZFlowLayout(可以设置展开和收起按钮)

仿淘宝历史搜索效果:

Android 实现流式布局的几种方式和FlexboxLayout的使用

ZFlowLayout

/**
 * 实现流式布局,自定义ViewGroup,实现标签等 - 单行垂直居中 或 水平平分
 * <p>
 * 可以设置展开和收起按钮
 */
public class ZFlowLayout extends ViewGroup {
    /**
     * 存储每一行的剩余的空间
     */
    private List<Integer> lineSpaces = new ArrayList<>();
    /**
     * 存储每一行的高度
     */
    private List<Integer> lineHeights = new ArrayList<>();
    /**
     * 存储每一行的view
     */
    private List<List<View>> lineViews = new ArrayList<>();
    /**
     * 提供添加view
     */
    private List<View> children = new ArrayList<>();

    /**
     * 每一行是否平分空间
     */
    private boolean isAverageInRow = false;

    /**
     * 每一列是否垂直居中
     */
    private boolean isAverageInColumn = true;

    private int mLineCount = 0;//行数

    private int mTwoLineViewCount = 0;//前两行里面view的个数

    //展开是最多显示几行
    private int mExpandLineCount = 5;

    //展开时显示view的个数
    private int mExpandLineViewCount = 0;

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

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

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

    public int getLineCount() {
        return mLineCount;
    }

    public int getTwoLineViewCount() {
        return mTwoLineViewCount;
    }

    public int getExpandLineViewCount() {
        return mExpandLineViewCount;
    }

    /**
     * 设置是否每列垂直居中
     *
     * @param averageInColumn 是否垂直居中
     */
    public void setAverageInColumn(boolean averageInColumn) {
        if (isAverageInColumn != averageInColumn) {
            isAverageInColumn = averageInColumn;
            requestLayout();
        }
    }

    /**
     * 设置是否每一行居中
     *
     * @param averageInRow 是否水平平分
     */
    public void setAverageInRow(boolean averageInRow) {
        if (isAverageInRow != averageInRow) {
            isAverageInRow = averageInRow;
            requestLayout();
        }
    }

    /**
     * 动态添加view
     */
    public void setChildren(List<View> children) {
        if (children == null)
            return;
        this.children = children;
        mLineCount = 0;
        mTwoLineViewCount = 0;
        mExpandLineViewCount = 0;
        this.removeAllViews();
        for (int i = 0; i < children.size(); i++) {
            this.addView(children.get(i));
            if (children.get(i) instanceof TextView) {
                int finalI = i;
                children.get(i).setOnClickListener(v -> {
                    if (mOnTagClickListener != null) {
                        mOnTagClickListener.onTagClick(children.get(finalI), finalI);
                    }
                });
            }
        }
    }

    /**
     * 重新方法用来获取子view的margin值
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //清除记录数据
        lineSpaces.clear();
        lineHeights.clear();
        lineViews.clear();
        //测量view的宽高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
        //计算children的数量
        int count = this.getChildCount();
        //统计子view总共高度
        int childrenTotalHeight = 0;

        //一行中剩余的空间
        int lineLeftSpace = 0;
        int lineRealWidth = 0;
        int lineRealHeight = 0;

        List<View> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //不可见的View不作处理
            if (child.getVisibility() == GONE)
                continue;
            //对子view进行测量
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //获取子view的间距
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            //获取view占据的空间大小
            int childViewWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
            int childViewHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;

            //            System.out.println("111111111 视图 " + " 子View宽度: " + childViewWidth + "  lineRealWidth:" + lineRealWidth + " lineRealHeight :" + lineRealHeight);

            if (childViewWidth + lineRealWidth <= viewWidth) {// 一行
                //已占用的空间
                lineRealWidth += childViewWidth;
                //剩余的空间
                lineLeftSpace = viewWidth - lineRealWidth;
                //一行的最大高度
                lineRealHeight = Math.max(lineRealHeight, childViewHeight);
                //将一行中的view加到同意个集合
                list.add(child);
            } else {// 下一行
                if (list.size() != 0) {
                    // 统计上一行的总高度
                    childrenTotalHeight += lineRealHeight;
                    //上一行的高度
                    lineHeights.add(lineRealHeight);
                    //上一行剩余的空间
                    lineSpaces.add(lineLeftSpace);
                    //将上一行的元素保存起来
                    lineViews.add(list);
                }
                //重置一行中已占用的空间
                lineRealWidth = childViewWidth;
                //重置一行中剩余的空间
                lineLeftSpace = viewWidth - lineRealWidth;
                //重置一行中的高度
                lineRealHeight = childViewHeight;
                //更换新的集合存储下一行的元素
                list = new ArrayList<>();
                list.add(child);
            }

            if (i == count - 1) {// 最后一个元素
                childrenTotalHeight += lineRealHeight;
                // 将最后一行的信息保存下来
                lineViews.add(list);
                lineHeights.add(lineRealHeight);
                lineSpaces.add(lineLeftSpace);
            }
        }
        // 宽度可以不用考虑 主要考虑高度
        if (heightMode == MeasureSpec.EXACTLY) {
            setMeasuredDimension(viewWidth, viewHeight);
        } else {
            setMeasuredDimension(viewWidth, childrenTotalHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // View最开始左边
        int viewLeft = 0;
        // View最开始上边
        int viewTop = 0;

        // 每一个view layout的位置
        int vl;
        int vt;
        int vr;
        int vb;

        // 每一行中每一个view多平分的空间
        float averageInRow;
        // 每一列中每一个view距离顶部的高度
        float averageInColumn;

        // 列数
        int columns = lineViews.size();
        mLineCount = columns;


        for (int i = 0; i < columns; i++) {
            // 该行剩余的空间
            int lineSpace = lineSpaces.get(i);
            // 该行的高度
            int lineHeight = lineHeights.get(i);
            // 该行的所有元素
            List<View> list = lineViews.get(i);
            // 每一行的view的个数
            int rows = list.size();
            if (i == 0 || i == 1) {
                mTwoLineViewCount = mTwoLineViewCount + rows;
            }

            if (i < mExpandLineCount) {
                mExpandLineViewCount = mExpandLineViewCount + rows;
            }

            // view layout的位置
            // 每一行中每一个view多平分的空间<一行只有一个不管>
            if (isAverageInRow && rows > 1) {
                averageInRow = lineSpace * 1.0f / (rows + 1);
            } else {
                averageInRow = 0;
            }

            // 获取View的间距属性
            MarginLayoutParams params;
            for (int j = 0; j < rows; j++) {
                // 对应位置的view元素
                View child = list.get(j);
                params = (MarginLayoutParams) child.getLayoutParams();
                // 是否计算每一列中的元素垂直居中的时候多出的距离
                if (isAverageInColumn && rows > 1) {
                    averageInColumn = (lineHeight - child.getMeasuredHeight() - params.topMargin - params.bottomMargin) / 2;
                } else {
                    averageInColumn = 0;
                }

                // 左边位置 =起始位置+view左间距+多平分的空间
                vl = (int) (viewLeft + params.leftMargin + averageInRow);
                // 上面的位置 = 起始位置+view上间距+多平分的空间
                vt = (int) (viewTop + params.topMargin + averageInColumn);
                vr = vl + child.getMeasuredWidth();
                vb = vt + child.getMeasuredHeight();
                child.layout(vl, vt, vr, vb);
                viewLeft += child.getMeasuredWidth() + params.leftMargin + params.rightMargin + averageInRow;
            }
            viewLeft = 0;
            viewTop += lineHeight;
        }
    }


    private OnTagClickListener mOnTagClickListener;

    public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
        mOnTagClickListener = onTagClickListener;
    }

    public interface OnTagClickListener {
        void onTagClick(View view, int position);
    }

}

           

使用

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ZFlowLayoutActivity">


    <com.zly.flowlayoutdemo.widget.ZFlowLayout
        android:id="@+id/zFlowLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="10dp"
        android:layout_marginTop="20dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
           

ZFlowLayoutActivity

public class ZFlowLayoutActivity extends AppCompatActivity {

    private ZFlowLayout mZFlowLayout;

    private List<String> mDatas;

    private List<View> mViewList = new ArrayList<>();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_z_flow_layout);

        mZFlowLayout = (ZFlowLayout) findViewById(R.id.zFlowLayout);

        loadData();

        //ZFlowLayout的使用:可添加展开和收起按钮
        initZFlowLayout();
    }


    private void initZFlowLayout() {
        mViewList.clear();
        for (int i = 0; i < mDatas.size(); i++) {
            TextView textView = (TextView) LayoutInflater.from(this).inflate(R.layout.item_search_history, mZFlowLayout, false);
            textView.setText(mDatas.get(i));
            mViewList.add(textView);
        }
        mZFlowLayout.setChildren(mViewList);

        mZFlowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mZFlowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int lineCount = mZFlowLayout.getLineCount();  //行数
                int twoLineViewCount = mZFlowLayout.getTwoLineViewCount();  //前两行里面view的个数
                int expandLineViewCount = mZFlowLayout.getExpandLineViewCount(); ///展开时显示view的个数
                if (lineCount > 2) {  //默认展示2行,其余折叠收起,最多展示5行
                    initIvClose(twoLineViewCount, expandLineViewCount);
                }
            }
        });

        mZFlowLayout.setOnTagClickListener((view, position) -> {
            //点击了
            Toast.makeText(this, mDatas.get(position), Toast.LENGTH_SHORT).show();
        });

    }

    private void initIvClose(int twoLineViewCount, int expandLineViewCount) {
        mViewList.clear();
        for (int i = 0; i < twoLineViewCount; i++) {
            TextView textView = (TextView) LayoutInflater.from(this).inflate(R.layout.item_search_history, mZFlowLayout, false);
            textView.setText(mDatas.get(i));
            mViewList.add(textView);
        }

        //展开按钮
        ImageView imageView = (ImageView) LayoutInflater.from(this).inflate(R.layout.item_search_history_img, mZFlowLayout, false);
        imageView.setImageResource(R.mipmap.search_close);
        imageView.setOnClickListener(v -> {
            initIvOpen(twoLineViewCount, expandLineViewCount);

        });
        mViewList.add(imageView);
        mZFlowLayout.setChildren(mViewList);
        mZFlowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mZFlowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int lineCount = mZFlowLayout.getLineCount();
                int twoLineViewCount = mZFlowLayout.getTwoLineViewCount();
                if (lineCount > 2) {
                    initIvClose(twoLineViewCount - 1, mZFlowLayout.getExpandLineViewCount());
                }
            }
        });
    }

    private void initIvOpen(int twoLineViewCount, int expandLineViewCount) {
        mViewList.clear();

        /*for (int i = 0; i < mDatas.size(); i++) {
            TextView textView = (TextView) LayoutInflater.from(this).inflate(R.layout.item_search_history, mZFlowLayout, false);
            textView.setText(mDatas.get(i));
            mViewList.add(textView);
        }*/

        for (int i = 0; i < expandLineViewCount; i++) {
            TextView textView = (TextView) LayoutInflater.from(this).inflate(R.layout.item_search_history, mZFlowLayout, false);
            textView.setText(mDatas.get(i));
            mViewList.add(textView);
        }

        //收起按钮
        ImageView imageView = (ImageView) LayoutInflater.from(this).inflate(R.layout.item_search_history_img, mZFlowLayout, false);
        imageView.setImageResource(R.mipmap.search_open);
        imageView.setOnClickListener(v -> initIvClose(twoLineViewCount, expandLineViewCount));
        mViewList.add(imageView); //不需要的话可以不添加
        mZFlowLayout.setChildren(mViewList);
    }


    private void loadData() {
        mDatas = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            if (i % 2 == 0) {
                mDatas.add("数据" + i);
            } else {
                mDatas.add("数据数据数据" + i);
            }
        }
    }
}
           

使用RecyclerView实现流式布局

RvActivity

public class RvActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;

    private List<String> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rv);

        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);

        loadData();

        FlowLayoutManager flowLayoutManager = new FlowLayoutManager();
        mRecyclerView.setLayoutManager(flowLayoutManager);

        mRecyclerView.setAdapter(new CommomRvAdapter<String>(this, mDatas, R.layout.item_rv) {
            @Override
            protected void fillData(CommomRvViewHolder holder, int position, String s) {
                holder.setText(R.id.tv_label, s);
            }
        });

    }

    private void loadData() {
        mDatas = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            if (i % 2 == 0) {
                mDatas.add("数据" + i);
            } else {
                mDatas.add("数据数据数据" + i);
            }
        }
    }
}
           

FlowLayoutManager

自定义FlowLayoutManager,配合RecyclerView实现流式布局。

/**
 * 自定义FlowLayoutManager,配合RecyclerView实现流式布局
 */
public class FlowLayoutManager extends RecyclerView.LayoutManager {

    private static final String TAG = FlowLayoutManager.class.getSimpleName();

    final FlowLayoutManager self = this;

    protected int width, height;
    private int left, top, right;
    //最大容器的宽度
    private int usedMaxWidth;
    //竖直方向上的偏移量
    private int verticalScrollOffset = 0;

    public int getTotalHeight() {
        return totalHeight;
    }

    //计算显示的内容的高度
    protected int totalHeight = 0;
    private Row row = new Row();
    private List<Row> lineRows = new ArrayList<>();

    //保存所有的Item的上下左右的偏移量信息
    private SparseArray<Rect> allItemFrames = new SparseArray<>();

    public FlowLayoutManager() {
    }

    //设置主动测量规则,适应recyclerView高度为wrap_content
    @Override
    public boolean isAutoMeasureEnabled() {
        return true;
    }

    public int getRowCounts()  {
        return lineRows.size();
    }

    //每个item的定义
    public class Item {
        int useHeight;
        View view;

        public void setRect(Rect rect) {
            this.rect = rect;
        }

        Rect rect;

        public Item(int useHeight, View view, Rect rect) {
            this.useHeight = useHeight;
            this.view = view;
            this.rect = rect;
        }
    }

    //行信息的定义
    public class Row {
        public void setCuTop(float cuTop) {
            this.cuTop = cuTop;
        }

        public void setMaxHeight(float maxHeight) {
            this.maxHeight = maxHeight;
        }

        //每一行的头部坐标
        float cuTop;
        //每一行需要占据的最大高度
        float maxHeight;
        //每一行存储的item
        List<Item> views = new ArrayList<>();

        public void addViews(Item view) {
            views.add(view);
        }
    }

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    //该方法主要用来获取每一个item在屏幕上占据的位置
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        Log.d(TAG, "onLayoutChildren");
        totalHeight = 0;
        int cuLineTop = top;
        //当前行使用的宽度
        int cuLineWidth = 0;
        int itemLeft;
        int itemTop;
        int maxHeightItem = 0;
        row = new Row();
        lineRows.clear();
        allItemFrames.clear();
        removeAllViews();
        if (getItemCount() == 0) {
            detachAndScrapAttachedViews(recycler);
            verticalScrollOffset = 0;
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {
            return;
        }
        //onLayoutChildren方法在RecyclerView 初始化时 会执行两遍
        detachAndScrapAttachedViews(recycler);
        if (getChildCount() == 0) {
            width = getWidth();
            height = getHeight();
            left = getPaddingLeft();
            right = getPaddingRight();
            top = getPaddingTop();
            usedMaxWidth = width - left - right;
        }

        for (int i = 0; i < getItemCount(); i++) {
            Log.d(TAG, "index:" + i);
            View childAt = recycler.getViewForPosition(i);
            if (View.GONE == childAt.getVisibility()) {
                continue;
            }
            measureChildWithMargins(childAt, 0, 0);
            int childWidth = getDecoratedMeasuredWidth(childAt);
            int childHeight = getDecoratedMeasuredHeight(childAt);
            int childUseWidth = childWidth;
            int childUseHeight = childHeight;
            //如果加上当前的item还小于最大的宽度的话
            if (cuLineWidth + childUseWidth <= usedMaxWidth) {
                itemLeft = left + cuLineWidth;
                itemTop = cuLineTop;
                Rect frame = allItemFrames.get(i);
                if (frame == null) {
                    frame = new Rect();
                }
                frame.set(itemLeft, itemTop, itemLeft + childWidth, itemTop + childHeight);
                allItemFrames.put(i, frame);
                cuLineWidth += childUseWidth;
                maxHeightItem = Math.max(maxHeightItem, childUseHeight);
                row.addViews(new Item(childUseHeight, childAt, frame));
                row.setCuTop(cuLineTop);
                row.setMaxHeight(maxHeightItem);
            } else {
                //换行
                formatAboveRow();
                cuLineTop += maxHeightItem;
                totalHeight += maxHeightItem;
                itemTop = cuLineTop;
                itemLeft = left;
                Rect frame = allItemFrames.get(i);
                if (frame == null) {
                    frame = new Rect();
                }
                frame.set(itemLeft, itemTop, itemLeft + childWidth, itemTop + childHeight);
                allItemFrames.put(i, frame);
                cuLineWidth = childUseWidth;
                maxHeightItem = childUseHeight;
                row.addViews(new Item(childUseHeight, childAt, frame));
                row.setCuTop(cuLineTop);
                row.setMaxHeight(maxHeightItem);
            }
            //不要忘了最后一行进行刷新下布局
            if (i == getItemCount() - 1) {
                formatAboveRow();
                totalHeight += maxHeightItem;
            }

        }
        totalHeight = Math.max(totalHeight, getVerticalSpace());
        Log.d(TAG, "onLayoutChildren totalHeight:" + totalHeight);
        fillLayout(recycler, state);
    }

    //对出现在屏幕上的item进行展示,超出屏幕的item回收到缓存中
    private void fillLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (state.isPreLayout() || getItemCount() == 0) { // 跳过preLayout,preLayout主要用于支持动画
            return;
        }

        // 当前scroll offset状态下的显示区域
        Rect displayFrame = new Rect(getPaddingLeft(), getPaddingTop() + verticalScrollOffset,
                getWidth() - getPaddingRight(), verticalScrollOffset + (getHeight() - getPaddingBottom()));

        //对所有的行信息进行遍历
        for (int j = 0; j < lineRows.size(); j++) {
            Row row = lineRows.get(j);
            float lineTop = row.cuTop;
            float lineBottom = lineTop + row.maxHeight;
            //如果该行在屏幕中,进行放置item
//            if (lineTop < displayFrame.bottom && displayFrame.top < lineBottom) {
            List<Item> views = row.views;
            for (int i = 0; i < views.size(); i++) {
                View scrap = views.get(i).view;
                measureChildWithMargins(scrap, 0, 0);
                addView(scrap);
                Rect frame = views.get(i).rect;
                //将这个item布局出来
                layoutDecoratedWithMargins(scrap,
                        frame.left,
                        frame.top - verticalScrollOffset,
                        frame.right,
                        frame.bottom - verticalScrollOffset);
            }
//            } else {
//                //将不在屏幕中的item放到缓存中
//                List<Item> views = row.views;
//                for (int i = 0; i < views.size(); i++) {
//                    View scrap = views.get(i).view;
//                    removeAndRecycleView(scrap, recycler);
//                }
//            }
        }
    }

    /**
     * 计算每一行没有居中的viewgroup,让居中显示
     */
    private void formatAboveRow() {
        List<Item> views = row.views;
        for (int i = 0; i < views.size(); i++) {
            Item item = views.get(i);
            View view = item.view;
            int position = getPosition(view);
            //如果该item的位置不在该行中间位置的话,进行重新放置
            if (allItemFrames.get(position).top < row.cuTop + (row.maxHeight - views.get(i).useHeight) / 2) {
                Rect frame = allItemFrames.get(position);
                if (frame == null) {
                    frame = new Rect();
                }
                frame.set(allItemFrames.get(position).left, (int) (row.cuTop + (row.maxHeight - views.get(i).useHeight) / 2),
                        allItemFrames.get(position).right, (int) (row.cuTop + (row.maxHeight - views.get(i).useHeight) / 2 + getDecoratedMeasuredHeight(view)));
                allItemFrames.put(position, frame);
                item.setRect(frame);
                views.set(i, item);
            }
        }
        row.views = views;
        lineRows.add(row);
        row = new Row();
    }

    /**
     * 竖直方向需要滑动的条件
     *
     * @return
     */
    @Override
    public boolean canScrollVertically() {
        return true;
    }

    //监听竖直方向滑动的偏移量
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                                  RecyclerView.State state) {

        Log.d("TAG", "totalHeight:" + totalHeight);
        //实际要滑动的距离
        int travel = dy;

        //如果滑动到最顶部
        if (verticalScrollOffset + dy < 0) {//限制滑动到顶部之后,不让继续向上滑动了
            travel = -verticalScrollOffset;//verticalScrollOffset=0
        } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {//如果滑动到最底部
            travel = totalHeight - getVerticalSpace() - verticalScrollOffset;//verticalScrollOffset=totalHeight - getVerticalSpace()
        }

        //将竖直方向的偏移量+travel
        verticalScrollOffset += travel;

        // 平移容器内的item
        offsetChildrenVertical(-travel);
        fillLayout(recycler, state);
        return travel;
    }

    private int getVerticalSpace() {
        return self.getHeight() - self.getPaddingBottom() - self.getPaddingTop();
    }

    public int getHorizontalSpace() {
        return self.getWidth() - self.getPaddingLeft() - self.getPaddingRight();
    }

}
           

FlexboxLayout的使用

Github地址

依赖

dependencies {
    implementation 'com.google.android:flexbox:2.0.1'
}
           

什么是FlexboxLayout

那么FlexboxLayout 它到底是个什么东西呢?看一下Github对这个库的介绍:FlexboxLayout is a library project which brings the similar capabilities of CSS Flexible Box Layout Module to Android. 意思是:FlexboxLayout是一个Android平台上与CSS的 Flexible box 布局模块 有相似功能的库。Flexbox 是CSS 的一种布局方案,可以简单、快捷的实现复杂布局。FlexboxLayout可以理解成一个高级版的LinearLayout,因为两个布局都把子view按顺序排列。两者之间最大的差别在于FlexboxLayout具有换行的特性。

FlexboxLayout示例

既然说FlexboxLayout方便、强大,那么我们就先以一个示例来看一下它的一个简单实用场景:现在很多APP都有标签功能,本节以简书首页的热门专题(标签)为例,看一下使用FlexboxLayout来实现有多方便。

简书首页热门专题如下图:

Android 实现流式布局的几种方式和FlexboxLayout的使用

FlexboxLayout使用

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.flexbox.FlexboxLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/flexbox_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:alignContent="flex_start"
    app:alignItems="center"
    app:dividerDrawable="@drawable/divider_shape"
    app:flexDirection="row"
    app:flexWrap="wrap"
    app:justifyContent="flex_start"
    app:showDivider="beginning|middle|end">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="程序员"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="影视天堂"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start"
        app:layout_flexGrow="1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="美食"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start"
        app:layout_flexGrow="1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="漫画.手绘"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start"
        app:layout_flexGrow="1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="广告圈"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="旅行.在路上"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="娱乐八卦"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="青春"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="谈写作"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="短篇小说"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="散文"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:background="@drawable/label_bg_shape"
        android:gravity="center"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:text="摄影"
        android:textColor="@color/text_color"
        app:layout_alignSelf="flex_start"
        app:layout_order="2" />
</com.google.android.flexbox.FlexboxLayout>
           

实现效果如下:

Android 实现流式布局的几种方式和FlexboxLayout的使用

很简单,就一个布局文件,以FlexboxLayout为父布局,向容器里面添加子Item 就行了。当然了,你可以在代码中向FlexboxLayout布局动态添加子元素,代码如下:

ublic class FlexboxLayoutActivity extends AppCompatActivity {

    private FlexboxLayout mFlexboxLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flexbox_layout);

        mFlexboxLayout = (FlexboxLayout) findViewById(R.id.flexbox_layout);

        // 通过代码向FlexboxLayout添加View
        for (int i = 0; i < 10; i++) {
            TextView textView = new TextView(this);
            textView.setBackground(getResources().getDrawable(R.drawable.label_bg_shape));
            textView.setText("散文" + i);
            textView.setGravity(Gravity.CENTER);
            textView.setPadding(dp2px(15), 0, dp2px(15), 0);
            textView.setTextColor(getResources().getColor(R.color.text_color));

            FlexboxLayout.LayoutParams layoutParams = new FlexboxLayout.LayoutParams(FlexboxLayout.LayoutParams.WRAP_CONTENT, dp2px(40));
            textView.setLayoutParams(layoutParams);
            mFlexboxLayout.addView(textView);
        }
    }

    private int dp2px(float value) {
        float density = getResources().getDisplayMetrics().density;
        return (int) (density * value + 0.5);
    }
}
           

FlexboxLayout支持的属性介绍

上面说了FlexboxLayout真正强大的是它定义的属性,那么这一节我们看一下Flexbox支持哪些属性,分为2个方面,FlexboxLayout支持的属性和FlexboxLayout 子元素支持的属性。

flexDirection:

flexDirection属性决定了主轴的方向,即FlexboxLayout里子Item的排列方向,有以下四种取值:

  • row (default): 默认值,主轴为水平方向,起点在左端,从左到右。
  • row_reverse:主轴为水平方向,起点在右端,从右到左。
  • column:主轴为竖直方向,起点在上端,从上到下。
  • column_reverse:主轴为竖直方向,起点在下端,从下往上。
Android 实现流式布局的几种方式和FlexboxLayout的使用
Android 实现流式布局的几种方式和FlexboxLayout的使用

flexWrap

flexWrap 这个属性决定Flex 容器是单行还是多行,并且决定副轴(与主轴垂直的轴)的方向。可能有以下3个值:

  • noWrap: 不换行,一行显示完子元素。
  • wrap: 按正常方向换行。
  • wrap_reverse: 按反方向换行。

justifyContent

justifyContent 属性控制元素主轴方向上的对齐方式,有以下5种取值:

  • flex_start (default): 默认值,左对齐
  • flex_end: 右对齐 center: 居中对齐
  • space_between: 两端对齐,中间间隔相同
  • space_around: 每个元素到两侧的距离相等。

alignItems

alignItems 属性控制元素在副轴方向的对齐方式,有以下5种取值:

  • stretch (default) :默认值,如果item没有设置高度,则充满容器高度。
  • flex_start:顶端对齐
  • flex_end:底部对齐
  • center:居中对齐
  • baseline:第一行内容的的基线对齐。
Android 实现流式布局的几种方式和FlexboxLayout的使用

alignContent

alignContent 属性控制多根轴线的对齐方式(也就是控制多行,如果子元素只有一行,则不起作用),可能有一下6种取值:

  • stretch (default): 默认值,充满交叉轴的高度(测试发现,需要alignItems 的值也为stretch 才有效)。
  • flex_start: 与交叉轴起点对齐。
  • flex_end: 与交叉轴终点对齐。
  • center: 与交叉轴居中对齐。
  • space_between: 交叉轴两端对齐,中间间隔相等。
  • space_around: 到交叉轴两端的距离相等。

showDividerHorizontal

showDividerHorizontal 控制显示水平方向的分割线,值为none | beginning | middle | end其中的一个或者多个。

dividerDrawableHorizontal

dividerDrawableHorizontal 设置Flex 轴线之间水平方向的分割线。

showDividerVertical

showDividerVertical 控制显示垂直方向的分割线,值为none | beginning | middle | end其中的一个或者多个。

dividerDrawableVertical

dividerDrawableVertical 设置子元素垂直方向的分割线。

showDivider

showDivider 控制显示水平和垂直方向的分割线,值为none | beginning | middle | end其中的一个或者多个。

dividerDrawable

dividerDrawable 设置水平和垂直方向的分割线,但是注意,如果同时和其他属性使用,比如为 Flex 轴、子元素设置了justifyContent=“space_around” 、alignContent=“space_between” 等等。可能会看到意料不到的空间,因此应该避免和这些值同时使用。

FleboxLayout子元素支持的属性介绍:

layout_order

layout_order 属性可以改变子元素的排列顺序,默认情况下,FlexboxLayout子元素的排列是按照xml文件中出现的顺序。默认值为1,值越小排在越靠前。

layout_flexGrow(float)

layout_flexGrow 子元素的放大比例, 决定如何分配剩余空间(如果存在剩余空间的话),默认值为0,不会分配剩余空间,如果有一个item的 layout_flexGrow 是一个正值,那么会将全部剩余空间分配给这个Item,如果有多个Item这个属性都为正值,那么剩余空间的分配按照layout_flexGrow定义的比例(有点像LinearLayout的layout_weight属性)。

layout_flexShrink(float)

layout_flexShrink:子元素缩小比例,当空间不足时,子元素需要缩小(设置了换行则无效),默认值为1,如果所有子元素的layout_flexShrink 值为1,空间不足时,都等比缩小,如果有一个为0,其他为1,空间不足时,为0的不缩小,负值无效。

layout_alignSelf

layout_alignSelf 属性可以给子元素设置对齐方式,上面讲的alignItems属性可以设置对齐,这个属性的功能和alignItems一样,只不过alignItems作用于所有子元素,而layout_alignSelf 作用于单个子元素。默认值为auto, 表示继承alignItems属性,如果为auto以外的值,则会覆盖alignItems属性。有以下6种取值:

  • auto (default)
  • flex_start
  • flex_end
  • center
  • baseline
  • stretch

除了auto以外,其他和alignItems属性一样。

layout_flexBasisPercent (fraction)

layout_flexBasisPercent的值为一个百分比,表示设置子元素的长度为它父容器长度的百分比,如果设置了这个值,那么通过这个属性计算的值将会覆盖layout_width或者layout_height的值。但是需要注意,这个值只有设置了父容器的长度时才有效(也就是MeasureSpec mode 是 MeasureSpec.EXACTLY)。默认值时-1。

layout_minWidth / layout_minHeight (dimension)

强制限制 FlexboxLayout的子元素(宽或高)不会小于最小值,不管layout_flexShrink这个属性的值为多少,子元素不会被缩小到小于设置的这个最小值。

layout_maxWidth / layout_maxHeight (dimension)

这个和上面的刚好相反,强制限制FlexboxLayout子元素不会大于这个最大值, 不管layout_flexGrow的值为多少,子元素不会被放大到超过这个最大值。

layout_wrapBefore

layout_wrapBefore 属性控制强制换行,默认值为false,如果将一个子元素的这个属性设置为true,那么这个子元素将会成为一行的第一个元素。这个属性将忽略flex_wrap 设置的 noWrap值。

与RecyclerView 的结合使用

Flexbox能够作为一个LayoutManager(FlexboxlayoutManager) 用在RecyclerView里面,这也就意味着你可以在一个有大量Item的可滚动容器里面使用Flexbox了。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="10dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
           
public class FlexboxRvActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;

    private List<String> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flexbox_rv_layout);

        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);


        FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(this);

        //flexDirection 属性决定主轴的方向(即项目的排列方向)。类似 LinearLayout 的 vertical 和 horizontal。
        flexboxLayoutManager.setFlexDirection(FlexDirection.ROW);//主轴为水平方向,起点在左端。

        //flexWrap 默认情况下 Flex 跟 LinearLayout 一样,都是不带换行排列的,但是flexWrap属性可以支持换行排列。
        flexboxLayoutManager.setFlexWrap(FlexWrap.WRAP);//按正常方向换行

        //justifyContent 属性定义了项目在主轴上的对齐方式。
        flexboxLayoutManager.setJustifyContent(JustifyContent.FLEX_START);//交叉轴的起点对齐。

        mRecyclerView.setLayoutManager(flexboxLayoutManager);

        loadData();

        CommomRvAdapter<String> adapter = new CommomRvAdapter<String>(this, mDatas, R.layout.item_rv) {
            @Override
            protected void fillData(CommomRvViewHolder holder, int position, String s) {
                holder.setText(R.id.tv_label, s);
            }
        };
        mRecyclerView.setAdapter(adapter);


        adapter.setOnItemClickListener((view, position) -> {
            Toast.makeText(this, mDatas.get(position), Toast.LENGTH_SHORT).show();
        });


    }

    private void loadData() {
        mDatas = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            if (i % 2 == 0) {
                mDatas.add("数据" + i);
            } else {
                mDatas.add("数据数据数据" + i);
            }
        }
    }
}
           

FlexboxlayoutManager是支持View回收的,而FlexboxLayout是不支持View回收的,FlexboxLayout只适用于少量Item的场景,这也是为什么会出现FlexboxLayoutManager的原因吧。

FlexboxLayout总结

FlexboxLayout是Google 开源的一个与CSS Flexbox有类似功能的强大布局,具有换行特性,使用起来特别方便,但是,FlexboxLayout是没有考虑View回收的,因此,它只使用于只有少量子Item的场景,如果向其中添加大量Item 是灰导致内存溢出的。所幸,最新的版本添加了与RecyclerView的集成,这就可以在有大量子Item的场景下使用了。另外,FlexboxLayout的这写属性的意义可能不好理解 ,建议大家去写个demo试一下每个属性的每个值看看是什么效果,这样就能很好的理解每个属性了。

项目完整代码