高效复用:RecyclerView内部嵌套横向列表时的优化技巧

news2024/11/25 11:55:55

背景

假设要实现下面的效果图:

如图所示,首先这是一个多样式的滑动列表(截图里只列举了其中的3 种样式),整体外部使用 RecyclerView 来实现没什么疑问。那么截图第3个ItemView 中箭头指向的横向标签列表如何实现呢?

实现思路

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

外部纵向列表关键代码实现如下:

//RecyclerView.Adapter
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

    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)
    }
}

//工厂模式,用于生产BaseVHolder
interface IVHFactory {
    fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*>
}
  • onCreateViewHolder()用于创建 ViewHolder对象。它会在每次需要一个新的 ItemView 时被调用,并返回一个包含了 ItemViewViewHolder 对象。
  • onBindViewHolder()则负责将数据与指定位置上的ItemView视图进行关联,在滚动列表时会多次调用此函数来更新显示内容。
class ChatVHolderFactory : IVHFactory {
    companion object {
        const val TYPE_ASK_TXT = 1 //type1
        const val TYPE_REPLY_TXT = 2 //type2
        const val TYPE_REPLY_SPAN = 3 //type3
    }

    override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
        return when (viewType) {
            TYPE_ASK_TXT -> ChatAskHolder(context, parent)
            TYPE_REPLY_TXT -> ChatReplyTxHolder(context, parent)
            TYPE_REPLY_SPAN -> ChatReplyImgTextHolder(context, parent)
            else -> throw IllegalStateException("unSupport type")
        }
    }
}

class ChatGptActivity : AppCompatActivity() {

    private val mRv: RecyclerView by id(R.id.rv_view)
    private val chatAdapter by lazy { BaseAdapter<MessageModel>(ChatVHolderFactory()) }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_layout_rv)
        setRvInfo()
    }

    private fun setRvInfo() {
        val list = mutableListOf<MessageModel>()
        list.add(MessageModel(content = "天气预报", type = ChatVHolderFactory.TYPE_ASK_TXT))
        list.add(MessageModel(content = "天气情况如下:", type = ChatVHolderFactory.TYPE_REPLY_TXT))
        list.add(MessageModel(type = ChatVHolderFactory.TYPE_REPLY_SPAN))
        for (i in 0..20) {
            list.add(MessageModel(content = "天气预报", type = ChatVHolderFactory.TYPE_ASK_TXT))
        }
        chatAdapter.submitList(list)
        mRv.layoutManager = LinearLayoutManager(this)
        mRv.adapter = chatAdapter
    }

上述代码对多类型列表场景下做了个简单的封装,不再过多解释。

重点看第3个ItemView内部的横向列表如何实现。其中横向标签列表个数有两种情况:

  • case1:标签列表个数是固定的;
  • case2:标签列表个数是不固定的(数据由服务端下发),如果不固定,那么列表应该是在Adapter#onBindViewHolder中得到数据之后动态创建的。

针对不同情况得到下面几种可能的实现方式。

方式一

标签列表直接使用固定个数的TextView控件实现,可以满足 case1的场景,什么也不用想,就是干!

使用起来也很方便,因为不涉及动态创建,所以上下滑动时也不会有频繁创建子View的问题,但这种实现方式是有缺点的:

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

方式二

使用一个 LinearLayoutViewGroup 来动态添加每个标签子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
     log("方式2:LinearLayout.addView $index")
     llLabel.addView(itemView, LinearLayoutCompat.LayoutParams(
     0, ViewGroup.LayoutParams.WRAP_CONTENT, 1 / labels.size.toFloat()).apply { 
     if (index != labels.lastIndex) marginEnd = 10.dp2px() })
  }

方式三

内部横向标签列表也使用RecyclerView来实现。注意使用细节,我们要使用DiffUtil来更新数据,这样做的优点是可以利用 RecyclerView 的复用机制和 DiffUtil 提高性能。关键代码如下:

//声明了BaseViewHolder,方便后面直接使用
//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)
    }
}

使用它:

//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) 

性能对比

上述截图是利用方式2、方式3实现的UI效果,方式1由于不够灵活,就不再看了。下面来对比下方式2、方式3的性能,当第一次打开页面时,日志输出如下:

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

因为是第一次创建,方式2中通过 LinearLayout#addView 添加各个标签子View,而方式3中通过RecyclerView.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却不会再重新创建了,这是因为方式3通过DiffUtil再次设置数据时,会进行数据对比,如果数据没有发生变化,那么什么都不会做。而我们在第一次创建View的时候,已经给每个子View设置了数据,所以此时数据展示的依然是正确的。

