高效复用:RecyclerView Item中嵌套列表时的优化技巧

news2024/12/24 20:41:40

文章目录

      • 背景
      • 实现思路
        • 方式一
        • 方式二
        • 方式三
      • 性能对比
      • 总结

背景

如上面截图中的效果,首先这是一个多样式的滑动列表(截图里只列举了其中的3
种样式),整体使用 RecyclerView 来实现毋庸置疑。接下来要探讨的是截图中第3个ItemView 中箭头指向的标签列表如何实现?

实现思路

我们对上述问题进行一个抽象,本质上就是两个列表:外部是纵向列表,内部有一个横向列表。如下:

为什么要考虑内部这个横向列表的实现方式呢?是因为涉及到了RecyclerView的复用机制,先来复习下RecyclerView的缓存机制:

RecyclerView缓存机制
详细介绍参见:Android | 深入理解RecyclerView缓存机制

open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
    RecyclerView.Adapter<BaseVHolder<T>>() {
    
    private val models = mutableListOf<T>()

    override fun getItemViewType(position: Int): Int {
        val model = models[position]
        if (model is IMultiType) return model.getItemViewType()
        return super.getItemViewType(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
        //在这里创建ViewHolder
        return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
    }

    override fun onBindViewHolder(holder: BaseVHolder<T>, position: Int) {
        //在这里绑定数据
        holder.onBindViewHolder(models[position], position)
    }

    override fun getItemCount(): Int = models.size
}

Adapter中onCreateViewHolder创建ViewHolder,onBindViewHolder则负责给View绑定数据。当第一次创建带有横向列表的ItemView时,会分别执行onCreateViewHolder、onBindViewHolder。

假设横向列表个数不固定(数据由服务端下发),那么内部横向列表的创建一定是在onBindViewHolder中创建的,通过什么方式创建可以让内部的列表高效复用呢?我们分别列举几种实现方式。

方式一

标签列表直接使用四个TextView控件实现,什么也不用想,就是干!

使用起来也很方便,因为不涉及动态创建,所以外部列表上下滑动时也不会有频繁创建子View的问题,所以这就是最优解了嘛?显然不是,这种实现方式是有缺点的:

  • 需要创建多个TextView对象并且需要给每个对象引用一一赋值
  • 不够灵活,当标签列表的数量不固定时,这种方式就无能为力了。

方式二

使用一个LinearLayout父View来动态添加每个标签子View,代码如下:

private val labels = mutableListOf<CardItemModel>().apply {
        add(CardItemModel().apply { sceneName = "标签1" })
        add(CardItemModel().apply { sceneName = "标签2" })
        add(CardItemModel().apply { sceneName = "标签3" })
        add(CardItemModel().apply { sceneName = "标签4" })
    }
private val llLabel: LinearLayoutCompat = bind(R.id.ll_label)

llLabel.removeAllViews()
llLabel.weightSum = 1F
labels.forEachIndexed { index, it ->
   val itemView = LayoutInflater.from(context).inflate(R.layout.chat_reply_language_change_item, null)
   val tv: TextView = itemView.findViewById(R.id.tv_language)
   tv.text = it.sceneName
   //添加子View
   llLabel.addView(itemView, LinearLayoutCompat.LayoutParams(
     0, ViewGroup.LayoutParams.WRAP_CONTENT, 1 / labels.size.toFloat()).apply { 
     if (index != labels.lastIndex) marginEnd = 10.dp2px() })
  }

方式三

内部横向标签列表也使用RecyclerView来实现,注意使用细节,我们要使用DiffUtil来更新数据,这样可以保证数据没有变化时,标签列表不再进行任何更新处理,代码如下:

//声明了BaseAdapter、BaseViewHolder,方便后面直接使用

//BaseAdapter.kt
open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
    RecyclerView.Adapter<BaseVHolder<T>>() {
    private val models = mutableListOf<T>()

    override fun getItemViewType(position: Int): Int {
        val model = models[position]
        if (model is IMultiType) return model.getItemViewType()
        return super.getItemViewType(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
        return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
    }

    override fun getItemCount(): Int = models.size

    override fun onBindViewHolder(holder: BaseVHolder<T>, position: Int) {
        holder.onBindViewHolder(models[position], position)
    }

    fun submitList(newList: List<T>) {
        //传入新旧数据进行比对
        val diffUtil = ChatDiffUtil(models, newList)
        //经过比对得到差异结果
        val diffResult = DiffUtil.calculateDiff(diffUtil)
        //NOTE:注意这里要重新设置Adapter中的数据
        models.clear()
        models.addAll(newList)
        //将数据传给adapter,最终通过adapter.notifyItemXXX更新数据
        diffResult.dispatchUpdatesTo(this)
    }
}

//BaseViewHolder
abstract class BaseVHolder<T>(context: Context, parent: ViewGroup, resource: Int) :
    RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(resource, parent, false)) {

    fun onBindViewHolder(item: T, position: Int) {
        onBindView(item, position)
    }

    abstract fun onBindView(item: T, position: Int)

    protected fun <V : View> bind(id: Int): V {
        return itemView.findViewById(id)
    }
}

//工厂模式,用于创建BaseVHolder
interface IVHFactory {
    fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*>
}

使用它:

//ViewHolder
class LabelItemHolder(
        context: Context,
        parent: ViewGroup,
        layoutId: Int = R.layout.chat_reply_language_change_item,
    ) : BaseVHolder<CardItemModel>(context, parent, layoutId) {

        private val sceneName = bind<TextView>(R.id.tv_language)

        override fun onBindView(item: CardItemModel, position: Int) {
            log("方式3:onBindViewHolder: $position")
            sceneName.text = item.sceneName
        }
    }
    
//声明Adapter
private val labelAdapter by lazy {
        BaseAdapter<CardItemModel>(object : IVHFactory{
            override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
                log("方式3:onCreateViewHolder")
                return LabelItemHolder(context, parent)
            }
        })
    }

