flowlayout流失布局:一行放不下直接换行

来源:互联网 发布:虚拟电话号码软件 编辑:程序博客网 时间:2024/04/29 19:12

前言

flow layout, 流式布局, 这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中, 往往起到了关键的作用。然而Android 官方, 并没有为开发者提供这样一个布局, 于是有很多开发者自己做了这样的工作,github上也出现了很多自定义FlowLayout。 最近, 我也实现了这样一个FlowLayout,自己感觉可能是当前最好用的FlowLayout了(捂脸),在这里做一下分享。
项目地址:https://github.com/2547095199/HuaYuan



展示


第一张图, 展示向FlowLayout中不断添加子View
第二张图, 展示压缩子View, 使他们尽可能充分利用空间
第三张图, 展示调整子View之间间隔, 使各行左右对齐



这张图,截断flowlayout到指定行数。--20160520更新。


话不多说,接下来看代码:


flawlayout页面代码:


package yuan.bwie.com.flowlayout;import android.content.Context;import android.content.res.TypedArray;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import java.util.ArrayList;import java.util.List;/** * Created by CZ on 2017/11/30. */public class FlowLayout extends ViewGroup {    private Context mContext;    private int usefulWidth; // the space of a line we can use(line's width minus the sum of left and right padding    private int lineSpacing = 0; // the spacing between lines in flowlayout    List<View> childList = new ArrayList();    List<Integer> lineNumList = new ArrayList();    public FlowLayout(Context context) {        this(context, null);    }    public FlowLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        mContext = context;        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,                R.styleable.FlowLayout);        lineSpacing = mTypedArray.getDimensionPixelSize(                R.styleable.FlowLayout_lineSpacing, 0);        mTypedArray.recycle();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int mPaddingLeft = getPaddingLeft();        int mPaddingRight = getPaddingRight();        int mPaddingTop = getPaddingTop();        int mPaddingBottom = getPaddingBottom();        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int lineUsed = mPaddingLeft + mPaddingRight;        int lineY = mPaddingTop;        int lineHeight = 0;        for (int i = 0; i < this.getChildCount(); i++) {            View child = this.getChildAt(i);            if (child.getVisibility() == GONE) {                continue;            }            int spaceWidth = 0;            int spaceHeight = 0;            LayoutParams childLp = child.getLayoutParams();            if (childLp instanceof MarginLayoutParams) {                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, lineY);                MarginLayoutParams mlp = (MarginLayoutParams) childLp;                spaceWidth = mlp.leftMargin + mlp.rightMargin;                spaceHeight = mlp.topMargin + mlp.bottomMargin;            } else {                measureChild(child, widthMeasureSpec, heightMeasureSpec);            }            int childWidth = child.getMeasuredWidth();            int childHeight = child.getMeasuredHeight();            spaceWidth += childWidth;            spaceHeight += childHeight;            if (lineUsed + spaceWidth > widthSize) {                //approach the limit of width and move to next line                lineY += lineHeight + lineSpacing;                lineUsed = mPaddingLeft + mPaddingRight;                lineHeight = 0;            }            if (spaceHeight > lineHeight) {                lineHeight = spaceHeight;            }            lineUsed += spaceWidth;        }        setMeasuredDimension(                widthSize,                heightMode == MeasureSpec.EXACTLY ? heightSize : lineY + lineHeight + mPaddingBottom        );    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int mPaddingLeft = getPaddingLeft();        int mPaddingRight = getPaddingRight();        int mPaddingTop = getPaddingTop();        int lineX = mPaddingLeft;        int lineY = mPaddingTop;        int lineWidth = r - l;        usefulWidth = lineWidth - mPaddingLeft - mPaddingRight;        int lineUsed = mPaddingLeft + mPaddingRight;        int lineHeight = 0;        int lineNum = 0;        lineNumList.clear();        for (int i = 0; i < this.getChildCount(); i++) {            View child = this.getChildAt(i);            if (child.getVisibility() == GONE) {                continue;            }            int spaceWidth = 0;            int spaceHeight = 0;            int left = 0;            int top = 0;            int right = 0;            int bottom = 0;            int childWidth = child.getMeasuredWidth();            int childHeight = child.getMeasuredHeight();            LayoutParams childLp = child.getLayoutParams();            if (childLp instanceof MarginLayoutParams) {                MarginLayoutParams mlp = (MarginLayoutParams) childLp;                spaceWidth = mlp.leftMargin + mlp.rightMargin;                spaceHeight = mlp.topMargin + mlp.bottomMargin;                left = lineX + mlp.leftMargin;                top = lineY + mlp.topMargin;                right = lineX + mlp.leftMargin + childWidth;                bottom = lineY + mlp.topMargin + childHeight;            } else {                left = lineX;                top = lineY;                right = lineX + childWidth;                bottom = lineY + childHeight;            }            spaceWidth += childWidth;            spaceHeight += childHeight;            if (lineUsed + spaceWidth > lineWidth) {                //approach the limit of width and move to next line                lineNumList.add(lineNum);                lineY += lineHeight + lineSpacing;                lineUsed = mPaddingLeft + mPaddingRight;                lineX = mPaddingLeft;                lineHeight = 0;                lineNum = 0;                if (childLp instanceof MarginLayoutParams) {                    MarginLayoutParams mlp = (MarginLayoutParams) childLp;                    left = lineX + mlp.leftMargin;                    top = lineY + mlp.topMargin;                    right = lineX + mlp.leftMargin + childWidth;                    bottom = lineY + mlp.topMargin + childHeight;                } else {                    left = lineX;                    top = lineY;                    right = lineX + childWidth;                    bottom = lineY + childHeight;                }            }            child.layout(left, top, right, bottom);            lineNum++;            if (spaceHeight > lineHeight) {                lineHeight = spaceHeight;            }            lineUsed += spaceWidth;            lineX += spaceWidth;        }        // add the num of last line        lineNumList.add(lineNum);    }    /**     * resort child elements to use lines as few as possible     */    public void relayoutToCompress() {        post(new Runnable() {            @Override            public void run() {                compress();            }        });    }    private void compress() {        int childCount = this.getChildCount();        if (0 == childCount) {            //no need to sort if flowlayout has no child view            return;        }        int count = 0;        for (int i = 0; i < childCount; i++) {            View v = getChildAt(i);            if (v instanceof BlankView) {                //BlankView is just to make childs look in alignment, we should ignore them when we relayout                continue;            }            count++;        }        View[] childs = new View[count];        int[] spaces = new int[count];        int n = 0;        for (int i = 0; i < childCount; i++) {            View v = getChildAt(i);            if (v instanceof BlankView) {                //BlankView is just to make childs look in alignment, we should ignore them when we relayout                continue;            }            childs[n] = v;            LayoutParams childLp = v.getLayoutParams();            int childWidth = v.getMeasuredWidth();            if (childLp instanceof MarginLayoutParams) {                MarginLayoutParams mlp = (MarginLayoutParams) childLp;                spaces[n] = mlp.leftMargin + childWidth + mlp.rightMargin;            } else {                spaces[n] = childWidth;            }            n++;        }        int[] compressSpaces = new int[count];        for (int i = 0; i < count; i++) {            compressSpaces[i] = spaces[i] > usefulWidth ? usefulWidth : spaces[i];        }        sortToCompress(childs, compressSpaces);        this.removeAllViews();        for (View v : childList) {            this.addView(v);        }        childList.clear();    }    private void sortToCompress(View[] childs, int[] spaces) {        int childCount = childs.length;        int[][] table = new int[childCount + 1][usefulWidth + 1];        for (int i = 0; i < childCount + 1; i++) {            for (int j = 0; j < usefulWidth; j++) {                table[i][j] = 0;            }        }        boolean[] flag = new boolean[childCount];        for (int i = 0; i < childCount; i++) {            flag[i] = false;        }        for (int i = 1; i <= childCount; i++) {            for (int j = spaces[i - 1]; j <= usefulWidth; j++) {                table[i][j] = (table[i - 1][j] > table[i - 1][j - spaces[i - 1]] + spaces[i - 1]) ? table[i - 1][j] : table[i - 1][j - spaces[i - 1]] + spaces[i - 1];            }        }        int v = usefulWidth;        for (int i = childCount; i > 0 && v >= spaces[i - 1]; i--) {            if (table[i][v] == table[i - 1][v - spaces[i - 1]] + spaces[i - 1]) {                flag[i - 1] = true;                v = v - spaces[i - 1];            }        }        int rest = childCount;        View[] restArray;        int[] restSpaces;        for (int i = 0; i < flag.length; i++) {            if (flag[i] == true) {                childList.add(childs[i]);                rest--;            }        }        if (0 == rest) {            return;        }        restArray = new View[rest];        restSpaces = new int[rest];        int index = 0;        for (int i = 0; i < flag.length; i++) {            if (flag[i] == false) {                restArray[index] = childs[i];                restSpaces[index] = spaces[i];                index++;            }        }        table = null;        childs = null;        flag = null;        sortToCompress(restArray, restSpaces);    }    /**     * add some blank view to make child elements look in alignment     */    public void relayoutToAlign() {        post(new Runnable() {            @Override            public void run() {                align();            }        });    }    private void align() {        int childCount = this.getChildCount();        if (0 == childCount) {            //no need to sort if flowlayout has no child view            return;        }        int count = 0;        for (int i = 0; i < childCount; i++) {            View v = getChildAt(i);            if (v instanceof BlankView) {                //BlankView is just to make childs look in alignment, we should ignore them when we relayout                continue;            }            count++;        }        View[] childs = new View[count];        int[] spaces = new int[count];        int n = 0;        for (int i = 0; i < childCount; i++) {            View v = getChildAt(i);            if (v instanceof BlankView) {                //BlankView is just to make childs look in alignment, we should ignore them when we relayout                continue;            }            childs[n] = v;            LayoutParams childLp = v.getLayoutParams();            int childWidth = v.getMeasuredWidth();            if (childLp instanceof MarginLayoutParams) {                MarginLayoutParams mlp = (MarginLayoutParams) childLp;                spaces[n] = mlp.leftMargin + childWidth + mlp.rightMargin;            } else {                spaces[n] = childWidth;            }            n++;        }        int lineTotal = 0;        int start = 0;        this.removeAllViews();        for (int i = 0; i < count; i++) {            if (lineTotal + spaces[i] > usefulWidth) {                int blankWidth = usefulWidth - lineTotal;                int end = i - 1;                int blankCount = end - start;                if (blankCount >= 0) {                    if (blankCount > 0) {                        int eachBlankWidth = blankWidth / blankCount;                        MarginLayoutParams lp = new MarginLayoutParams(eachBlankWidth, 0);                        for (int j = start; j < end; j++) {                            this.addView(childs[j]);                            BlankView blank = new BlankView(mContext);                            this.addView(blank, lp);                        }                    }                    this.addView(childs[end]);                    start = i;                    i--;                    lineTotal = 0;                } else {                    this.addView(childs[i]);                    start = i + 1;                    lineTotal = 0;                }            } else {                lineTotal += spaces[i];            }        }        for (int i = start; i < count; i++) {            this.addView(childs[i]);        }    }    /**     * use both of relayout methods together     */    public void relayoutToCompressAndAlign() {        post(new Runnable() {            @Override            public void run() {                compress();                align();            }        });    }    /**     * cut the flowlayout to the specified num of lines     *     * @param line_num_now     */    public void specifyLines(final int line_num_now) {        post(new Runnable() {            @Override            public void run() {                int line_num = line_num_now;                int childNum = 0;                if (line_num > lineNumList.size()) {                    line_num = lineNumList.size();                }                for (int i = 0; i < line_num; i++) {                    childNum += lineNumList.get(i);                }                List<View> viewList = new ArrayList<>();                for (int i = 0; i < childNum; i++) {                    viewList.add(getChildAt(i));                }                removeAllViews();                for (View v : viewList) {                    addView(v);                }            }        });    }    @Override    protected LayoutParams generateLayoutParams(LayoutParams p) {        return new MarginLayoutParams(p);    }    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new MarginLayoutParams(getContext(), attrs);    }    @Override    protected LayoutParams generateDefaultLayoutParams() {        return new MarginLayoutParams(super.generateDefaultLayoutParams());    }    class BlankView extends View {        public BlankView(Context context) {            super(context);        }    }}


main activity页面代码:

package yuan.bwie.com.flowlayout;import android.content.Context;import android.graphics.Color;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.util.TypedValue;import android.view.Gravity;import android.view.View;import android.view.ViewGroup;import android.widget.TextView;/*这是flowlayout的如果一行里放不下一个东西的时候就会直接换行 */public class MainActivity extends AppCompatActivity implements View.OnClickListener {    FlowLayout flowLayout;    String[] texts = new String[]{            "good", "bad", "understand", "it is a good day !",            "how are you", "ok", "fine", "name", "momo",            "lankton", "lan", "flowlayout demo", "soso"    };    int length;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        length = texts.length;        flowLayout = (FlowLayout) findViewById(R.id.flowlayout);        findViewById(R.id.btn_add_random).setOnClickListener(this);        findViewById(R.id.btn_relayout1).setOnClickListener(this);        findViewById(R.id.btn_remove_all).setOnClickListener(this);        findViewById(R.id.btn_relayout2).setOnClickListener(this);        findViewById(R.id.btn_specify_line).setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.btn_add_random:                int ranHeight = dip2px(this, 30);                ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ranHeight);                lp.setMargins(dip2px(this, 10), 0, dip2px(this, 10), 0);                TextView tv = new TextView(this);                tv.setPadding(dip2px(this, 15), 0, dip2px(this, 15), 0);                tv.setTextColor(Color.parseColor("#FF3030"));                tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);                int index = (int)(Math.random() * length);                tv.setText(texts[index]);                tv.setGravity(Gravity.CENTER_VERTICAL);                tv.setLines(1);                tv.setBackgroundResource(R.drawable.bg_tag);                flowLayout.addView(tv, lp);                break;            case R.id.btn_remove_all:                flowLayout.removeAllViews();                break;            case R.id.btn_relayout1:                flowLayout.relayoutToCompress();                break;            case R.id.btn_relayout2:                flowLayout.relayoutToAlign();                break;            case R.id.btn_specify_line:                flowLayout.specifyLines(3);                break;            default:                break;        }    }    public static int dip2px(Context context, float dpValue) {        final float scale = context.getResources().getDisplayMetrics().density;        return (int) (dpValue * scale + 0.5f);    }    public static int px2dip(Context context, float pxValue) {        final float scale = context.getResources().getDisplayMetrics().density;        return (int) (pxValue / scale + 0.5f);    }}