这里开始有个疑问,为什么上下滑动列表并返回原位置时,方式3没有重新设置数据也能正确显示呢? 我们知道RecyclerView是通过RecyclerView.Recycler缓存的ViewHolder,当尝试获取ViewHolder中的itemView时,会调用下面的方法:

//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;
}

当上下滑动时,我们的 ViewHolder 会逐级进行缓存,假设最后存到了 mRecyclerPool中,此时ItemView因为在第一次创建时设置了数据,所以会把绑定的数据一块存入ViewHolder中。因此再次滑动到原 position 时,虽然没有设置数据,但是会从缓存池中获取数据并正确显示。

这里可以把ViewHolder看成是一个普通的对象,缓存时不仅缓存了ItemView,如果之前设置过数据,会一并进行缓存

总结

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

  • 直接创建多个固定的子View:这种方式不够灵活扩展性差,且在动态创建子View时就无能为力了;
  • 通过ViewGroup方式动态的创建各个子View:这种方式本身不能缓存子View,所以每次上下滑动时都会重新创建子View,虽然能实现我们想要的效果,但是性能并不是最优的;
  • 通过RecyclerView创建内部列表并使用 DiffUtil 进行数据对比和更新操作:数据变化时更新,否则什么都不做。这样做可以最大限度地利用 RecyclerView 的复用机制和缓存优势,在数据变化时进行精准刷新并提高整体渲染效率。所以此种方式是最优解。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

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

相关文章

网络编程套接字 | 预备知识

在之后的文章中我们将来讲解网络编程中的相关知识点&#xff0c;再本文中我们首先来讲解一下网络编程中的预备知识&#xff1a; 预备知识 源IP地址和目的IP地址 在IP数据包中有两个IP地址分别是源IP地址和目的IP地址&#xff0c;此时这里就会出现一个问题就是&#xff1a;如…

Spring MVC 之MVC 体系结构、什么是SpringMVC

Spring MVC简介 MVC 体系结构三层架构MVC设计模式 Spring MVC 是什么&#xff1f;扩展知识Spring模块Data Access/Integration&#xff08;数据访问/集成&#xff09;Web&#xff08;网络层&#xff09;AOP&#xff08;面向切面&#xff09;Messaging&#xff08;消息传送&…

Invalid bound statement (not found): com.test.mapper.ItemsMapper.selectItems

分析原因 如下图&#xff0c;因为idea默认只会扫描resources目录下xml的文件&#xff0c;所有java目录下ItemsMapper.xml没有被扫描到&#xff0c;导致Invalid bound statement (not found): com.test.mapper.ItemsMapper.selectItems 解决办法 在pom文件中&#xff0c;增加…

【LeetCode-中等题】47. 全排列 II

文章目录 题目方法一&#xff1a;递归回溯去重 题目 相比较46题&#xff1a;不需要去重&#xff1a;【LeetCode-中等题】46. 全排列 需要做出的改变就是&#xff1a; 首先需要对待全排列的数组进行排序&#xff08;为去重操作做准备&#xff09; Arrays.sort(nums);//对数组…

Nacos Java SDK

概述部分 Maven 坐标 <dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId><version>${version}</version> </dependency>注意&#xff1a;由于Nacos Java SDK在2.0版本后引入了gRPC&am…

直播邀约|8个数字了解2023腾讯全球数字生态大会

2023腾讯数字生态大会&#xff0c;一起来看看吧~

NoSQL之Redis配置与优化(一)

关系数据库与非关系型数据库 &#xff1a; ●关系型数据库&#xff1a; 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 SQL 语句&#xff08;标准数据查询语言&#xff09;就是一种基于…

软件工程:万字长文,说透常见的5种部署策略(文末有总结)

在软件工程中&#xff0c;最终的价值交付&#xff0c;都是要通过软件的部署上线来完成的。 那如何将新的或改进的软件功能交付给用户&#xff0c;同时还要确保高质量、稳定性和用户体验&#xff0c;选择适当的部署策略变得至关重要。 复杂软件工程中&#xff0c;新功能的部署…

GC Root

开章&#xff0c;不知道有没有小伙伴跟我一样&#xff0c;在快要毕业或者换工作的时候疯狂刷八股文&#xff0c;这样做有什么坏处呢 好多面试题自己都是靠死记硬背&#xff0c;这也是我们过后压根记不着这些内容的原因&#xff1b;每次面试问题需要自己反复去刷&#xff0c;甚…

