先上一张我的实现效果图:
想要实现的效果和思路:向这个布局容器中添加的组件填充在一行里,宽度超出该行剩余空间时转到下一行,该行剩余空间由该行的几个组件平分。布局类负责安排每一行的组件和行的纵坐标,行对象负责安排自己行里组件的位置。最后为了美观将这些组件赋予随机的颜色值即可。
1.在attr.xml中定义两个属性,每个组件的水平间距和行间距:
<declare-styleable name="FlowLayout">
<attr name="space_horizontal" format="dimension"></attr>
<attr name="space_vertical" format="dimension"></attr>
</declare-styleable>
private Context context;
private float space_horizontal,space_vertical;
//除去两旁空白后的行宽度
private int maxWidth;
//储存每一行的列表
private List<Line> mLines = new ArrayList();
//当前行
private Line mCurrentLine = null;
3.从构造器中获取用户给予的属性,这里把一个参数的构造器也写上,让它调用两个参数的方法:
public FlowLayout(Context context) {
super(context,null);
this.context = context;
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//获取用户赋予的属性,包括水平和竖直标签的间距
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.FlowLayout);
space_horizontal = array.getDimension(R.styleable.FlowLayout_space_horizontal,0);
space_vertical = array.getDimension(R.styleable.FlowLayout_space_vertical,0);
array.recycle();
this.context = context;
}
4.测量方法,要遍历孩子进行测量,至于测量自己,宽度就是传入的宽度,高度这里定义为所有行加和:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 每次测量之前都先清空集合,不让会覆盖掉以前
mLines.clear();
mCurrentLine = null;
// 获取总宽度
int width = MeasureSpec.getSize(widthMeasureSpec);
// 计算最大的宽度
maxWidth = width - getPaddingLeft() - getPaddingRight();
// ******************** 测量孩子 ********************
// 遍历获取孩子
int childCount = this.getChildCount();
//Toast.makeText(context,childCount,Toast.LENGTH_SHORT).show();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 测量孩子
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// 测量完需要将孩子添加到管理行的孩子的集合中,将行添加到管理行的集合中
if (mCurrentLine == null) {
// 初次添加第一个孩子的时候
mCurrentLine = new Line(maxWidth, space_horizontal);
// 添加孩子
mCurrentLine.addView(childView);
// 添加行
mLines.add(mCurrentLine);
} else {
// 行中有孩子的时候,判断时候能添加
if (mCurrentLine.canAddView(childView)) {
// 继续往该行里添加
mCurrentLine.addView(childView);
} else {
// 添加到下一行
mCurrentLine = new Line(maxWidth, space_horizontal);
mCurrentLine.addView(childView);
mLines.add(mCurrentLine);
}
}
}
// ******************** 测量自己 *********************
// 测量自己只需要计算高度,宽度肯定会被填充满的
int height = getPaddingTop() + getPaddingBottom();
for (int i = 0; i < mLines.size(); i++) {
// 所有行的高度
height += mLines.get(i).height;
}
// 所有竖直的间距
height += (mLines.size() - 1) * space_vertical;
// 测量
setMeasuredDimension(width, height);
}
5.布局方法,确定每一行的纵坐标,也就是把每一行的高度传给行,最终每个组件调用layout方法布局是在行的方法中:
@Override
protected void onLayout(boolean changed, int l, int t, int i2, int i3) {
// 这里只负责高度的位置,具体的宽度和子孩子的位置让具体的行去管理
l = getPaddingLeft();
t = getPaddingTop();
for (int i = 0; i < mLines.size(); i++) {
// 获取行
Line line = mLines.get(i);
// 管理
line.layout(t, l);
// 更新高度
t += line.height;
if (i != mLines.size() - 1) {
// 不是最后一条就添加间距
t += space_vertical;
}
}
}
6.行对象类,注意添加组件时的判断逻辑,判断是否宽度超出改行剩余空间。每行的高度是根据最高的组件确定:
public class Line {
// 定义一个行的集合来存放子View
private List<View> views = new ArrayList<>();
// 行的最大宽度
private int maxWidth;
// 行中已经使用的宽度
private int usedWidth;
// 行的高度
private int height;
// 孩子之间的距离
private float space;
// 通过构造初始化最大宽度和边距
public Line(int maxWidth, float horizontalSpace) {
this.maxWidth = maxWidth;
this.space = horizontalSpace;
}
/**
* 往集合里添加孩子
*/
public void addView(View view) {
int childWidth = view.getMeasuredWidth();
int childHeight = view.getMeasuredHeight();
// 更新行的使用宽度和高度
if (views.size() == 0) {
// 集合里没有孩子的时候
if (childWidth > maxWidth) {
usedWidth = maxWidth;
height = childHeight;
} else {
usedWidth = childWidth;
height = childHeight;
}
} else {
usedWidth += childWidth + space;
height = childHeight > height ? childHeight : height;
}
// 添加孩子到集合
views.add(view);
}
/**
* 判断当前的行是否能添加孩子
*
* @return
*/
public boolean canAddView(View view) {
// 集合里没有数据可以添加
if (views.size() == 0) {
return true;
}
// 最后一个孩子的宽度大于剩余宽度就不添加
if (view.getMeasuredWidth() > (maxWidth - usedWidth - space)) {
return false;
}
// 默认可以添加
return true;
}
/**
* 指定孩子显示的位置
*
* @param t
* @param l
*/
public void layout(int t, int l) {
// 平分剩下的空间
int avg = (maxWidth - usedWidth) / views.size();
// 循环指定孩子位置
for (View view : views) {
// 获取宽高
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
// 重新测量
view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));
// 重新获取宽度值
measuredWidth = view.getMeasuredWidth();
int top = t;
int left = l;
int right = measuredWidth + left;
int bottom = measuredHeight + top;
// 指定位置
view.layout(left, top, right, bottom);
// 更新数据
l += measuredWidth + space;
}
}
}
——————————————————————————————————————————————————————————————————
FlowLayout类完整代码:
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 流式布局,把长宽不一的子项流式排列
* Created by lenovo on 2016/10/16.
*/
public class FlowLayout extends ViewGroup{
private Context context;
private float space_horizontal,space_vertical;
//除去两旁空白后的行宽度
private int maxWidth;
//储存每一行的列表
private List<Line> mLines = new ArrayList();
//当前行
private Line mCurrentLine = null;
public FlowLayout(Context context) {
super(context,null);
this.context = context;
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//获取用户赋予的属性,包括水平和竖直标签的间距
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.FlowLayout);
space_horizontal = array.getDimension(R.styleable.FlowLayout_space_horizontal,0);
space_vertical = array.getDimension(R.styleable.FlowLayout_space_vertical,0);
array.recycle();
this.context = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 每次测量之前都先清空集合,不让会覆盖掉以前
mLines.clear();
mCurrentLine = null;
// 获取总宽度
int width = MeasureSpec.getSize(widthMeasureSpec);
// 计算最大的宽度
maxWidth = width - getPaddingLeft() - getPaddingRight();
// ******************** 测量孩子 ********************
// 遍历获取孩子
int childCount = this.getChildCount();
//Toast.makeText(context,childCount,Toast.LENGTH_SHORT).show();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 测量孩子
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// 测量完需要将孩子添加到管理行的孩子的集合中,将行添加到管理行的集合中
if (mCurrentLine == null) {
// 初次添加第一个孩子的时候
mCurrentLine = new Line(maxWidth, space_horizontal);
// 添加孩子
mCurrentLine.addView(childView);
// 添加行
mLines.add(mCurrentLine);
} else {
// 行中有孩子的时候,判断时候能添加
if (mCurrentLine.canAddView(childView)) {
// 继续往该行里添加
mCurrentLine.addView(childView);
} else {
// 添加到下一行
mCurrentLine = new Line(maxWidth, space_horizontal);
mCurrentLine.addView(childView);
mLines.add(mCurrentLine);
}
}
}
// ******************** 测量自己 *********************
// 测量自己只需要计算高度,宽度肯定会被填充满的
int height = getPaddingTop() + getPaddingBottom();
for (int i = 0; i < mLines.size(); i++) {
// 所有行的高度
height += mLines.get(i).height;
}
// 所有竖直的间距
height += (mLines.size() - 1) * space_vertical;
// 测量
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int i2, int i3) {
// 这里只负责高度的位置,具体的宽度和子孩子的位置让具体的行去管理
l = getPaddingLeft();
t = getPaddingTop();
for (int i = 0; i < mLines.size(); i++) {
// 获取行
Line line = mLines.get(i);
// 管理
line.layout(t, l);
// 更新高度
t += line.height;
if (i != mLines.size() - 1) {
// 不是最后一条就添加间距
t += space_vertical;
}
}
}
/**
* 内部类,行管理器,管理每一行的孩子
*/
public class Line {
// 定义一个行的集合来存放子View
private List<View> views = new ArrayList<>();
// 行的最大宽度
private int maxWidth;
// 行中已经使用的宽度
private int usedWidth;
// 行的高度
private int height;
// 孩子之间的距离
private float space;
// 通过构造初始化最大宽度和边距
public Line(int maxWidth, float horizontalSpace) {
this.maxWidth = maxWidth;
this.space = horizontalSpace;
}
/**
* 往集合里添加孩子
*/
public void addView(View view) {
int childWidth = view.getMeasuredWidth();
int childHeight = view.getMeasuredHeight();
// 更新行的使用宽度和高度
if (views.size() == 0) {
// 集合里没有孩子的时候
if (childWidth > maxWidth) {
usedWidth = maxWidth;
height = childHeight;
} else {
usedWidth = childWidth;
height = childHeight;
}
} else {
usedWidth += childWidth + space;
height = childHeight > height ? childHeight : height;
}
// 添加孩子到集合
views.add(view);
}
/**
* 判断当前的行是否能添加孩子
*
* @return
*/
public boolean canAddView(View view) {
// 集合里没有数据可以添加
if (views.size() == 0) {
return true;
}
// 最后一个孩子的宽度大于剩余宽度就不添加
if (view.getMeasuredWidth() > (maxWidth - usedWidth - space)) {
return false;
}
// 默认可以添加
return true;
}
/**
* 指定孩子显示的位置
*
* @param t
* @param l
*/
public void layout(int t, int l) {
// 平分剩下的空间
int avg = (maxWidth - usedWidth) / views.size();
// 循环指定孩子位置
for (View view : views) {
// 获取宽高
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
// 重新测量
view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));
// 重新获取宽度值
measuredWidth = view.getMeasuredWidth();
int top = t;
int left = l;
int right = measuredWidth + left;
int bottom = measuredHeight + top;
// 指定位置
view.layout(left, top, right, bottom);
// 更新数据
l += measuredWidth + space;
}
}
}
}
然后在活动布局中加入这个布局组件,往里面加组件就可以了。