背景
最近项目中用到了流式布局,最初就决定自己写一个,发现一时竟然没有思路。虽然自定义控件的博客看了不少,也写过简单的自定义控件,但是真正自己独立写出一个流式布局,还是有些考验的。查找了几篇博客,思路大同小异,理清思路,自己开干写了一下。中间改了几个问题,觉得可以正常使用后,这才有了这篇博客。
我想说,会写流式布局了,表示你对ViewGroup的测量(onMeasure)和布局(onLayout)有了一个较为深入的理解。流式布局主要涉及ViewGroup对子View的测量和摆放(布局)。
效果图
参考布局
参考布局,尺寸和颜色根据自己需求修改:
<com.istarshine.views.MFlowLayout
android:id="@+id/flow_search_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/common_margin"
android:layout_marginTop="2dp"
android:layout_marginRight="18dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="鞋子"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="吹风机"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="袜子 男 纯棉"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="豆浆机 九阳"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="三只松鼠"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="三只松鼠 三只松鼠 三只松鼠 三只松鼠 三只松鼠 三只松鼠 三只松鼠"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/shape_solid_ff"
android:paddingLeft="@dimen/common_margin"
android:paddingTop="4dp"
android:paddingRight="@dimen/common_margin"
android:paddingBottom="4dp"
android:text="刮皮刀 水果刀"
android:textColor="@color/common_text_22"
android:textSize="@dimen/text_size_14" />
</com.istarshine.views.MFlowLayout>
MFlowLayout 代码
在 onMeasure()
中测量子View,按流式布局算出MFlowLayout自己(ViewGroup)的宽高;在 onLayout()
中按流式布局准确摆放子View。见如下代码:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* 流式布局
*/
public class MFlowLayout extends ViewGroup {
public MFlowLayout(Context context) {
super(context);
}
public MFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量所有的子元素(调用子元素的measure()),
// 只有测量过的元素调用child.getMeasuredHeight/Width()才能获取到值,否则为0
// measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int spaceWidth = widthSize - getPaddingLeft() - getPaddingRight();
int resultWidth = 0;
int resultHeight = 0;
int lineWidth = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
//测量每个子元素的宽高
int widthUsed = getPaddingLeft() + getPaddingRight();
int heightUsed = getPaddingTop() + getPaddingBottom();
measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
// measureChild(child, widthMeasureSpec, heightMeasureSpec);
//测量后的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams childMlp = (MarginLayoutParams) child.getLayoutParams();
int childWidthWithMargin = childWidth + childMlp.leftMargin + childMlp.rightMargin;
int childHeightWithMargin = childHeight + childMlp.topMargin + childMlp.bottomMargin;
//一行放不下了,就换行
if (lineWidth + childWidthWithMargin > spaceWidth) {
//换行,计算宽高
resultWidth = Math.max(resultWidth, lineWidth);
resultHeight += lineHeight;
//换行结束,重新给lineWidth和lineHeight赋值
lineWidth = childWidthWithMargin;
lineHeight = childHeightWithMargin;
} else {
//不换行,宽度直接相加
lineWidth += childWidthWithMargin;
//高度取二者最大值
lineHeight = Math.max(lineHeight, childHeightWithMargin);
}
//最后一个肯定是最后一行
if (i == getChildCount() - 1) {
resultWidth = Math.max(resultWidth, lineWidth);
resultHeight += lineHeight;
}
}
//因为上面resultWidth参与了宽度比较,所以计算padding必须放在这里
resultWidth += getPaddingLeft() + getPaddingRight();
resultHeight += getPaddingTop() + getPaddingBottom();
//设置FlowLayout的宽高
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : resultWidth,
heightMode == MeasureSpec.EXACTLY ? heightSize : resultHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int spaceWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int paddingTop = getPaddingTop();
int paddingLeft = getPaddingLeft();
int childLeft = 0;
int childTop = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
MarginLayoutParams childMlp = (MarginLayoutParams) child.getLayoutParams();
int childWidthWithMargin = childWidth + childMlp.leftMargin + childMlp.rightMargin;
int childHeightWithMargin = childHeight + childMlp.topMargin + childMlp.bottomMargin;
if (childLeft + childWidthWithMargin > spaceWidth) {
childTop += lineHeight;
//换行处理
childLeft = 0;
lineHeight = childHeightWithMargin;
} else {
lineHeight = Math.max(lineHeight, childHeightWithMargin);
}
int left = childLeft + paddingLeft + childMlp.leftMargin;
int top = childTop + paddingTop + childMlp.topMargin;
int right = left + childWidth;
int bottom = top + childHeight;
child.layout(left, top, right, bottom);
childLeft += childWidthWithMargin;
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
if (p instanceof MarginLayoutParams) {
return p;
}
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
public void setLayoutParams(LayoutParams params) {
super.setLayoutParams(params);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof MarginLayoutParams;
}
}
代码并不难,但需要一些计算逻辑,和一些注意点。
解释一下为什么这么测量子View,而不是直接使用注释部分(measureChild(child, widthMeasureSpec, heightMeasureSpec);
):
//测量每个子元素的宽高
int widthUsed = getPaddingLeft() + getPaddingRight();
int heightUsed = getPaddingTop() + getPaddingBottom();
measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
// measureChild(child, widthMeasureSpec, heightMeasureSpec);
如果想要MFlowLayout可以设置padding,子View可以设置margin,就需要使用measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed)
,这样就会在测量的时候把MFlowLayout设置的padding(wideUsed,heightUsed)和子View设置的margin计算在内。而子View可以设置margin,则需要MarginLayoutParams,具体见上面代码。