【Android】RecyclerView的高度问题、VH复用概念、多子项的实现;

NestedScrollView嵌套RV

不建议:NestedScrollView会强制测量所有子项的高度;

有四种大致情况:

  1. NSV 是 wrap_content,RV 是 wrap_content

    形成依赖链,此时由于NSV需要测量子项所有的高度,此时RV测的所有子项的总高度,那么RV会被撑开失去滚动能力,同时NSV也会被撑大导致高度异常,此时可能会造成布局重合;

  2. NSV 是 wrap_content,RV 是 match_parent

    此时可能相互依赖导致高度为0或者极小;

  3. NSVmatch_parent,RV 是 wrap_content

    此时RV会测得所有子项的高度(撑导致失去滚动能力),但是NSV不会撑爆,会浪费内存或者测量不准;

  4. NSV 是 match_parent,RV 是 match_parent

    都有滚动能力,NSV会优先处理滚动事件;

    真的不建议呀,本来rv的机制就是渲染可见项,nsv就是测量子项全部内容,本来设计理念就有问题,可能会出现复用失效,滚动混乱,内存飙升的问题;

RV高度异常

解决方法结论:如果RV外层的容器可以match_parent或者固定高度,可以就没问题,不可以的话,就必须重测RV的高度并且重测外层容器的高度,或者使用coor产生联动;

为什么会出现异常?

  1. 内层的RV: 因为RV的设计理念就是只渲染可见项,不一次性加载所有子项;所以RV的默认测量逻辑是最小化高度;

    测量时,会测量一个子项的高度,再结合当前可见区域的高度限制,最终测量出的高度仅能容纳一个子项的高度或者可见区域的少量子项高度,而非所有可见子项,这点也可以结合viewholder的复用机制理解;

  2. 如果是VP2+Fragment的嵌套结构,如果VP2的高度为wrap_content的话,那么就会形成依赖链,最终显示的高度就会被压缩;

  3. 动态渲染延迟加剧高度错位:

    RV的子项是"动态加载的",获得数据后渲染子项;

    VP2/Fragment 的布局测量是"启动时同步执行的";

    那么有可能出现VP2/Fragment测量时,RV还没有加载到布局当中,此时测量的高度为0或者是默认最小值;后续VP2也不会主动测量;

出现以上异常情况(VP2(wrap_content)+RV(wrap_content))之后,解决办法也很显而易见了,就是重测RV的高度,并且在渲染之后手动触发VP2的重测,这样依赖链构建完成!

