code小生,一个专注 Android 领域的技术平台
公众号回复 Android 加入我的安卓技术群
作者:wo叫天然呆
链接:https://www.jianshu.com/p/2ed499fff984
声明:本文来自wo叫天然呆
投稿,转发等请联系原作者授权
最近在做一个聊天功能,其中需要给对方打标签,第一时间想到的就是流式布局,目前项目上用的是鸿洋大神的FlowLayout,功能很强大,不过我项目上只用到了展示效果,读了大神的源码,给了我一些灵感,这里我也写一个FlowLayout,并且参考了一些Recycler.Adapter的做法。
hongyangAndroid/FlowLayout
Android流式布局(FlowLayout)
自定义View、动画
使用SparseArraymLineDesArray;保存每行的数据,其中包括行高,行宽以及该行中包含的View的集合。
使用BaseTagFlowAdapter mAdapter;来管理数据加载,点击事件,选中事件。
在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 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);
}
}
Mapcompute(int flowWidth, int widthMeasureSpec, int heightMeasureSpec)是遍历子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;
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;
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;
在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(); }
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用于绑定相关数据,并处理点击,选中等事件。
使用SparseIntArray mLayoutResIds保存layoutId,实现多布局样式。
使用SparseArray
使用HashMapmCheckedPosList保存选中的View,实现单选,多选等功能
像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); } }
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);
}
}
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<>(); }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<>();
}
欢迎大家留言指出我的不足。
支持,「在看」