结合ConcatAdapter使用RecyclerView 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

