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


    背景

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

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

    实现思路

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

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

    //RecyclerView.Adapter
    open class BaseAdapter(private val vhFactory: IVHFactory) :
        RecyclerView.Adapter>() {
        
        private val models = mutableListOf()
    
        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 {
            //在这里创建ViewHolder
            return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder
        }
    
        override fun onBindViewHolder(holder: BaseVHolder, position: Int) {
            //在这里绑定数据
            holder.onBindViewHolder(models[position], position)
        }
    
        override fun getItemCount(): Int = models.size
    
        fun submitList(newList: List) {
            //传入新旧数据进行比对
            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<*>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 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(ChatVHolderFactory()) }
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_layout_rv)
            setRvInfo()
        }
    
        private fun setRvInfo() {
            val list = mutableListOf()
            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
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

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

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

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

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

    方式一

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

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

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

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

      private val labels = mutableListOf().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() })
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    方式三

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

    //声明了BaseViewHolder,方便后面直接使用
    //BaseViewHolder
    abstract class BaseVHolder(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  bind(id: Int): V {
            return itemView.findViewById(id)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用它:

    //ViewHolder
    class LabelItemHolder(
            context: Context,
            parent: ViewGroup,
            layoutId: Int = R.layout.chat_reply_language_change_item,
        ) : BaseVHolder(context, parent, layoutId) {
    
            private val sceneName = bind(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(object : IVHFactory{
                override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
                    log("方式3:onCreateViewHolder")
                    return LabelItemHolder(context, parent)
                }
            })
        }
    
    private val labels = mutableListOf().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) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    性能对比

    上述截图是利用方式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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    因为是第一次创建,方式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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到列表再次滑动到原位置时,方式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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当上下滑动时,我们的 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

  • 相关阅读:
    【python】设置里的BASE_DIR
    javascript 数组升序、降序
    操作系统 一个进程通过内核事件 来控制另一个线程的结束
    0基础24岁女硕士生,想转行做月薪30k的测试开发,需要从什么开始学习?
    [linux] SFTP文件传输基本命令 --- xshell 直接上传文件
    Spring Boot 面试题及答案整理,最新面试题
    深度学习--基础语法
    【365天深度学习训练营】第二周 彩色图片分类
    心法利器[62] | 向量召回和字面召回的选择与权衡
    工业互联网网络体系安全防护研究
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/132714173