C++初阶:C++入门

目录 一.iostream文件 二.命名空间 2.1.命名空间的定义 2.2.命名空间的使用 三.C的输入输出 四.缺省参数 4.1.缺省参数概念 4.2.缺省参数分类 4.3.缺省参数注意事项 4.4.缺省参数用途 五.函数重载 5.1.重载函数概念 5.2.C支持函数重载的原理--名字修饰(name Mangl…

富士康曲线救国,iPhone 15 Pro订单较上代有减少,iPhone 15增加

据外媒报道&#xff0c;苹果将于9月13日凌晨举行的秋季新品发布会上推出iPhone 15系列智能手机。然而&#xff0c;令人惊讶的是&#xff0c;这款备受期待的手机在8月份就已开始批量生产&#xff0c;以确保上市初期供应充足。 随着iPhone 15系列发布时间的临近&#xff0c;越来越…

Cisco 5580 ASA service 如何Console?

一、Cisco ASA 5580 需要一个9针转Rj45到头子 如图&#xff1a; 二、插在设备的这个接口上 就可以了

RabbitMQ从入门到精通之安装、通讯方式详解

文章目录 RabbitMQ一、RabbitMQ介绍1.1 现存问题 一、RabbitMQ介绍二、RabbitMQ安装三、RabbitMQ架构四、RabbitMQ通信方式4.1 RabbitMQ提供的通讯方式4.2 Helloworld 方式4.2Work queues4.3 Publish/Subscribe4.4 Routing4.5 Topics4.6 RPC (了解) 五、Springboot 操作RabbitM…

《C和指针》读书笔记(第十三章 高级指针话题)

目录 0 简介1 进一步探讨指向指针的指针2 高级声明3 函数指针3.1 回调函数3.2 转移表 4 命令行参数4.1 传递命令行参数4.2 处理命令行参数 5 字符串常量6 总结 0 简介 众所周知&#xff0c;指针是C语言的灵魂&#xff0c;所以本书&#xff08;《C和指针》&#xff09;才会将较…

为安全带来光明:光耦继电器的 10 种救生应用

在安全性和可靠性至关重要的世界中&#xff0c;光耦继电器已成为推动各行业进步的关键技术。这些卓越的设备经常在主流新闻中被忽视&#xff0c;但它们一直在默默地为保障生命和提高整体运营效率的关键系统提供动力。今天&#xff0c;我们重点介绍光耦继电器的十种救生应用&…

设计模式-01简单工厂模式详解 详细代码对比

目录 ChatGpt问答原生代码简单工厂模式代码 简单工厂模式&#xff08;Simple Factory Pattern&#xff09;新增boat 对比两种方法原生代码为什么使用强制转换&#xff1f;简单工厂模式 简单工厂方法总结与原生代码的区别&#xff1a;优点:缺点&#xff1a; 参考 本文将介绍什么…

golang指针的学习笔记

package main // 声音文件所在的包&#xff0c;每个go文件必须有归属的包 import ("fmt" )// 引入程序中需要用的包&#xff0c;为了使用包下的函数&#xff0c;比如&#xff1a;Printin// 字符类型使用 func main(){ // 基本数据类型&#xff0c;变量存的就是值&am…

做tiktok怎么运营?

一、揭开tiktok的神秘面纱 说到tiktok&#xff0c;你可能想到的是那些精彩的短视频&#xff0c;以及那些在几秒钟内就能吸引无数粉丝的创作者。然而&#xff0c;tiktok的魅力远不止于此。这个全球最受欢迎的短视频社交平台&#xff0c;正以惊人的速度改变着社交媒体的面貌。 二…

【IR】Vision-Language Tracking

调研&#xff1a;视觉-语言跟踪 0x01 Transformer vision-language tracking via proxy token guided cross-modal fusion, PRL2023AbstractIntroductionContribution效果Conclusion 0x02 Divert More Attention to Vision-Language Object Tracking, NeurIPS2022AbstractIntro…

起飞!Python 3.11的10个高效新特性

性能有巨大的提升是Python 3.11的一个重要的改进&#xff0c;除此以外Python 3.11还有增加了许多新的特性。在本文中我们将介绍Python 3.11新特性&#xff0c;通过代码示例演示这些技巧如何提高生产力并优化代码。 1、模式匹配 Python 3.11引入了模式匹配&#xff0c;可以简化…