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(可以设置展开和收起按钮)

仿淘宝历史搜索效果:

在这里插入图片描述
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来实现有多方便。
简书首页热门专题如下图:

在这里插入图片描述

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>

实现效果如下:
在这里插入图片描述

很简单,就一个布局文件,以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:主轴为竖直方向,起点在下端,从下往上。

在这里插入图片描述

在这里插入图片描述

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:第一行内容的的基线对齐。

在这里插入图片描述
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试一下每个属性的每个值看看是什么效果,这样就能很好的理解每个属性了。

项目完整代码

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: Android中的布局是一种常用的布局方式,它可以根据内容的大小和数量自动调整控件的位置和大小,使得界面能够自适应屏幕的宽度。通常在需要展示多个标签、图片或文字等的场景下使用布局的特点是将内容按照先后顺序从左到右排列,当一行的宽度不足以容纳下一个控件时,会自动换行。这种布局方式能够节省空间,提高界面的可读性和美观性。 在Android中,可以使用FlowLayout这个第三方库来实现布局使用FlowLayout的步骤如下:首先在项目的build.gradle文件中添加依赖,然后在布局文件中将根布局设置为FlowLayout,并在其中添加需要展示的控件,可以通过调整控件的属性来定义布局的样和排列方式布局可以动态调整控件的位置和大小,可以通过设置权重来控制每个控件在水平方向上的占比,也可以设置边距来调整控件之间的间隔。另外,布局还可以为每个控件设置点击事件和长按事件,方便实现更丰富的交互效果。 总之,布局是一种灵活且强大的布局方式,可以有效地解决多个控件在界面上排列不下或排列不美观的问题,同时也能够提高界面的可读性和用户体验。在开发Android应用时,如果遇到需要展示多个标签、图片或文字等的场景,布局是一个很好的选择。 ### 回答2: Android布局是一种灵活的布局方式,用于在屏幕上动态自适应地显示一系列视图。它能够根据子视图的大小和屏幕大小自动调整子视图的位置和宽度。这种布局方式适用于显示不规则大小的子视图,尤其适用于显示标签、图片、标签云等。 Android布局可以通过使用LinearLayout或GridLayout来实现。在LinearLayout中,可以设置orientation属性为horizontal或vertical来实现水平或垂直布局。在GridLayout中,可以通过设置列数来控制每行显示的子视图数量。 Android布局的优点是可以根据屏幕的大小和方向自动调整子视图的布局,使得页面在不同设备上都能够良好地显示。同时,它也提供了更好的用户体验,因为用户可以在不同屏幕上以不同的方式查看和交互。 然而,Android布局也存在一些限制。由于其自适应特性,子视图的大小和位置可能会受到限制。此外,较复杂的布局可能会导致性能问题,因为在布局过程中需要进行多次测量和计算。因此,在使用布局时,需要谨慎处理子视图的大小和数量,以提高性能并避免布局过于复杂。 总结来说,Android布局是一种灵活而自适应的布局方式,适用于显示不规则大小的子视图。它可以根据屏幕的大小和方向自动调整子视图的布局,并提供更好的用户体验。然而,需要注意处理子视图的大小和数量,以提高性能并避免布局过于复杂。 ### 回答3: Android布局(Flow Layout)是一种动态适应屏幕宽度的布局方式,主要用于解决在屏幕上按行排列多个子视图的问题。 在传统的线性布局中,如果视图超出屏幕宽度,就会自动换行,但是每一行只会放置一个子视图。而在布局中,子视图会根据屏幕宽度自动换行,并且每一行可以放置多个子视图,适应屏幕不同宽度的设备。 布局使用非常方便,只需要将子视图添加到布局中即可。它提供了一些属性来控制子视图在布局中的排列方式,比如子视图之间的间距、子视图的对齐方式等。此外,布局还可以通过设置权重属性,实现子视图的均匀分布或者按比例分布。 布局在一些场景下非常有用,比如在标签云、瀑布展示等需要动态调整子视图排列的情况下。相比于其他布局方式布局可以更好地利用屏幕空间,提高用户体验。 总之,Android布局是一种动态适应屏幕宽度的布局方式,可以方便地排列多个子视图,并提供了一些属性来控制子视图的排列方式和样。它的使用简单灵活,适用于多种场景,可以有效地提高用户界面的可用性和美观性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值