You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DiffUtil优化RecyclerView选中项CardView变色方案求助

使用DiffUtil实现RecyclerView选中项UI切换(替代过时的notifyItemChanged)

核心思路

用ListAdapter(内置DiffUtil支持)替代传统RecyclerView.Adapter,结合DiffUtil的局部更新能力,只刷新选中状态变化的item,避免全量刷新。同时通过数据模型维护选中状态,让DiffUtil自动识别变化,实现高效更新。


1. 数据模型类(包含选中状态)

data class ItemModel(
    val id: String, // 唯一标识,用于DiffUtil判断是否为同一item
    val title: String,
    var isSelected: Boolean = false // 维护选中状态
)

2. 实现DiffUtil.ItemCallback

通过getChangePayload实现局部更新,只传递选中状态变化的信息,避免刷新整个item布局:

class ItemDiffCallback : DiffUtil.ItemCallback<ItemModel>() {
    override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
        // 用唯一id判断是否为同一item
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
        // 判断内容是否完全一致,选中状态变化时返回false
        return oldItem.isSelected == newItem.isSelected && oldItem.title == newItem.title
    }

    override fun getChangePayload(oldItem: ItemModel, newItem: ItemModel): Any? {
        // 仅当选中状态变化时,返回标记用于局部更新
        if (oldItem.isSelected != newItem.isSelected) {
            return "SELECTED_STATE_CHANGED"
        }
        return super.getChangePayload(oldItem, newItem)
    }
}

3. 编写ListAdapter实现

使用ListAdapter,处理点击事件时更新数据并提交,利用DiffUtil自动计算差异并高效刷新:

class ItemAdapter : ListAdapter<ItemModel, ItemAdapter.ItemViewHolder>(ItemDiffCallback()) {
    // 记录之前选中的item,实现单选逻辑
    private var lastSelectedPosition: Int = -1

    inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val cardView: CardView = itemView.findViewById(R.id.card_view)
        val titleTv: TextView = itemView.findViewById(R.id.title_tv)

        fun bind(model: ItemModel) {
            titleTv.text = model.title
            // 直接通过selector设置状态,无需手动判断颜色
            cardView.isSelected = model.isSelected
            titleTv.isSelected = model.isSelected
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_layout, parent, false)
        val holder = ItemViewHolder(view)
        // 设置点击事件
        holder.itemView.setOnClickListener {
            val currentPosition = holder.adapterPosition
            if (currentPosition == RecyclerView.NO_POSITION) return@setOnClickListener

            // 复制当前数据列表,避免修改原数据
            val newList = currentList.toMutableList()
            // 取消之前选中项的状态
            if (lastSelectedPosition != -1 && lastSelectedPosition != currentPosition) {
                newList[lastSelectedPosition] = newList[lastSelectedPosition].copy(isSelected = false)
            }
            // 更新当前选中项的状态
            val currentItem = newList[currentPosition]
            newList[currentPosition] = currentItem.copy(isSelected = !currentItem.isSelected)
            // 更新选中位置记录
            lastSelectedPosition = if (currentItem.isSelected) -1 else currentPosition
            // 提交新列表,DiffUtil自动处理刷新
            submitList(newList)
        }
        return holder
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
        // 处理局部更新,仅当选中状态变化时刷新
        if (payloads.isNotEmpty()) {
            val model = currentList[position]
            holder.cardView.isSelected = model.isSelected
            holder.titleTv.isSelected = model.isSelected
            return
        }
        // 无payload时执行全量绑定
        super.onBindViewHolder(holder, position, payloads)
    }
}

4. 布局XML优化(使用Selector)

创建card_background_selector.xml(drawable目录下):

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/selected_card_bg" android:state_selected="true"/>
    <item android:drawable="@color/default_card_bg"/>
</selector>

创建text_color_selector.xml(color目录下):

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/selected_text_color" android:state_selected="true"/>
    <item android:color="@color/default_text_color"/>
</selector>

然后在item布局item_layout.xml中引用:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:clickable="true"
    android:focusable="true"
    android:foreground="?attr/selectableItemBackground"
    app:cardBackgroundColor="@drawable/card_background_selector"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:gravity="center"
        android:textColor="@color/text_color_selector"
        android:textSize="16sp"/>

</androidx.cardview.widget.CardView>

5. 使用Adapter

在Activity/Fragment中初始化RecyclerView:

val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = ItemAdapter()
recyclerView.adapter = adapter

// 初始化数据
val initialList = listOf(
    ItemModel("1", "选项1"),
    ItemModel("2", "选项2"),
    ItemModel("3", "选项3")
)
adapter.submitList(initialList)

为什么这是更高效的实现?

  1. ListAdapter内置DiffUtil:自动对比新旧数据差异,只刷新变化的item,避免notifyDataSetChanged()的全量刷新。
  2. 局部更新(Payload):通过getChangePayload只传递选中状态变化的标记,onBindViewHolder仅更新背景和文字颜色,无需重新绑定整个item的所有数据。
  3. 数据驱动UI:选中状态维护在数据模型中,符合组件化开发思想,UI自动跟随数据变化更新。
  4. 替代过时方法:完全避免使用notifyItemChanged(),采用submitList()触发DiffUtil计算,符合Jetpack组件的最新规范。

内容的提问来源于stack exchange,提问作者Noah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 04:21:25