流式布局
流式布局是一种特殊的布局模式,生活中有很多地方会使用到流式布局,比如网站的标签云就是使用的流式布局,流式布局的特点是,按方向一个一个的排列元素,当一行无法容纳新元素的时候,会自动换行,将新元素放置到下一行,如下图局势一个典型的流式布局。

自定义ViewGroup打造流式布局
还是老规矩,介绍之前贴出Demo效果图,看看是否为你想要的样子。

- 自定义View的步骤
自定义一个View往往有三个方法需要注意。
onMeasure(); 用于测量View的大小
onLayout(); 用于分配View的位置,定义布局
onDraw(); 绘制View
由于网上关于这三个函数的讲解已经很多了,而且都是大神级别的人物写的,所以我就不献丑了,只贴出一些我觉得好的博客。
- 实战编码
如果你详细阅读了上面3篇博客,那么你一定对View的绘制流程有了基本了解,现在我们开始实际编码。首先是自定义一个类FlowLayout继承自ViewGroup。我先贴出全部源码,然后讲解。
/**
* 流式布局
*
* @author Jay
*/
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public FlowLayout(Context context) {
super(context, null);
}
/**
* 测量视图
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
// 最终测量的大小
int width = 0;
int height = 0;
// 每一行的宽高
int lineWidth = 0;
// 第N-1行的高度
int tempHeight = 0;
// 获取有多少个子布局
int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
// 获取子View
View child = getChildAt(index);
// 如果子View不可见
if (child.getVisibility() == View.GONE) {
continue;
}
// 测量子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获取子View的布局参数
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 获取子布局的宽高
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
// 如果一行放不下了,那么这个子View应该放到下一行
if (lineWidth + childWidth + getPaddingLeft() + getPaddingRight() > sizeWidth) {
tempHeight = height;// 记录N-1行的高度
width = Math.max(width, lineWidth);
lineWidth = childWidth;
height = height + childHeight;
} else {
lineWidth = lineWidth + childWidth;
height = Math.max(height, tempHeight + childHeight);
}
}
// 加上边距
height = height + getPaddingTop() + getPaddingBottom();
width = width + getPaddingLeft() + getPaddingRight();
Log.v("x", height + "/" + width);
setMeasuredDimension(
//
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width,
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height);
}
/**
* 视图布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取视图的宽度
int width = getWidth();
// 布局坐标,考虑到布局有可能设置了内边距
int left = getPaddingLeft();
int top = getPaddingTop();
// 一行中最高的一个view的高度
int maxHeight = 0;
int viewCount = getChildCount();
for (int index = 0; index < viewCount; index++) {
View child = getChildAt(index);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
// 换行
if (left + childWidth + getPaddingRight() > width) {
left = getPaddingLeft();
top = top + maxHeight;
maxHeight = 0;
} else {
// 得到最高高度
maxHeight = Math.max(childHeight, maxHeight);
}
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
left = left + childWidth;
}
}
/**
* 返回默认的LayoutParams,如果一个视图使用addView(View)添加而没有指定LayoutParams的时候
* 使用MarginLayoutParams当做LayoutParams
* 这个函数返回的类型就是View.getLayoutParams的返回值类型
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
首先自定义一个类FlowLayout去继承ViewGroup然后覆写其三个构造函数,还有覆写onMeasure()和onLayout()。下面还覆写了一个generateLayoutParams(),这个函数的作用是给子View也就是FlowLayout里面包含的View去设置默认的LayoutParams,这里使用MarginLayoutParams的原因是我们的子View有可能会设置外边距。
先来看看onMeasure()方法。
总体思路是如果测量模式是MeasureSpec.EXACTLY(match_parent),那么直接使用传递进来的Size,如果为自适应大小,那么我们就需要测量了,测量的思路是,遍历所有的子View,如果当前一行已经使用了的宽度+将放入的View的宽度大于总宽度,说明要换行了,将这个View放入下一行,具体请看代码,这样遍历完所有的View以后,则获取了FlowLayout的宽高,然后根据布局文件中宽高的配置设置大小。
onLayout()方法
onLayout()是用来分配子布局的位置,基本思路和上面onMeasure()的一样,遍历所有子View,然后根据每个子View的位置获取具体坐标然后设置,具体请看代码实现。
注意:计算需要考虑到内外边距,所以计算得细心。当然如果你不想自己实现,也可以直接下载源码中的FlowLayout.java文件,然后在布局文件中使用。
在布局文件中使用FlowLayout。使用方法和普通控件是一样的,只是需要带上包名。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.jay.flowlayout.FlowLayout
android:padding="5dp"
android:id="@+id/flowlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#E5E5F5">
</com.jay.flowlayout.FlowLayout>
</LinearLayout>
在Acitivity中进行动态添加控件就不再贴出了,可自行查看源码。
源码下载:360云盘 访问密码 8337
另外在慕课网有视频讲解,不过具体实现麻烦了一点,可以对照本博客共同学习