main activity页面布局:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    xmlns:app="http://schemas.android.com/apk/res-auto">    <ScrollView        android:layout_width="match_parent"        android:layout_height="match_parent">        <yuan.bwie.com.flowlayout.FlowLayout            android:id="@+id/flowlayout"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:paddingLeft="-10dp"            android:paddingRight="-10dp"            app:lineSpacing="10dp"            app:maxLine="3"            android:background="#F0F0F0">        </yuan.bwie.com.flowlayout.FlowLayout>    </ScrollView>    <TextView        android:id="@+id/btn_add_random"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:textAllCaps="false"        android:text="add"        android:padding="10dp"        android:background="#48a0a3"        android:layout_alignParentBottom="true"/>    <TextView        android:id="@+id/btn_remove_all"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toRightOf="@id/btn_add_random"        android:textAllCaps="false"        android:text="clean"        android:padding="10dp"        android:background="#48a0a3"        android:layout_marginLeft="5dp"        android:layout_alignParentBottom="true"/>    <TextView        android:id="@+id/btn_relayout1"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toRightOf="@id/btn_remove_all"        android:textAllCaps="false"        android:text="compress"        android:padding="10dp"        android:background="#48a0a3"        android:layout_marginLeft="5dp"        android:layout_alignParentBottom="true"/>    <TextView        android:id="@+id/btn_relayout2"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toRightOf="@id/btn_relayout1"        android:textAllCaps="false"        android:text="align"        android:padding="10dp"        android:background="#48a0a3"        android:layout_marginLeft="5dp"        android:layout_alignParentBottom="true"/>    <TextView        android:id="@+id/btn_specify_line"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toRightOf="@id/btn_relayout2"        android:textAllCaps="false"        android:text="line=3"        android:padding="10dp"        android:background="#48a0a3"        android:layout_marginLeft="5dp"        android:layout_alignParentBottom="true"/></RelativeLayout>
在values文件夹创建一个attrsxml文件
<declare-styleable name="FlowLayout">    <attr name="lineSpacing" format="dimension"/>    <attr name="maxLine" format="integer"/></declare-styleable>

原创粉丝点击