文章目录
- 背景
- 实现思路
- 方式一
- 方式二
- 方式三
- 性能对比
- 总结
背景
如上面截图中的效果,首先这是一个多样式的滑动列表(截图里只列举了其中的3
种样式),整体使用 RecyclerView
来实现毋庸置疑。接下来要探讨的是截图中第3个ItemView
中箭头指向的标签列表如何实现?
实现思路
我们对上述问题进行一个抽象,本质上就是两个列表:外部是纵向列表,内部有一个横向列表。如下:
为什么要考虑内部这个横向列表的实现方式呢?是因为涉及到了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,后面每次上下滑动时由于数据没有变化,所以会什么都不做,但是数据展示依然是正确的。所以此种方式是最优解。