RecyclerView删除含最后项的多选中项时抛出IndexOutOfBound异常
RecyclerView多选删除最后一项索引越界修复方案
问题核心原因
当前代码触发越界是四个逻辑错误共同导致的:
mSelectedItems没有去重逻辑,多次长按同一项会重复存入相同索引,删除时重复移除同位置项直接触发越界sortedDescending()是返回新排序列表的非原地排序方法,原代码没有接收返回值,实际遍历的还是乱序的索引集合:先删靠前位置的项后列表长度缩短,原本靠后的索引(比如最后一项的索引)会大于当前列表最大下标,直接触发越界- 监听全部写在
onBindViewHolder中,会随列表绑定反复注册,导致选中状态、删除逻辑重复触发,进一步加剧索引错乱;同时原多选逻辑写死了selectedPosition = position,根本没有实现选中/取消的状态切换,本质还是错误的单选逻辑。 - 删除时同时调用
notifyItemRemoved和notifyDataSetChanged属于冗余操作,会打断RecyclerView的位置修正流程,导致动画和索引计算异常。
可直接落地的修复代码
第一步:调整监听绑定位置,修正多选逻辑
把所有item点击、长按、删除按钮的监听从onBindViewHolder移到onCreateViewHolder中,避免重复绑定,同时用HashSet存选中索引自动去重,实现真正的多选切换:
// 选中索引用HashSet自动去重,避免重复存储 val mSelectedItems = HashSet<Int>() val deletList: ArrayList<String> = ArrayList() var items: ArrayList<GetListModel> = arrayListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OutagesViewHolder { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.chats_list_item, parent, false) val holder = OutagesViewHolder(itemView) // Item长按切换选中状态 holder.itemView.setOnLongClickListener { val currentPos = holder.adapterPosition if (currentPos == RecyclerView.NO_POSITION) return@setOnLongClickListener true if (mSelectedItems.contains(currentPos)) { // 已选中则取消选中 mSelectedItems.remove(currentPos) items[currentPos].id?.let { deletList.remove(it) } holder.itemView.setBackgroundColor(Color.parseColor("#00000000")) } else { // 未选中则加入选中集合 mSelectedItems.add(currentPos) items[currentPos].id?.let { deletList.add(it) } holder.itemView.setBackgroundColor(Color.parseColor("#FFECECEC")) } notifyItemChanged(currentPos) true } // 删除按钮点击触发删除 holder.deletebutton.setOnClickListener { removeSelectedItems() } return holder } override fun getItemCount(): Int = items.size // 注意:如果没有多类型布局需求,直接删掉原代码里写的getItemId、getItemViewType重写,返回position的写法会导致列表复用异常 override fun onBindViewHolder(holder: OutagesViewHolder, position: Int) { // 这里只做数据绑定,不要注册任何点击监听 val itemData = items[position] // 补充业务数据绑定逻辑,比如赋值文本、加载图片等 }
第二步:修正删除逻辑,从根源避免索引错位
倒序删除时必须接收排序后的新列表,同时增加边界校验做双重保险:
fun removeSelectedItems(){ // 必须接收sortedDescending的返回值,才能拿到倒序排列的索引 val sortedDeleteIndices = mSelectedItems.sortedDescending() sortedDeleteIndices.forEach { index -> // 双重边界校验,极端场景下也不会触发越界 if (index in 0 until items.size) { items.removeAt(index) notifyItemRemoved(index) } } // 删完统一刷新剩余item的位置,不需要调用notifyDataSetChanged全量刷新 notifyItemRangeChanged(0, items.size) // 清空选中状态 mSelectedItems.clear() deletList.clear() }
额外注意点
- 如果需要支持滚动后选中态不丢失,需要在
onBindViewHolder里根据mSelectedItems是否包含当前position,主动设置item的背景状态,不要只在点击时修改 - 不要在删除单个item后立刻调用
notifyDataSetChanged,会让RecyclerView无法计算item动画,还会导致位置计算失效
内容的提问来源于stack exchange,提问作者Prateek Sharma
相关产品推荐
相关产品推荐

