Android 基于 ViewGroup 实现流式布局
场景
最近在做一个聊天功能,其中需要给对方打标签,第一时间想到的就是流式布局,目前项目上用的是鸿洋大神的FlowLayout,功能很强大,不过我项目上只用到了展现效果,读了大神的源码,给了我少量灵感,这里我也写一个FlowLayout,并且参考了少量Recycler.Adapter的做法。
参考资料
hongyangAndroid/FlowLayout
Android流式布局(FlowLayout)
自己设置View、动画
实现功能
- 使用adapter的形式绑定并解决数据
- 支持多种布局一同展现
- 支持多行,单行,指定显示行数
- 支持Item左对齐,居中对齐,右对齐
- 支持行布局顶部对齐,居中对齐,底部对齐
- 支持选中状态
- 支持设置行间距
- 支持设置item间距
ScFlowLayout
1.思路
使用SparseArraymLineDesArray;保存每行的数据,其中包括行高,行宽以及该行中包含的View的集合。
使用BaseTagFlowAdapter mAdapter;来管理数据加载,点击事件,选中事件。
2.onMeasure()中的业务逻辑
在onMeasure()中
首先遍历测量子View,将子View的顶点坐标通过view.setTag()方法保存,同时把每行的数据保存在LineDes中,这样写是为了后续在onLayout()好解决,不用重复计算。
接下来我们在调用setMeasuredDimension()方法之前需要给出布局的宽跟高,我这边是通过getLayoutParams().width与getLayoutParams().height来判断布局的宽高,至于为什么要这样写大家可以参考这篇文章,假如想要实现指定行数的话需要遍历每行高度,而后累加到mMeasuredHeight中。
在计算高度的时候,因为我这里实现了自己设置行间距,因而实际计算高度的时候还需要加上行间距的高度。
//因为计算子view所占宽度 Map<String, Integer> 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;
得到每个子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<View> 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<arraylist> mCheckedStateViewResIds保存需要实现选中状态的子ViewId
使用HashMapmCheckedPosList保存选中的View,实现单选,多选等功能</arraylist
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<Integer> viewResId = mCheckedStateViewResIds.get(viewType, new ArrayList<Integer>()); 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<Integer, TagView> 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<View> views; private final LinkedHashSet<Integer> childClickViewIds;//需要增加点击事件的子View private final LinkedHashSet<Integer> itemChildLongClickViewIds;//需要增加点击事件的子View private final HashSet<Integer> 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<>(); }
更多资料分享欢迎Android工程师朋友们加入安卓开发技术进阶互助:856328774免费提供安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于Android面试的题目汇总。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android 基于 ViewGroup 实现流式布局