记一次`RecyclerView`嵌套`FlowLayout`滑动后`FlowLayout`子`View`内容丢失问题的排查解决过程

记一次RecyclerView嵌套FlowLayout滑动后FlowLayoutView内容丢失问题的排查解决过程


布局基础可参考: Android 手把手教您自定义ViewGroup(一)

一、需求及问题描述

1.1 业务需求

因业务需求需要实现一个纵向列表,列表中有一系列类似标签的相似内容,数量不定,需要换行,第一印象考虑用嵌套RecyclerView的方式实现,但是子RecyclerView需要指定每行元素的个数,考虑到更好的兼容性,采用嵌套RecyclerView嵌套FlowLayout的方式实现。

1.2 问题描述

在列表滑动过程中,当初始的一屏滑出屏幕外时,继续滑动,后面出现的itemFlowLayut的内容为空白,如果上滑,原来有内容的itemFlowLayout中内容也变为空白了。

二、问题排查

1.1 初步分析

根据问题描述初步分析,可能是RecyclerViewitem复用导致出现的问题,经过各种尝试,包括给ViewHolder设置tag等方式,都未能解决,最后索性禁用RecyclerView的复用机制,问题得以解决:

override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
    holder.setIsRecyclable(false)
    super.onBindViewHolder(holder, position)
}

1.2 深入分析

禁用RecyclerView的复用机制虽然能解决问题,但是却极大牺牲了列表的性能,有些得不偿失,所以考虑深入分析问题,力求找到更加优雅的解决方式。

后仔细分析,RecyclerViewitem View,中除了FlowLayout外还有其它的View,而这些View的内容不会随着列表的滚动而消失,所以考虑可能是FlowLayout自身的问题。

FlowLayout单独拿出来分析,第一步先在初始化的时候添加一些View,一切正常;第二步调用removeAllViews()方法,然后继续添加一些子View,此时复现问题。作为对比,对LinearLayout做相同操作,此时每一步都是正常的,所以断定是FlowLayout自身的问题。

问题范围缩小后,利用 开发者工具边界布局 功能,并且对于FlowLayout做断点和日志一步一步分析,先是确定第二步添加完子View之后,获取这些子View的数量是正确的,然后排查View的大小,测试可知添加的子Viewview.getMeasuredWidth()大小是正确的,但是view.getWidth()大小为0,说明FlowLayoutonMeasure()方法是没有问题的,而子View并未得到正确绘制,问题可能出在onLayout()方法上,因为FlowLayout是自定义ViewGroup,并且未涉及到绘制,所以不考虑onDraw()方法。在onLayout(boolean changed, int l, int t, int r, int b)方法中添加日志,发现其代码逻辑只在changed参数为true时才进行重新摆放的操作逻辑,而在FlowLayout首次添加子View时,其changed参数才为true,在removeAllViews()后再重新添加子View时,changed参数为false,此时便没有执行摆放逻辑,导致子View的大小便为0,即出现最开始的问题。

解决问题也很简单,即不考虑changed参数,在任何时候都进行摆放操作逻辑就可以了。经过验证,问题的确可以解决。

三、源码

3.1 FlowLayout

