美文网首页Android开发程序员Android技术知识
Android 基于 ViewGroup 实现流式布局

Android 基于 ViewGroup 实现流式布局

作者: Android开发架构 | 来源:发表于2019-05-16 16:59 被阅读18次

    场景

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

    相关文章

      网友评论

        本文标题:Android 基于 ViewGroup 实现流式布局

        本文链接:https://www.haomeiwen.com/subject/xyaiaqtx.html