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

结合ConcatAdapter使用RecyclerView Selection库的多选异常问题

RecyclerView ConcatAdapter 结合 Selection 库多选异常解决

问题背景

实现带头部的笔记列表,用ConcatAdapter组合HeaderAdapter和NotesAdapter展示。基于官方recyclerview-selection库的多选功能原本正常,结合ConcatAdapter后失效:选中第一条笔记时,HeaderAdapter的onBindViewHolder被触发,笔记卡片无法正确勾选。

临时采用先给RecyclerView设置NotesAdapter构建SelectionTracker,再替换为ConcatAdapter的方案后,多选基本恢复,但出现新bug:长按位置0的项时,偶尔会同时选中位置1的项。

问题根源

临时方案的问题在于索引错位:

  • 构建SelectionTracker时,它基于NotesAdapter的局部索引(从0开始)记录选中状态;
  • 替换为ConcatAdapter后,RecyclerView的全局索引是HeaderAdapter(占1个位置)+ NotesAdapter的项,此时NotesAdapter的第0项对应全局索引1;
  • 长按全局索引0(Header)时,Selection库因索引映射错误,误将全局索引1的笔记项也标记为选中,导致同时选中两个项。

正确解决方案

通过自定义ItemKeyProvider和ItemDetailsLookup,让SelectionTracker正确处理ConcatAdapter的索引偏移,无需临时替换Adapter。

1. 自定义ItemKeyProvider,处理索引转换

将RecyclerView的全局索引转换为NotesAdapter的局部索引,同时过滤Header的不可选中项:

class NotesItemKeyProvider(private val recyclerView: RecyclerView) : ItemKeyProvider<Long>(SCOPE_CACHED) {
    override fun getKey(position: Int): Long? {
        val concatAdapter = recyclerView.adapter as? ConcatAdapter ?: return null
        val (adapter, localPos) = concatAdapter.getWrappedAdapterAndPosition(position)
        return if (adapter is NotesAdapter) {
            adapter.getItemId(localPos)
        } else {
            // Header位置不支持选中,返回null
            null
        }
    }

    override fun getPosition(key: Long): Int {
        val concatAdapter = recyclerView.adapter as ConcatAdapter
        val notesAdapter = concatAdapter.adapters.find { it is NotesAdapter } as NotesAdapter
        // 根据ItemId找到NotesAdapter中的局部位置
        val localPos = notesAdapter.getPositionById(key)
        // 计算Header占用的总项数,得到全局位置
        val headerItemCount = concatAdapter.adapters.takeWhile { it !is NotesAdapter }.sumOf { it.itemCount }
        return headerItemCount + localPos
    }
}

(需给NotesAdapter添加getPositionById方法,根据ItemId返回对应局部位置)

2. 自定义ItemDetailsLookup,适配全局索引

确保触摸事件对应的项是NotesAdapter的内容,并正确返回选中所需的位置和Key:

class NotesItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(e.x, e.y) ?: return null
        val holder = recyclerView.getChildViewHolder(view)
        return if (holder is NotesAdapter.NoteViewHolder) {
            val concatAdapter = recyclerView.adapter as ConcatAdapter
            val (adapter, localPos) = concatAdapter.getWrappedAdapterAndPosition(holder.bindingAdapterPosition)
            if (adapter is NotesAdapter) {
                object : ItemDetails<Long>() {
                    override fun getPosition(): Int = holder.absoluteAdapterPosition
                    override fun getSelectionKey(): Long? = adapter.getItemId(localPos)
                }
            } else null
        } else null
    }
}

3. 构建SelectionTracker并关联Adapter

直接基于ConcatAdapter构建,无需临时替换:

private fun setupSelection() {
    val selectionTracker = SelectionTracker.Builder<Long>(
        "notes-selection-tag",
        binding.recyclerViewNotes,
        NotesItemKeyProvider(binding.recyclerViewNotes),
        NotesItemDetailsLookup(binding.recyclerViewNotes),
        StorageStrategy.createLongStorage()
    ).build()

    notesAdapter.setSelectionTracker(selectionTracker)
}

4. 修正NotesAdapter的选中状态判断

在onBindViewHolder中用ItemId判断选中状态,避免位置偏移问题:

override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
    val note = getItem(position)
    val isSelected = selectionTracker?.isSelected(note.id) ?: false
    holder.bind(note, isSelected)
}

效果验证

完成以上修改后,SelectionTracker可以正确识别ConcatAdapter中的不同子Adapter,不会再出现索引错位导致的多选异常,也无需依赖临时替换Adapter的hack方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 06:15:39