github地址
https://github.com/zhouxu88/FlowLayout/
一、概述:
在日常的app使用中,我们会在Android 的app中看见 热门标签等自动换行的流式布局, 场景:主要用于关键词搜索或者热门标签等场景下面我们就来详细介绍流式布局的应用特点以及用的的技术点:
效果图:
1. 特点:当上面一行的空间不够容纳新的TextView时候,才开辟下一行的空间
原理图:
2.自定义ViewGroup,重点重写下面两个方法
1)、onMeasure:测量子view的宽高,设置自己的宽和高
2)、onLayout:设置子view的位置
onMeasure:根据子view的布局文件中属性,来为子view设置测量模式和测量值 测量=测量模式+测量值;
测量模式有3种: EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY; AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST; UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。3.LayoutParams ViewGroup LayoutParams :每个 ViewGroup 对应一个 LayoutParams; 即 ViewGroup -> LayoutParams getLayoutParams 不知道转为哪个对应的LayoutParams ,其实很简单,就是如下: 子View.getLayoutParams 得到的LayoutParams对应的就是 子View所在的父控件的LayoutParams; 例如,LinearLayout 里面的子view.getLayoutParams ->LinearLayout.LayoutParams 所以 咱们的FlowLayout 也需要一个LayoutParams,由于上面的效果图是子View的 margin, 所以应该使用MarginLayoutParams。
即FlowLayout->MarginLayoutParams
二、热门标签的流式布局的实现:
1. 自定义热门标签的ViewGroup实现
根据上面的技术分析,自定义类继承于ViewGroup,并重写 onMeasure和onLayout等方法。具体实现代码如下:
public class FlowLayout extends ViewGroup {
private static final String LOG_TAG = "FlowLayout";
/**
* Special value for the child view spacing.
* SPACING_AUTO means that the actual spacing is calculated according to the size of the
* container and the number of the child views, so that the child views are placed evenly in
* the container.
*/
public static final int SPACING_AUTO = -65536;
/**
* Special value for the horizontal spacing of the child views in the last row
* SPACING_ALIGN means that the horizontal spacing of the child views in the last row keeps
* the same with the spacing used in the row above. If there is only one row, this value is
* ignored and the spacing will be calculated according to childSpacing.
*/
public static final int SPACING_ALIGN = -65537;
private static final int SPACING_UNDEFINED = -65538;
private static final boolean DEFAULT_FLOW = true;
private static final int DEFAULT_CHILD_SPACING = 0;
private static final int DEFAULT_CHILD_SPACING_FOR_LAST_ROW = SPACING_UNDEFINED;
private static final float DEFAULT_ROW_SPACING = 0;
private static final boolean DEFAULT_RTL = false;
private boolean mFlow = DEFAULT_FLOW;
private int mChildSpacing = DEFAULT_CHILD_SPACING;
private int mChildSpacingForLastRow = DEFAULT_CHILD_SPACING_FOR_LAST_ROW;
private float mRowSpacing = DEFAULT_ROW_SPACING;
private float mAdjustedRowSpacing = DEFAULT_ROW_SPACING;
private boolean mRtl = DEFAULT_RTL;
private List<Float> mHorizontalSpacingForRow = new ArrayList<>();
private List<Integer> mHeightForRow = new ArrayList<>();
private List<Integer> mChildNumForRow = new ArrayList<>();
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.FlowLayout, 0, 0);
try {
mFlow = a.getBoolean(R.styleable.FlowLayout_flow, DEFAULT_FLOW);
try {
mChildSpacing = a.getInt(R.styleable.FlowLayout_childSpacing, DEFAULT_CHILD_SPACING);
} catch (NumberFormatException e) {
mChildSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_childSpacing, (int)dpToPx(DEFAULT_CHILD_SPACING));
}
try {
mChildSpacingForLastRow = a.getInt(R.styleable.FlowLayout_childSpacingForLastRow, SPACING_UNDEFINED);
} catch (NumberFormatException e) {
mChildSpacingForLastRow = a.getDimensionPixelSize(R.styleable.FlowLayout_childSpacingForLastRow, (int)dpToPx(DEFAULT_CHILD_SPACING));
}
try {
mRowSpacing = a.getInt(R.styleable.FlowLayout_rowSpacing, 0);
} catch (NumberFormatException e) {
mRowSpacing = a.getDimension(R.styleable.FlowLayout_rowSpacing, dpToPx(DEFAULT_ROW_SPACING));
}
mRtl = a.getBoolean(R.styleable.FlowLayout_rtl, DEFAULT_RTL);
} finally {
a.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
mHorizontalSpacingForRow.clear();
mChildNumForRow.clear();
mHeightForRow.clear();
int measuredHeight = 0, measuredWidth = 0, childCount = getChildCount();
int rowWidth = 0, maxChildHeightInRow = 0, childNumInRow = 0;
int rowSize = widthSize - getPaddingLeft() - getPaddingRight();
boolean allowFlow = widthMode != MeasureSpec.UNSPECIFIED && mFlow;
int childSpacing = mChildSpacing == SPACING_AUTO && widthMode == MeasureSpec.UNSPECIFIED
? 0 : mChildSpacing;
float tmpSpacing = childSpacing == SPACING_AUTO ? 0 : childSpacing;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams childParams = child.getLayoutParams();
int horizontalMargin = 0, verticalMargin = 0;
if (childParams instanceof MarginLayoutParams) {
measureChildWithMargins(child, widthMeasureSpec, 0,heightMeasureSpec, measuredHeight);
MarginLayoutParams marginParams = (MarginLayoutParams) childParams;
horizontalMargin = marginParams.leftMargin + marginParams.rightMargin;
verticalMargin = marginParams.topMargin + marginParams.bottomMargin;
} else {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
int childWidth = child.getMeasuredWidth() + horizontalMargin;
int childHeight = child.getMeasuredHeight() + verticalMargin;
if (allowFlow && rowWidth + childWidth > rowSize) { // Need flow to next row
// Save parameters for current row
mHorizontalSpacingForRow.add(
getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow));
mChildNumForRow.add(childNumInRow);
mHeightForRow.add(maxChildHeightInRow);
measuredHeight += maxChildHeightInRow;
measuredWidth = max(measuredWidth, rowWidth);
// Place the child view to next row
childNumInRow = 1;
rowWidth = childWidth + (int)tmpSpacing;
maxChildHeightInRow = childHeight;
} else {
childNumInRow++;
rowWidth += childWidth + tmpSpacing;
maxChildHeightInRow = max(maxChildHeightInRow, childHeight);
}
}
// Measure remaining child views in the last row
if (mChildSpacingForLastRow == SPACING_ALIGN) {
// For SPACING_ALIGN, use the same spacing from the row above if there is more than one
// row.
if (mHorizontalSpacingForRow.size() >= 1) {
mHorizontalSpacingForRow.add(
mHorizontalSpacingForRow.get(mHorizontalSpacingForRow.size() - 1));
} else {
mHorizontalSpacingForRow.add(
getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow));
}
} else if (mChildSpacingForLastRow != SPACING_UNDEFINED) {
// For SPACING_AUTO and specific DP values, apply them to the spacing strategy.
mHorizontalSpacingForRow.add(
getSpacingForRow(mChildSpacingForLastRow, rowSize, rowWidth, childNumInRow));
} else {
// For SPACING_UNDEFINED, apply childSpacing to the spacing strategy for the last row.
mHorizontalSpacingForRow.add(
getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow));
}
mChildNumForRow.add(childNumInRow);
mHeightForRow.add(maxChildHeightInRow);
measuredHeight += maxChildHeightInRow;
measuredWidth = max(measuredWidth, rowWidth);
if (childSpacing == SPACING_AUTO) {
measuredWidth = widthSize;
} else if (widthMode == MeasureSpec.UNSPECIFIED) {
measuredWidth = measuredWidth + getPaddingLeft() + getPaddingRight();
} else {
measuredWidth = min(measuredWidth + getPaddingLeft() + getPaddingRight(), widthSize);
}
measuredHeight += getPaddingTop() + getPaddingBottom();
int rowNum = mHorizontalSpacingForRow.size();
float rowSpacing = mRowSpacing == SPACING_AUTO && heightMode == MeasureSpec.UNSPECIFIED
? 0 : mRowSpacing;
if (rowSpacing == SPACING_AUTO) {
if (rowNum > 1) {
mAdjustedRowSpacing = (heightSize - measuredHeight) / (rowNum - 1);
} else {
mAdjustedRowSpacing = 0;
}
measuredHeight = heightSize;
} else {
mAdjustedRowSpacing = rowSpacing;
if (heightMode == MeasureSpec.UNSPECIFIED) {
measuredHeight = (int)(measuredHeight + mAdjustedRowSpacing * (rowNum - 1));
} else {
measuredHeight = min(
(int)(measuredHeight + mAdjustedRowSpacing * (rowNum - 1)), heightSize);
}
}
measuredWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : measuredWidth;
measuredHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : measuredHeight;
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int x = mRtl ? (getWidth() - paddingRight) : paddingLeft;
int y = paddingTop;
int rowCount = mChildNumForRow.size(), childIdx = 0;
for (int row = 0; row < rowCount; row++) {
int childNum = mChildNumForRow.get(row);
int rowHeight = mHeightForRow.get(row);
float spacing = mHorizontalSpacingForRow.get(row);
for (int i = 0; i < childNum; i++) {
View child = getChildAt(childIdx++);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams childParams = child.getLayoutParams();
int marginLeft = 0, marginTop = 0, marginRight = 0;
if (childParams instanceof MarginLayoutParams) {
MarginLayoutParams marginParams = (MarginLayoutParams) childParams;
marginLeft = marginParams.leftMargin;
marginRight = marginParams.rightMargin;
marginTop = marginParams.topMargin;
}
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (mRtl) {
child.layout(x - marginRight - childWidth, y + marginTop,
x - marginRight, y + marginTop + childHeight);
x -= childWidth + spacing + marginLeft + marginRight;
} else {
child.layout(x + marginLeft, y + marginTop,
x + marginLeft + childWidth, y + marginTop + childHeight);
x += childWidth + spacing + marginLeft + marginRight;
}
}
x = mRtl ? (getWidth() - paddingRight) : paddingLeft;
y += rowHeight + mAdjustedRowSpacing;
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
/**
* Returns whether to allow child views flow to next row when there is no enough space.
*
* @return Whether to flow child views to next row when there is no enough space.
*/
public boolean isFlow() {
return mFlow;
}
/**
* Sets whether to allow child views flow to next row when there is no enough space.
*
* @param flow true to allow flow. false to restrict all child views in one row.
*/
public void setFlow(boolean flow) {
mFlow = flow;
requestLayout();
}
/**
* Returns the horizontal spacing between child views.
*
* @return The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO}, or a fixed size in pixels.
*/
public int getChildSpacing() {
return mChildSpacing;
}
/**
* Sets the horizontal spacing between child views.
*
* @param childSpacing The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO}, or a fixed size in
* pixels.
*/
public void setChildSpacing(int childSpacing) {
mChildSpacing = childSpacing;
requestLayout();
}
/**
* Returns the horizontal spacing between child views of the last row.
*
* @return The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO},
* {@link com.zx.flowlayout.FlowLayout#SPACING_ALIGN}, or a fixed size in pixels
*/
public int getChildSpacingForLastRow() {
return mChildSpacingForLastRow;
}
/**
* Sets the horizontal spacing between child views of the last row.
*
* @param childSpacingForLastRow The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO},
* {@link com.zx.flowlayout.FlowLayout#SPACING_ALIGN}, or a fixed size in pixels
*/
public void setChildSpacingForLastRow(int childSpacingForLastRow) {
mChildSpacingForLastRow = childSpacingForLastRow;
requestLayout();
}
/**
* Returns the vertical spacing between rows.
*
* @return The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO}, or a fixed size in pixels.
*/
public float getRowSpacing() {
return mRowSpacing;
}
/**
* Sets the vertical spacing between rows in pixels. Use SPACING_AUTO to evenly place all rows
* in vertical.
*
* @param rowSpacing The spacing, either {@link com.zx.flowlayout.FlowLayout#SPACING_AUTO}, or a fixed size in
* pixels.
*/
public void setRowSpacing(float rowSpacing) {
mRowSpacing = rowSpacing;
requestLayout();
}
private int max(int a, int b) {
return a > b ? a : b;
}
private int min(int a, int b) {
return a < b ? a : b;
}
private float getSpacingForRow(int spacingAttribute, int rowSize, int usedSize, int childNum) {
float spacing;
if (spacingAttribute == SPACING_AUTO) {
if (childNum > 1) {
spacing = (rowSize - usedSize) / (childNum - 1);
} else {
spacing = 0;
}
} else {
spacing = spacingAttribute;
}
return spacing;
}
private float dpToPx(float dp){
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
2、布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center"
tools:context="com.zx.flowlayout.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:text="布局文件静态的流式布局"
android:layout_marginBottom="16dp"/>
<com.zx.flowlayout.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:childSpacing="auto"
app:childSpacingForLastRow="align"
app:rowSpacing="16dp" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:background="@drawable/label_bg"
android:text="Java"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="Android"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="OC"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="PHP"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:text="C++"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="VR"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="Kobe Bryant"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="Jordan"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="T_MAC"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/label_bg"
android:gravity="center"
android:padding="8dp"
android:layout_marginEnd="16dp"
android:text="Wade"/>
</com.zx.flowlayout.FlowLayout>
<Button
android:id="@+id/dynamic_flow_layout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="#000000"
android:text="java代码动态生成流式布局"
android:layout_marginBottom="16dp"/>
<com.zx.flowlayout.FlowLayout
android:id="@+id/flow_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:childSpacing="auto"
app:childSpacingForLastRow="align"
app:rowSpacing="16dp"/>
</LinearLayout>
3、drawable/label_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="@android:color/white"/>
<corners android:radius="25dp"/>
<stroke android:width="1dp" android:color="#3799f4"/>
</shape>
</shape>
4、activity中的调用
public class MainActivity extends AppCompatActivity {
private FlowLayout flowLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
flowLayout = (FlowLayout) findViewById(R.id.flow_layout); findViewById(R.id.dynamic_flow_layout_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String[] titles = new String[]{"控卫", "得分后卫", "小前锋", "大前锋", "中锋",
"Iphone", "三星", "华为", "小米", "Vivo"};
for (String text : titles) {
TextView textView = buildLabel(text);
flowLayout.addView(textView);
}
}
});
//选择item
for (int i = 0; i < flowLayout1.getChildCount(); i++) {
flowLayout1.getChildAt(i).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TextView tv = (TextView) v;
Log.i("tag", "onClick:--------->" + tv.getText());
Toast.makeText(MainActivity.this, tv.getText(), Toast.LENGTH_SHORT).show();
}
});
}
}
private TextView buildLabel(String text) {
TextView textView = new TextView(this);
textView.setText(text);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setPadding((int)dpToPx(16), (int)dpToPx(8), (int)dpToPx(16), (int)dpToPx(8));
textView.setGravity(Gravity.CENTER);
textView.setBackgroundResource(R.drawable.label_bg);
return textView;
}
//dp转换成px
private float dpToPx(float dp) {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}