private val labels = mutableListOf<CardItemModel>().apply {
        add(CardItemModel().apply { sceneName = "标签1" })
        add(CardItemModel().apply { sceneName = "标签2" })
        add(CardItemModel().apply { sceneName = "标签3" })
        add(CardItemModel().apply { sceneName = "标签4" })
    }
    
//在外部Adapter中的onBindViewHolder()里刷新列表数据   
labelAdapter.submitList(labels) 

性能对比

效果对比
方式一由于不符合要求,就不再看了。主要对比下方式二、方式三的性能对比,当第一次进入时,日志输出如下:

E/Tag: 外部Rv---> onBindViewHolder(): 2

E/Tag: 方式2:LinearLayout.addView 0
E/Tag: 方式2:LinearLayout.addView 1
E/Tag: 方式2:LinearLayout.addView 2
E/Tag: 方式2:LinearLayout.addView 3

E/Tag: 方式3:onCreateViewHolder
E/Tag: 方式3:onBindViewHolder: 0
E/Tag: 方式3:onCreateViewHolder
E/Tag: 方式3:onBindViewHolder: 1
E/Tag: 方式3:onCreateViewHolder
E/Tag: 方式3:onBindViewHolder: 2
E/Tag: 方式3:onCreateViewHolder
E/Tag: 方式3:onBindViewHolder: 3

因为是第一次创建,方式二中LinearLayout通过addView添加各个标签View,而方式三中通过Adapter中的onCreateViewHolder、onBindViewHolder来创建,假设列表够长,继续往下滑动然后再滑动回来,此时日志如下:

E/Tag: 外部Rv---> onBindViewHolder(): 2
E/Tag: 方式2:LinearLayout.addView 0
E/Tag: 方式2:LinearLayout.addView 1
E/Tag: 方式2:LinearLayout.addView 2
E/Tag: 方式2:LinearLayout.addView 3

可以看到方式2中还会重新创建标签子View,而方式3不会再重新创建了,这是因为通过DiffUtil再次设置数据时,会进行数据对比,如果数据没有发生变化,那么什么都不会做。而我们在第一次创建View的时候,已经给每个子View设置了数据,所以此时数据展示的依然是正确的。所以方式3是最优解。

