Android 基于 ViewGroup 实现流式布局

作者 : 开心源码 本文共8341个字,预计阅读时间需要21分钟 发布时间: 2022-05-12 共134人阅读

场景

最近在做一个聊天功能,其中需要给对方打标签,第一时间想到的就是流式布局,目前项目上用的是鸿洋大神的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 实现流式布局

发表回复