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

背景

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

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

实现思路

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

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

kotlin 复制代码
//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视图进行关联,在滚动列表时会多次调用此函数来更新显示内容。
kotlin 复制代码
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,关键代码如下:

kotlin 复制代码
  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 提高性能。关键代码如下:

kotlin 复制代码
//声明了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)
    }
}

使用它:

kotlin 复制代码
//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的性能,当第一次打开页面时,日志输出如下:

kotlin 复制代码
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来创建,假设列表够长,继续往下滑动然后再滑动回来,此时日志如下:

kotlin 复制代码
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时,会调用下面的方法:

arduino 复制代码
//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,如果之前设置过数据,会一并进行缓存 详细介绍参见:Android | 深入理解RecyclerView缓存机制

总结

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

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

示例地址

完整代码示例参见:ChatGptActivity示例

相关推荐
雨白2 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹4 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空6 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭6 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日7 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安7 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑7 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟11 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡13 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0013 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体