//RecyclerView.Recycler
@NonNull
 public View getViewForPosition(int position) {
      return getViewForPosition(position, false);
 }

 View getViewForPosition(int position, boolean dryRun) {
     return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

总结

对于RecyclerView内部某个ItemView嵌套列表,通常考虑下面几种方式:

  • 直接创建多个固定的子View,但这种方式不够灵活,且在动态创建子View时则无能为力了;
  • 通过ViewGroup方式动态的创建各个子View,这种方式本身不能缓存子View,所以每次上下滑动时都会重新创建子View,虽然能实现我们想要的效果,但是性能并不是最优的;
  • 通过RecyclerView创建内部的列表,且通过DiffUtil进行数据对比,数据变化时更新,否则什么都不做。这种方式会在第一次创建各个子View,后面每次上下滑动时由于数据没有变化,所以会什么都不做,但是数据展示依然是正确的。所以此种方式是最优解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/789536.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux系列---【CentOS 7通过MSTSC连接远程桌面】

安装对应的yum源 yum list lightdm xorgxrdp xrdp 可以看到这些软件都在epel中&#xff0c;如果没有的话&#xff0c;请先安装对应的yum源。命令如下&#xff1a; yum install -y epel-release 确认yum源没有问题之后&#xff0c;我们就可以进行安装了。 安装lightdm xorgxrdp…

QT【day1】

登录框&#xff1a; #include "mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) {//窗口设置this->setFixedSize(600,600); //大小this->setWindowTitle("MUMU"); //文本内容this->setWindowOpacity(0.8); //透…

Java五大内存区域介绍与常见异常

jvm虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的区域。这些区域各自有各自的用途&#xff0c;以及创建和销毁的时间&#xff0c;有的区域随着虚拟机进程的启动而存在&#xff0c;有的区域则依赖用户进程的启动和结束而建立和销毁。 1、程序计数器 程序计…

《动手学深度学习》——2.6概率(模拟掷色子代码解析)

文章目录 完整代码代码解析结果展示 完整代码 %matplotlib inline !pip install d2l import torch from torch.distributions import multinomial from d2l import torch as d2l fair_probs torch.ones([6]) / 6 counts multinomial.Multinomial(10, fair_probs).sample((50…

redis中缓存雪崩,缓存穿透,缓存击穿的原因以及解决方案

一 redis的缓存雪崩 1.1 缓存雪崩 在redis中&#xff0c;新&#xff0c;旧数据交替时候&#xff0c;旧数据进行了删除&#xff0c;新数据没有更新过来&#xff0c;造成在高并发环境下&#xff0c;大量请求查询redis没有数据&#xff0c;直接查询mysql&#xff0c;造成mysql的…

图技术在 LLM 下的应用:知识图谱驱动的大语言模型 Llama Index

LLM 如火如荼地发展了大半年&#xff0c;各类大模型和相关框架也逐步成型&#xff0c;可被大家应用到业务实际中。在这个过程中&#xff0c;我们可能会遇到一类问题是&#xff1a;现有的哪些数据&#xff0c;如何更好地与 LLM 对接上。像是大家都在用的知识图谱&#xff0c;现在…

7.25作业

用fgetc与fputc函数实现 1、文件拷贝&#xff0c;例如将7.txt的内容拷贝到8.txt中 2、要求用fgets计算一个文件有多少字节 3、用fgetc计算一个文件有几行 #include<stdio.h> #include<string.h> #include<stdlib.h> int main(int argc, const char *argv…

FreeRTOS源码分析-6 多任务使用要点

目录 1 临界段应用 1.1临界段的作用 1.2临界段API 1.3临界段应用 2 临界段原理 2.1FreeRTOS中断管理实现 2.2关开中断实现 2.3临界段实现 3 任务栈大小确定 3.1确定 3.2MKD-htm文件分析 3.3堆栈检测API 4 栈溢出检测应用 4.1栈溢出检测方案一 4.2栈溢出检测方案…

各种id转换 kegg id kegg compound id 与HMDB转换

KEGG COMPOUND 数据库 - 简书 (jianshu.com) kegg id号转换为可读 的name &#xff1a;使用KEGGREST #-------------kegg id的entry和Name转换 https://zhuanlan.zhihu.com/p/545494092 #BiocManager::install("KEGGREST") #安装KEGGREST这个包 library(…

Android访问其他程序中的数据——以读取系统联系人为例

首先&#xff0c;需要保证通讯录里面有联系人存在&#xff0c;这里手动新增了两条数据。 activity_main.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmln…

SpringBoot创建和使⽤

1.什么是Spring Boot&#xff1f;为什么要学Spring Boot&#xff1f; Spring 的诞⽣是为了简化 Java 程序的开发的&#xff0c;⽽ Spring Boot 的诞⽣是为了简化 Spring 程序开发 的。 Spring Boot 翻译⼀下就是 Spring 脚⼿架&#xff0c;什么是脚⼿架呢&#xff1f;如下图所…

函数(超详解)

函数 1.什么是函数2.C语言中函数的分类2.1 库函数2.1.1如何使用库函数 2.2自定义函数 3.函数的参数3.1实际参数&#xff08;实参&#xff09;3.2形式参数&#xff08;形参&#xff09; 4.函数的调用4.1 传值调用4.2 传址调用4.3 练习 5.函数的嵌套使用和链式访问5.1 嵌套调用5.…

第15章 呈现数据

默认情况下&#xff0c;大多数bash命令会将输出导向STDOUT文件描述符。 数据追加到某个文件用>> 符号来完成。 who命令生成的输出会被追加到test2文件中已有数据的后面。 只重定向错误STDERR文件描述符被设成2。可以选择只重定向错误消息&#xff0c;将该文件描述符值…

day02_springboot综合案例

day02_springboot综合案例 订单操作 查询所有订单 查询所有订单流程 查询订单&#xff0c;要把订单对应的产品也要查出来 Orders实体类 Data public class Orders {private String id;private String orderNum;DateTimeFormat(pattern"yyyy-MM-dd HH:mm")privat…

多线程面试题--线程安全

synchronized关键字的底层原理 基础版 当我们对代码就行反编译&#xff0c;会发现其实synchronized就是monitor Monitor 假如现在有一个线程过来了&#xff0c;要执行当前代码&#xff0c;会执行到synchronized (lock)&#xff0c;lock是一个对象锁。首先会让这个lock对象和m…

Clion开发stm32之微妙延迟(采用nop指令实现)

前言 需要借助逻辑分析仪动态调整参数此次测试的开发芯片为stm32f103vet6 延迟函数 声明 #define NOP_US_DELAY_MUL_CNT 5 /*nop 微妙延迟需要扩大的倍数(根据实际动态修改)*/ void bsp_us_delay_nop(uint32_t us);void bsp_ms_delay_nop(uint32_t ms);定义 void bsp_us_dela…

【深度学习】yolov5以及yolov8的微调后的模型精度对比

文章目录 前言1. 训练1.1 yolov5 的 yolov5m61.2 yolov5 的 yolov5l61.3 yolov8 的训练 结论&#xff1a; 前言 做了一个烟火识别&#xff0c;用了2W张图片&#xff0c;标注包括&#xff1a;fire&#xff0c;smoke 。在coco80类的模型上进行ft&#xff0c; 借此机会进行比较一…

koa2 从0使用mysql2做第一个接口

全局安装Koa命令行工具: npm install -g koa/cli 2. 创建新项目: koa new my-app 这将生成如下目录结构: my-app ├── app.js ├── package.json └── src └── index.js 3. 安装依赖: cd my-app npm install 4. 启动项目: npm start 5.安装mysql2 yarn add…

升级JDK17问题记录

1. 放开标准库的模块访问限制 Jdk9模块后, 有些类访问, 需要通过如下配置打开访问权限 --add-opensjava.base/jdk.internal.miscALL-UNNAMED --add-opensjava.base/java.langALL-UNNAMED --add-opensjava.base/java.utilALL-UNNAMED --add-opensjava.base/java.lang.refle…

加载数据集

批量梯度下降法&#xff08;Batch Gradient Descent&#xff0c;BGD&#xff09;&#xff1a;在更新参数时&#xff0c;BGD根据batch中的所有样本对参数进行更新。&#xff08;计算速度快&#xff09; 随机梯度下降法&#xff08;Stochastic Gradient Descent&#xff0c;SGD&…