解决一:重测RV的高度并且重测外层容器的高度

  1. 重写RV的onMeasure方法

    自定一RV,重写方法

    java 复制代码
        protected void onMeasure(int widthSpec, int heightSpec) {
            int expandSpec = MeasureSpec.makeMeasureSpec(
                    Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            super.onMeasure(widthSpec, expandSpec);
        }

    这样写的意思是,设置一个足够大的高度限制,使它能完整的测量所有子项的高度;

    Integer.MAX_VALUE >> 2表示一个极大的数值,右移两位:避免超出系统上限;

    MeasureSpec.AT_MOST ,放任去测量,不超过最大值就行;

  2. 手动触发VP2重测

    时机:在适配器加载完成之后或者碎片的onresume中,调用VP2的方法进行重测;

    方法:vp2.requestLayout()(重新测量,重新执行一遍 on measure ->onlayout-> ondraw)此时VP2会重新读取碎片(RV)的高度,实现高度适配;

解决二:coor产生联动;

场景:比如你要使用VP2包裹RV,而且VP2前面有其他view,rv高度不定所以vp2高度使用warp_content,此时我们可以使用coordinatorlayout

把前面的放进CTL里面,然后VP2设置behavior即可;

需要注意的是VP2必须是match_parent: 原因是和CTL联动的滚动视图必须知道可滚动区域的视图,才能判断滚动多少距离触发CTL的折叠展开;match_parent可以让它填满CTL以外的高度,给Behavior提供一个固定的高度确定联动精准;如果设置为w_content,那么如果内容过多,可能会被撑开,导致vp2的滚动和ctl滚动叠加导致混乱;

当vp2里面有可滚动的视图发生滚动,触发behavior的联动逻辑,带动如果ctl设置为scroll,会带动滚动;

其实关于rv的滚动逻辑把握一个点,如果外层的高度是确定值或者match_parent,此时就不需要依赖测量rv的高度来确定高度,所以此时高度基本都是OK的,但如果是wrap_content依赖rv,那么rv的高度测量就会有问题,解决办法就是重测全部高度(内存开销大),或者使用coor联动把;

Recyclerview的viewHolder的复用机制

  1. 原因:findviewbyid以及inflat都很耗时;viewholder可以缓存item内部的view,避免每次bind时重复findviewbyid;

  2. 工作流程:

    1. 新的item显示时,先从缓存池中找viewholder;
    2. 找到->复用, 没找到->创建新的;
    3. 完成创建/绑定数据之后,如果需要离开,不会销毁viewholder,会放进recyclerview的缓存池;
  3. 推:所以只有在屏幕之内才会oncreateviewholder(),当你离开了屏幕就会复用,不会重新创建了;

想多嘴问问大家,知道为何记这个笔记吗?

举个栗子,比如在做淘宝购物车界面时,要实时更新用户的checkbox状态,我的购物车是拿rv做的,就是由于rv 的viewholder的复用机制,导致我的checkbox状态混乱(气人qvq)怎么解决?我们直接定义checkbox的字段,标记是否被选中,每次都根据这个字段去取消监听,重新设置状态以及重新设置监听,这样就可以了qvq;

多子项的Rv

多子项的rv,其实很简单,我们创建好对应不同子项的布局资源xml,重点是在适配器中,对于不同的子项在OncreateViewHolder里去得到不同的viewholder,然后 onBindViewHolder里去根据不同的holder去调用对应的方法,实现多子项;我觉得里面的重点不仅仅在于实体类,更在于你代码中的适配器怎么写,以下是一个适配器实例,希望能帮助到大家理解;

我的实体类大致是一个list里,有一个List的字段,类似于淘宝购物车店铺和物品的关系,就是店铺List里面有一个物品List的字段;

代码更直观点:店铺代码

java 复制代码
public class BuyCarShopClass {
    private int id;
    private String shopName;
    private boolean isChecked;
    private List<BuyCarProClass> list;

    public BuyCarShopClass(int id, boolean isChecked, List<BuyCarProClass> list, String shopName) {
        this.id = id;
        this.isChecked = isChecked;
        this.list = list;
        this.shopName = shopName;
    }

    public void setId(int id) {
        this.id = id;
    }

下面是对应适配器的代码:

适配器大致流程以及思路:

  1. 首先在创建视图的方法中,我们要确定创建的是哪种视图,所以要获得viewtype,我们重写getitemviewtype()来确定创建的视图;
  2. getitemviewtype()中,我们根据不同位置看判断这个是哪种,然后返回一个int类型的数据;
  3. 创建好视图之后,开始写适配器,返回值,

还请大家根据注释理解吧,嘻;

java 复制代码
public class BurCarRecy1Adapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    List<BuyCarShopClass> list;
    //区分商品和店铺!!!的常亮
    private  int TYPE_SHOP = 0;
    private  int TYPE_PRO = 1;
    public BurCarRecy1Adapter(@NonNull Context context, List<BuyCarShopClass> list) {
        super();
        this.list = list;
    }
        //重写这个方法,使得我们得到正确的子项;这里为了解耦,抽象出了一个方法;
            //根据传入的位置值和List的值来确定返回的子项
    @Override
    public int getItemViewType(int position) {
        int count = getItem(position);
        return count;
    }

    private int getItem(int position) {
        int count = 0;
        for (BuyCarShopClass shopClass : list) {
            if(count == position){
                return TYPE_SHOP;
            }
            count++;
            for (BuyCarProClass proClass : shopClass.getList()) {
                if(count == position){
                    return TYPE_PRO;
                }
                count++;
            }
        }
        return TYPE_PRO;
    }
        //这个方法是根据位置找到对应的类,用于bind方法,后续会提到;
    public  Object getPosition(int i){
        int count  = 0;
        for (BuyCarShopClass shopClass : list) {
            if(count == i){
                return shopClass;
            }
            count++;
            for (BuyCarProClass proClass : shopClass.getList()) {
                if(count == i){
                    return proClass;
                }
                count++;
            }
        }
        return null;
    }
        //根据我们重写的方法和viewType,确定我们创建的哪个viewHolder,以及创建哪个布局资源
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
       if(viewType==TYPE_SHOP){
           View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.buycars_recy_shop,parent,false);
           return new BurCarShopViewHolder(view);
       }
       else{
           View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.buycar_recy_product,parent,false);
           return new BurCarProViewHolder(view);
       }

    }
    //根据不同的viewType来执行自己对应的方法,以及上文提到的对象,找到这个子项的值然后去根据bind方法去更新UI;
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object item = getPosition(position);
        if(holder instanceof  BurCarShopViewHolder){
           ((BurCarShopViewHolder) holder).bind((BuyCarShopClass)item);
        }
        else{
            ((BurCarProViewHolder) holder).bind((BuyCarProClass)item);
        }
    }
   //返回总数量
    @Override
    public int getItemCount() {
        int count = 0;
        for (BuyCarShopClass aClass : list) {
            count++;
            count+=aClass.getList().size();
        }
        return count;
    }
   //两个不同子项的viewHolder
    class BurCarShopViewHolder extends RecyclerView.ViewHolder{
        CheckBox shopCheckBox;
        public BurCarShopViewHolder(@NonNull View itemView) {
            super(itemView);
           //findviewbyid
        }
         public void bind(BuyCarProClass item){
           //写我们更新UI的逻辑
        }
    }

     class  BurCarProViewHolder extends RecyclerView.ViewHolder{
        CheckBox proCheckBox;
      
        public BurCarProViewHolder(@NonNull View itemView) {
            super(itemView);
             //findviewbyid
        }
        public void bind(BuyCarProClass item){
           //写我们更新UI的逻辑
        }
    }



}

希望大家看完会有一点收获,嘻!

相关推荐
张彦峰ZYF2 小时前
高并发优惠权益聚合接口的优雅实现(含超时控制 + 来源标识 + Fallback 降级)
java·后端·面试
4Forsee2 小时前
【Android】模板化解决复杂场景的滑动冲突问题
android·java·rpc
彭同学学习日志2 小时前
解决 Android Navigation 组件导航栏配置崩溃:从错误到实现的完整指南
android·kotlin
若水不如远方2 小时前
深入 Dubbo 服务暴露机制:从注解到网络的完整链路剖析
java·dubbo
tanxinji2 小时前
Netty编写Echo服务器
java·netty
法的空间2 小时前
让 Flutter 资源管理更智能
android·flutter·ios
LBuffer2 小时前
破解入门学习笔记题四十七
java·笔记·学习
可可苏饼干2 小时前
TOMCAT
java·运维·学习·tomcat