package com.joywifi.vlottery.widget;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.IntDef;
import com.blankj.utilcode.util.LogUtils;
import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
    private Line mLine = null;
    public static final int DEFAULT_SPACING = 20;
    //所有的子控件
    private SparseArray<View> mViews;
    /**
     * 横向间隔
     */
    private int mHorizontalSpacing = DEFAULT_SPACING;
    /**
     * 纵向间隔
     */
    private int mVerticalSpacing = DEFAULT_SPACING;

    /**
     * 当前行已用的宽度,由子View宽度加上横向间隔
     */
    private int mUsedWidth = 0;
    /**
     * 代表每一行的集合
     */
    private final List<Line> mLines = new ArrayList<Line>();
    //子View的对齐方式
    private int isAlignByCenter = 1;

    /**
     * 最大的行数
     */
    private int mMaxLinesCount = Integer.MAX_VALUE;
    /**
     * 是否需要布局,只用于第一次
     */
    boolean mNeedLayout = true;

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public interface AlienState {
        int RIGHT = 0;
        int LEFT = 1;
        int CENTER = 2;

        @IntDef(value = {RIGHT, LEFT, CENTER})
        @interface Val {
        }
    }

    //设置第二行的位置
    public void setAlignByCenter(@AlienState.Val int isAlignByCenter) {
        this.isAlignByCenter = isAlignByCenter;
        requestLayoutInner();
    }

    protected void requestLayoutInner() {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                requestLayout();
            }
        });
    }

    //设置 要添加的数据   子布局样式
    public void setAdapter(List<?> list, int res, FlowSetData mItemView) {
        if (list == null) {
            return;
        }
        removeAllViews();
        int layoutPadding = dipToPx(getContext(), 8);
        setHorizontalSpacing(layoutPadding);
        setVerticalSpacing(layoutPadding);
        int size = list.size();
        for (int i = 0; i < size; i++) {
            Object item = list.get(i);
            View inflate = LayoutInflater.from(getContext()).inflate(res, null);
            if (inflate != null) {
                if (item != null) {
                    mItemView.getCover(item, new FlowViewHolder(inflate), inflate, i);
                    addView(inflate);
                }
            }
        }
    }

    public class FlowViewHolder {
        View mConvertView;

        public FlowViewHolder(View mConvertView) {
            this.mConvertView = mConvertView;
            mViews = new SparseArray<>();
        }

        public <T extends View> T getView(int viewId) {
            View view = mViews.get(viewId);
            if (view == null) {
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            }
            try {
                return (T) view;
            } catch (ClassCastException e) {
                e.printStackTrace();
            }
            return null;
        }

        public void setText(int viewId, String text) {
            TextView view = getView(viewId);
            view.setText(text);
        }
    }

    public static int dipToPx(Context ctx, float dip) {
        return (int) TypedValue.applyDimension(1, dip, ctx.getResources().getDisplayMetrics());
    }

    public void setHorizontalSpacing(int spacing) {
        if (mHorizontalSpacing != spacing) {
            mHorizontalSpacing = spacing;
            requestLayoutInner();
        }
    }

    public void setVerticalSpacing(int spacing) {
        if (mVerticalSpacing != spacing) {
            mVerticalSpacing = spacing;
            requestLayoutInner();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        restoreLine();// 还原数据,以便重新记录
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight);
            // 测量child
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            if (mLine == null) {
                mLine = new Line();
            }
            int childWidth = child.getMeasuredWidth();
            mUsedWidth += childWidth;// 增加使用的宽度
            if (mUsedWidth <= sizeWidth) {// 使用宽度小于总宽度,该child属于这一行。
                mLine.addView(child);// 添加child
                mUsedWidth += mHorizontalSpacing;// 加上间隔
                if (mUsedWidth >= sizeWidth) {// 加上间隔后如果大于等于总宽度,需要换行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {// 使用宽度大于总宽度。需要换行
                if (mLine.getViewCount() == 0) {// 如果这行一个child都没有,即使占用长度超过了总长度,也要加上去,保证每行都有至少有一个child
                    mLine.addView(child);// 添加child
                    if (!newLine()) {// 换行
                        break;
                    }
                } else {// 如果该行有数据了,就直接换行
                    if (!newLine()) {// 换行
                        break;
                    }
                    // 在新的一行,不管是否超过长度,先加上去,因为这一行一个child都没有,所以必须满足每行至少有一个child
                    mLine.addView(child);
                    mUsedWidth += childWidth + mHorizontalSpacing;
                }
            }
        }

        if (mLine != null && mLine.getViewCount() > 0 && !mLines.contains(mLine)) {
            // 由于前面采用判断长度是否超过最大宽度来决定是否换行,则最后一行可能因为还没达到最大宽度,所以需要验证后加入集合中
            mLines.add(mLine);
        }

        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
        int totalHeight = 0;
        final int linesCount = mLines.size();
        for (int i = 0; i < linesCount; i++) {// 加上所有行的高度
            totalHeight += mLines.get(i).mHeight;
        }
        totalHeight += mVerticalSpacing * (linesCount - 1);// 加上所有间隔的高度
        totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding
        // 设置布局的宽高,宽度直接采用父view传递过来的最大宽度,而不用考虑子view是否填满宽度,因为该布局的特性就是填满一行后,再换行
        // 高度根据设置的模式来决定采用所有子View的高度之和还是采用父view传递过来的高度
        setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));
    }

    private void restoreLine() {
        mLines.clear();
        mLine = new Line();
        mUsedWidth = 0;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //if (changed) { //处理removeAllViews然后再addView()不成功的问题
            int left = getPaddingLeft();//获取最初的左上点
            int top = getPaddingTop();
            int count = mLines.size();
            for (int i = 0; i < count; i++) {
                Line line = mLines.get(i);
                line.LayoutView(left, top);//摆放每一行中子View的位置
                top += line.mHeight + mVerticalSpacing;//为下一行的top赋值
            }
        //}
    }

    /**
     * 新增加一行
     */
    private boolean newLine() {
        mLines.add(mLine);
        if (mLines.size() < mMaxLinesCount) {
            mLine = new Line();
            mUsedWidth = 0;
            return true;
        }
        return false;
    }

    public class Line {
        int mWidth = 0;// 该行中所有的子View累加的宽度
        int mHeight = 0;// 该行中所有的子View中高度的那个子View的高度
        List<View> views = new ArrayList<View>();

        public void addView(View view) {// 往该行中添加一个
            views.add(view);
            mWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            mHeight = mHeight < childHeight ? childHeight : mHeight;//高度等于一行中最高的View
        }

        public int getViewCount() {
            return views.size();
        }

        //摆放行中子View的位置
        public void LayoutView(int l, int t) {
            int left = l;
            int top = t;
            int count = getViewCount();
            int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//行的总宽度
            //剩余的宽度,是除了View和间隙的剩余空间
            int surplusWidth = layoutWidth - mWidth - mHorizontalSpacing * (count - 1);
            if (surplusWidth >= 0) {
                for (int i = 0; i < count; i++) {
                    final View view = views.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    //计算出每个View的顶点,是由最高的View和该View高度的差值除以2
                    int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);
                    if (topOffset < 0) {
                        topOffset = 0;
                    }

                    //布局View
                    if (i == 0) {
                        switch (isAlignByCenter) {
                            case AlienState.CENTER:
                                left += surplusWidth / 2;
                                break;
                            case AlienState.RIGHT:
                                left += surplusWidth;
                                break;
                            default:
                                left = 0;
                                break;
                        }
                    }
                    view.layout(left, top + topOffset, left + childWidth, top + topOffset + childHeight);
                    left += childWidth + mVerticalSpacing;//为下一个View的left赋值
                }
            }
        }
    }

    public interface FlowSetData<T> {
        void getCover(T item, FlowViewHolder holder, View inflate, int position);
    }

}

