ScFlowLayout一款Android基于ViewGroup实现的流式布局
本文原创,转载请注明出处。欢迎关注我的 。
安利一波我写的开发框架:MyScFrame喜欢的话就给个Star
ScFlowLayout
已经加入到该框架内
场景
最近在做一个聊天功能,其中需要给对方打标签,第一时间想到的就是流式布局,目前项目上用的是鸿洋大神的FlowLayout,功能很强大,不过我项目上只用到了展示效果,读了大神的源码,给了我一些灵感,这里我也写一个FlowLayout,并且参考了一些Recycler.Adapter的做法。
参考资料
hongyangAndroid/FlowLayout
Android流式布局(FlowLayout)
自定义View、动画
实现功能
- 使用adapter的形式绑定并处理数据
- 支持多种布局一同展示
- 支持多行,单行,指定显示行数
- 支持Item左对齐,居中对齐,右对齐
- 支持行布局顶部对齐,居中对齐,底部对齐
- 支持选中状态
- 支持设置行间距
- 支持设置item间距
ScFlowLayout
1.思路
使用SparseArray
保存每行的数据,其中包括行高,行宽以及该行中包含的View的集合。
使用BaseTagFlowAdapter mAdapter;
来管理数据加载,点击事件,选中事件。
2.onMeasure()中的业务逻辑
在onMeasure()
中
首先遍历测量子View,将子View的顶点坐标通过view.setTag()
方法保存,同时把每行的数据保存在LineDes
中,这样写是为了后续在onLayout()
好处理,不用重复计算。
接下来我们在调用setMeasuredDimension()
方法之前需要给出布局的宽跟高,我这边是通过getLayoutParams().width
与getLayoutParams().height
来判断布局的宽高,至于为什么要这样写大家可以参考这篇文章,如果想要实现指定行数的话需要遍历每行高度,然后累加到mMeasuredHeight
中。
在计算高度的时候,由于我这里实现了自定义行间距,因此实际计算高度的时候还需要加上行间距的高度。
//由于计算子view所占宽度
Map compute = compute(widthSize, widthMeasureSpec, heightMeasureSpec);
mMeasuredWidth = widthSize;
mMeasuredHeight = heightSize;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
mMeasuredWidth = compute.get(ALL_CHILD_WIDTH);
}
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
mMeasuredHeight = compute.get(ALL_CHILD_HEIGHT);
if (mLineDesArray.size() > 1) {
//加上行间距
mMeasuredHeight += mLineSpace * (mLineDesArray.size() - 1);
}
}
if (mMaxShowRow != 0) {
mMeasuredHeight = 0;
int lineCount = Math.min(mLineDesArray.size(), mMaxShowRow);
for (int i = 0; i < lineCount; i++) {
mMeasuredHeight += mLineDesArray.get(i).rowsMaxHeight;
}
mMeasuredHeight += getPaddingBottom();
if (lineCount > 1) {
//加上行间距
mMeasuredHeight += mLineSpace * (lineCount - 1);
}
}
Map
是遍历子View的方法(整个控件都靠它了)
我们先要设置几个参数:
int lineIndex
行数
int rowsWidth
当前行已占宽度
int columnHeight
当前行顶部已占高度
int rowsMaxHeight
当前行所有子元素的最大高度(用于换行累加高度)
LineDes lineDes
保存每行数据的bean类
思路是先遍历所有子View,然后计算出每个子View所占用的宽高,child.getMeasuredWidth()
计算出来的是包含子View中的Padding
参数,但是不包含Margin
,所以这里实际宽高还需要加上Margin
不然会导致实际大小与计算出来的不符
//遍历去调用所有子元素的measure方法(child.getMeasuredHeight()才能获取到值,否则为0)
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//获取元素测量宽度和高度
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
//获取元素的margin
marginParams = (MarginLayoutParams) child.getLayoutParams();
//子元素所占宽度 = MarginLeft+ child.getMeasuredWidth+MarginRight 注意此时不能child.getWidth,因为界面没有绘制完成,此时wdith为0
int childWidth = marginParams.leftMargin + marginParams.rightMargin + measuredWidth;
int childHeight = marginParams.topMargin + marginParams.bottomMargin + measuredHeight;
得到每个子View的宽高后就要开始计算行数以及每行所存放的View的数量了
我们之前已经有了一个rowsWidth
参数,默认值是getPaddingLeft()
,然后加上childWidth
看是否会超过父布局的宽度,这边还需要减去一个getPaddingRight()
切记切记!,如果超了,表示这个View已经无法存放在该行,需要换行。最后使用Rect
把子View的宽高赋值进去,然后保存在tag中,方便后续使用。
//该布局添加进去后会超过总宽度->换行
if (rowsWidth + childWidth > flowWidth - getPaddingRight()) {
getLineDesArray().put(lineIndex, lineDes);
lineDes = new LineDes();
lineIndex++;
//重置行宽度
rowsWidth = getPaddingLeft();
//累加上该行子元素最大高度
columnHeight += rowsMaxHeight;
//重置该行最大高度
rowsMaxHeight = childHeight;
} else {
rowsMaxHeight = Math.max(rowsMaxHeight, childHeight);
}
//累加上该行子元素宽度
rowsWidth += childWidth;
// 判断时占的宽段时加上margin计算,设置顶点位置时不包括margin位置,
// 不然margin会不起作用,这是给View设置tag,在onlayout给子元素设置位置再遍历取出
Rect rect = new Rect(
rowsWidth - childWidth + marginParams.leftMargin,
columnHeight + marginParams.topMargin,
rowsWidth - marginParams.rightMargin,
columnHeight + childHeight - marginParams.bottomMargin);
child.setTag(rect);
lineDes.rowsMaxHeight = rowsMaxHeight;
lineDes.rowsMaxWidth = rowsWidth;
lineDes.views.add(child);
//累加上item间距
rowsWidth += mItemSpace;
3.onLayout()中的业务逻辑
在onLayout()
中,通过所需要实现的类型去做不同的排版
因为这里我们实现了行间上,中,下与Item间的左,中,右对齐因此,这里需要有针对行与Item做两次处理
我们先设置一个diffvalue
用于存放位移参数。
为了让行内所有布局都居中对齐或下对齐,那么我们要先知道每行有多少个元素,以及行高与元素高度,这个时候LineDes
就派上用场了,之前在onMeasure()
中我们已经计算并保存了LineDes
,现在只需要遍历LineDes
即可,由于系统在绘制的时候就是使用顶部对齐,因此LINE_GRAVITY_TOP
不需要做处理,我们只需要处理LINE_GRAVITY_CENTER
和LINE_GRAVITY_BOTTOM
即可
LINE_GRAVITY_CENTER
:diffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
LINE_GRAVITY_BOTTOM
:diffvalue = lineDes.rowsMaxHeight - childWidth;
再来说一下Item间的排版,同样的TAG_GRAVITY_LEFT
可以不做处理
LINE_GRAVITY_CENTER
:diffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
LINE_GRAVITY_BOTTOM
:diffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();
改完重新写入Rect
中并传入子View的layout()
中即可。
private synchronized void formatAboveLine(int lineGravity) {
int lineIndex = getLineDesArray().size();
for (int i = 0; i < lineIndex; i++) {
LineDes lineDes = getLineDesArray().get(i);
List views = lineDes.views;
int viewIndex = views.size();
for (int j = 0; j < viewIndex; j++) {
View child = views.get(j);
Rect rect = (Rect) child.getTag();
int childWidth = (rect.bottom - rect.top);
//如果是当前行的高度大于了该view的高度话,此时需要重新放该view了
int diffvalue = 0;
if (childWidth < lineDes.rowsMaxHeight) {
switch (lineGravity) {
case LINE_GRAVITY_TOP:
break;
case LINE_GRAVITY_CENTER:
diffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
rect.top += diffvalue;
rect.bottom += diffvalue;
break;
case LINE_GRAVITY_BOTTOM:
diffvalue = lineDes.rowsMaxHeight - childWidth;
rect.top += diffvalue;
rect.bottom += diffvalue;
break;
default:
break;
}
}
switch (mTagGravity) {
case TAG_GRAVITY_LEFT:
break;
case TAG_GRAVITY_CENTER:
diffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
if (diffvalue > 0) {
rect.left += diffvalue;
rect.right += diffvalue;
}
break;
case TAG_GRAVITY_RIGHT:
diffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();
rect.left += diffvalue;
rect.right += diffvalue;
break;
default:
break;
}
//加上行间距
rect.top += mLineSpace * i;
rect.bottom += mLineSpace * i;
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
getLineDesArray().clear();
}
适配器
参考BaseRecyclerViewAdapterHelper
实现的一个Adapter
与ViewHolder
用于绑定相关数据,并处理点击,选中等事件。
1.思路
使用SparseIntArray mLayoutResIds
保存layoutId,实现多布局样式。
使用SparseArray
保存需要实现选中状态的子ViewId
使用HashMap
保存选中的View,实现单选,多选等功能
2.加载布局
像RecyclerView.Adapter
一样,我们把data
传进来,然后遍历数据,通过ViewType
来判断到底使用mLayoutResIds
中的哪个布局,并且遍历 mCheckedStateViewResIds
对需要做选中状态变更的view设置setDuplicateParentStateEnabled(true)
,然后把实例出来的View传入ViewHolder
最后加载出来。
private void addNewView() {
mFlowLayout.removeAllViews();
mCheckedPosList.clear();
TagView tagViewContainer = null;
K baseViewHolder = null;
T data = null;
int viewType = DEFAULT_VIEW_TYPE;
for (int i = 0; i < getCount(); i++) {
data = getItem(i);
viewType = getDefItemViewType(data);
baseViewHolder = onCreateViewHolder(mFlowLayout, viewType, i);
tagViewContainer = new TagView(mContext);
//关键代码,使得内部View可以使用TagView的状态
if (mCheckedStateViewResIds != null) {
ArrayList viewResId = mCheckedStateViewResIds.get(viewType, new ArrayList());
for (Integer stateViewId : viewResId) {
View stateView = baseViewHolder.getView(stateViewId.intValue());
if (stateView != null) {
stateView.setDuplicateParentStateEnabled(true);
}
}
}
baseViewHolder.itemView.setDuplicateParentStateEnabled(true);
if (baseViewHolder.itemView.getLayoutParams() != null) {
tagViewContainer.setLayoutParams(baseViewHolder.itemView.getLayoutParams());
} else {
ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
tagViewContainer.setLayoutParams(lp);
}
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
baseViewHolder.itemView.setLayoutParams(lp);
tagViewContainer.addView(baseViewHolder.itemView);
//处理选中与非选中逻辑
if (setDefSelected(data, i)) {
if (mSelectedMax == 1 && mCheckedPosList.size() > 0) {
int oldSelected = 0;
TagView oldTagView;
for (Map.Entry entry : mCheckedPosList.entrySet()) {
oldSelected = entry.getKey();
oldTagView = entry.getValue();
setChildUnChecked(oldSelected, oldTagView);
}
mCheckedPosList.clear();
}
mCheckedPosList.put(i, tagViewContainer);
setChildChecked(i, tagViewContainer);
}
mFlowLayout.addView(tagViewContainer);
convert(baseViewHolder, data);
bindViewClickListener(tagViewContainer, baseViewHolder);
}
}
3.ViewHolder
ViewHolder
里面只是保存一些常用数据,方便在使用的时候调用
private final SparseArray views;
private final LinkedHashSet childClickViewIds;//需要添加点击事件的子View
private final LinkedHashSet itemChildLongClickViewIds;//需要添加点击事件的子View
private final HashSet nestViews;//需要添加两种点击事件的子View
public final View itemView;
private BaseTagFlowAdapter adapter;
private int position = -1;
private int viewType = BaseTagFlowAdapter.DEFAULT_VIEW_TYPE;
public BaseTagFlowViewHolder(final View view) {
this.itemView = view;
this.views = new SparseArray<>();
this.childClickViewIds = new LinkedHashSet<>();
this.itemChildLongClickViewIds = new LinkedHashSet<>();
this.nestViews = new HashSet<>();
}
欢迎大家留言指出我的不足。