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 ListchildList = new ArrayList(); List 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 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>