1.支持设置行间距、列间距
2.支持适配器模式动态添加
在onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法中进行测量工作,主要是对自定义ViewGroup 的宽高进行测量。这里需要引入几个变量:行高、行宽以及期望的总高度。
行宽:当前行的宽度,不能超过GroupView 所能提供的最大宽度。当要添加一个子view 时,需先进行计算,如果添加的行宽未超过GroupView 所能提供的最大宽度,则追加到当前行宽。如果添加后的行宽大于最大宽度,则不能添加到该行,需要另起一行,而新一行的初始行宽即为当前子view 的测量宽度。
行高:用于记录行的高度,值为当前行中最高的子view 的高度。当未发生换行时,不断和新的子view高度进行对比,取最大高度。当发生换行时,则需要把行高置为换行后第一个显示的子view 的高度。
期望的总高度:简单来说就是当高度设置为wrap_content 时,测量的高度,其计算公式为:ViewGroup的上下内间距 + 行高…+行高。当添加子view 后如果引起换行,那么就追加该行的高度到总高度上 。
伪代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (遍历子view) {
测量子view 的宽高
if (行宽 + 子view的宽度 + 左右内边距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
行宽 = 行宽 + 子view的宽度
行高 = 行高和子view 高度中的最大值
} else { // 添加子view后需要换行
// 更新最新一行的宽度为此child 的测量宽度
行宽 = 子view 的宽度;
// 期望高度追加当前行的行高。
期望的总高度 = 期望的总高度 + 行高
}
}
// 这里添加的是最后一行的高度。因为上面是在换行时才追加的行高,
// 在不需要换行时并没有追加行高,丢失了不满足换行条件下的行高。
// 举例说明:比如一行最多显示5个,但是当前只有1个,或者当前有6个的情况下,少了一行的行高。
期望的总高度 = 期望的总高度 + 行高;
// 期望的总高度追加内外边距
期望的总高度 = 期望的总高度 + 上下内边距
存储计算得到的宽高,即调用 android.view.View.setMeasuredDimension() 方法
}
在onLayout(boolean changed, int l, int t, int r, int b)方法中进行布局工作,计算子view 的起始位置,即顶部偏移量和左侧偏移量,待计算得出偏移量之后调用子view 的 android.view.View#layout 方法对其进行布局。这里需要引入几个变量:child的顶部偏移、child的左侧偏移、以及行高。
行高:用于记录当前行的最高高度,在需要换行时,追加该行高到顶部偏移量上,这样也就得出最新一行的顶部偏移距离了,同时更新最新的行高初始值为新的子view 高度。
child的顶部偏移:用于布局子view 的top 位置,同一行的子view 拥有相同的顶部偏移距离。在布局过程中,不断记录需要布局的子view 的宽高,如果布局后宽度超过ViewGroup 提供的最大宽度,则更新顶部偏移为追加行高后的值,即 顶部偏移 = 当前偏移量+当前行高。
child的左侧偏移:用于布局子view 的left 位置。当新布局的子view 布局后,如果宽度未超过ViewGroup 提供的最大宽度,则使用当前数据进行子view布局。如果宽度超过最大宽度,则换行显示,换行后的左侧偏移为初始值,而后进行子view 的布局工作。待改子view布局完成后,更新左侧偏移为下一个需要布局的子view的左侧偏移距离,即 下一个子view 的左侧偏移距离 = 当前左侧偏移 + 当前子view 的测量宽度。
伪代码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (遍历子view) {
测量子view 的宽高
if (左偏移量 + 子view的宽度 + 内边距 <= 支持的最大宽度) {// 该child加入后未超过一行宽度。
// 行高取这一行中最高的child height
行高 = 当前行高与新子view 高度中的最大值
} else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
左偏移距离 = 初始化数据
上偏移距离 = 上偏移距离 + 行高
行高 = 子view 的测量高度
}
// 子view 布局
child.layout(,,,);
// 更新左侧偏移距离,即下一个child 的left
左偏移距离 = 左偏移距离 + 当前子view 的测量宽度;
}
}
其实间距问题挺容易处理,无非就是在测量和布局的时候额外额外考虑一个值就好了。
首先来看行间距,无论是在测量还是在布局过程中,只有在发生换行的时候才需要考虑行间距。在测量过程中,当需要换行时,需要把行间距追加到期望的总高度中。而在布局过程中,当需要换行时,需要把行间距追加到顶部偏移量上就好了,这样也就表示了下一行的顶部位置中包含了行间距。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
......
if (行宽 + 子view的宽度 + 左右内边距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
......
} else { // 添加子view后需要换行
......
// 期望高度追加当前行的行高、行间距
期望的总高度 = 期望的总高度 + 行高 + 行间距
}
......
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
......
if (左偏移量 + 子view的宽度 + 内边距 <= 支持的最大宽度) {// 该child 加入后未超过一行宽度。
......
} else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
......
上偏移距离 = 上偏移距离 + 行高 + 行间距
......
}
......
}
其次来看下列间距,首先第一个子child 是不需要有列间距的,而其后的子view在进行测量时需要考虑到间距问题,在进行行宽计算的时候需要加上列间距,当不满足换行条件时,在更新行宽的时候除了要追加子view的测量宽度之外,还要额外添加对应的列间距。而在布局过程中,在当前子view布局完成之后,更新下一个子view的左侧偏移量时同样要把列间距计入在内。因为在换行的时候把左侧偏移量进行了初始化,所以并不会对最左侧子view产生影响。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (遍历子view) {
......
列间距 = 如果是第一个子view,则为0,否则为正常的设定值
if (行宽 + 子view的宽度 + 左右内边距 + 列间距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
行宽 = 行宽 + 子view的宽度 + 列间距
......
} else { // 添加子view后需要换行
......
}
}
......
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (遍历子view) {
......
// 更新左侧偏移距离(+间距),即下一个child 的left
左偏移距离 = 左偏移距离 + 当前子view 的测量宽度 + 列间距;
}
}
可使用 android.widget.BaseAdapter 的实现类进行动态添加,当前仅简单实现view 的添加,后续进行迭代优化。
/**
* 描述 : 流式view
* 作者 : shiguotao
* 版本 : V1
* 创建时间 : 2020/3/24 8:08 PM
*/
public class FlowLayout extends ViewGroup {
private final String TAG = "FlowLayout";
private float mRowSpacing = 0.0f;// 行间距
private float mColumnSpacing = 0.0f;// 列间距
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mRowSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_rowSpacing, 0);
mColumnSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_columnSpacing, 0);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int expectHeight = 0;// 期望高度,累加 child 的 height
int lineWidth = 0;// 单行宽度,动态计算当前行的宽度。
int lineHeight = 0;// 单行高度,取该行中高度最大的view
float widthSpacing;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 测量子view 的宽高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
widthSpacing = i == 0 ? 0 : mColumnSpacing;
// 这里进行的是预判断。追加该child 后,行宽
// 若未超过提供的最大宽度,则行宽需要追加child 的宽度,并且计算该行的最大高度。
// 若超过提供的最大宽度,则需要追加该行的行高,并且更新下一行的行宽为当前child 的测量宽度。
if (lineWidth + widthSpacing + childWidth + getPaddingLeft() + getPaddingRight() <= widthSize) {// 未超过一行
// 追加行宽。
lineWidth += widthSpacing + childWidth;
// 不断对比,获取该行的最大高度
lineHeight = Math.max(lineHeight, childHeight);
} else {// 超过一行
// 更新最新一行的宽度为此child 的测量宽度
lineWidth = childWidth;
// 期望高度追加当前行的行高。
expectHeight += lineHeight + mRowSpacing;
}
}
// 这里添加的是最后一行的高度。因为上面是在换行时才追加的行高,在不需要换行时并没有追加行高,丢失了不满足换行条件下的行高。
// 举例说明:比如一行最多显示5个,但是当前只有1个,或者当前有6个的情况下,少了一行的行高。
expectHeight += lineHeight;
// 追加ViewGroup 的内边距
expectHeight += getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthSize, resolveSize(expectHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int childLeftOffset = getPaddingLeft();// child view 的left偏移距离,用于记录左边位置。
int childTopOffset = getPaddingTop();// child view 的top偏移距离,用于记录顶部位置。
int lineHeight = 0;// 行高
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//跳过View.GONE的子View
if (child.getVisibility() == View.GONE) {
continue;
}
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (childLeftOffset + childWidth + getPaddingRight() <= width) {// 该child 加入后未超过一行宽度。
// 行高取这一行中最高的child height
lineHeight = Math.max(lineHeight, childHeight);
} else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
childLeftOffset = getPaddingLeft();
childTopOffset += lineHeight + mRowSpacing;
lineHeight = childHeight;
}
child.layout(childLeftOffset, childTopOffset, childLeftOffset + childWidth, childTopOffset + childHeight);
// 更新左侧偏移距离(+间距),即下一个child 的left
childLeftOffset += childWidth + mColumnSpacing;
}
}
public void setAdapter(BaseAdapter mAdapter) {
this.removeAllViews();
for (int i = 0; i < mAdapter.getCount(); i++) {
View view = mAdapter.getView(i, null, this);
this.addView(view);
}
requestLayout();
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}