3.2 使用

    fun setDrawNumber(number: String?, red: Int, blue: Int) {
        if (number.isNullOrEmpty()) {
            return
        }
        val numbers = number.split(";")
        setAdapter(
            numbers, R.layout.item_ball
        ) { item, holder, inflate, position ->
            if(position >= red) {
                inflate.setBackgroundResource(R.drawable.shape_ball_blue)
            }else {
                inflate.setBackgroundResource(R.drawable.shape_ball_red)
            }
            holder.getView<TextView>(R.id.tv_ball).text = item.toString()
        }
    }

	//或者
    fun setDrawNumber2(number: String?, red: Int, blue: Int) {
        if (number.isNullOrEmpty()) {
            return
        }
        if (childCount > 0) {
            removeAllViews()
        }

        number.split(";").forEachIndexed { index, s ->
            if (index in 0 until red) {
                addNumber(s)
            } else if (index in red until (red + blue)) {
                addNumber(s, isRed = false)
            }
        }
    }

    private fun addNumber(number: String, isRed: Boolean = true) {
        val textView = TextView(context)
        val params = RelativeLayout.LayoutParams(DensityUtils.dp2px(35f), DensityUtils.dp2px(35f))
        params.marginEnd = DensityUtils.dp2px(5f)
        textView.apply {
            layoutParams = params
            text = number
            textSize = 20f
            gravity = Gravity.CENTER
            setTextColor(resources.getColor(R.color.color_white))
            typeface = Typeface.defaultFromStyle(Typeface.BOLD)
            background =
                context.getDrawable(if (isRed) R.drawable.shape_ball_red else R.drawable.shape_ball_blue)
            minWidth = DensityUtils.dp2px(35f)
            minHeight = DensityUtils.dp2px(35f)
            visibility = View.VISIBLE
        }
        addView(textView)
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值