RecycleView 多选批处理

  • androidx.recyclerview.selection 实现 RecycleView 的多选批处理

  • 资料来源:

    https://developer.android.com/reference/androidx/recyclerview/selection/package-summary
    https://developer.android.com/guide/topics/ui/menus#FloatingContextMenu

  • 更新

    1
    20.07.08 初始化

导语

多选批处理一直拖到现在,实在是失策.对底层改动太大,导致了几个恶性 BUG,闲话打住.

实现多选操作,大部分网上教程都是对 Adapter 改造,但是在 Nowakelock 因为复用了 Adapter 这样的改造基本不可行.

不过最终找到了 androidx.recyclerview.selection 这个官方库,资料有点少,但最终还是完成了功能,对 Adapter 的侵入性很小.

导航栏部分交由 ActionMode 配合.

androidx.recyclerview.selection

大致的使用过程

  • 实现一个 ItemKeyProvider<K>,K 代表的是 item 唯一标识的类型,可以是 自定义类型(可通过 Parcelable 序列化), String, Long,因为批量操作都是启用/禁用标志位,Nowakelock 里选择了 String.
  • 继承并实现 ItemDetailsLookup::getItemDetails.getItemDetails 是提供有关与用户选择关联的项目的信息,传入 MotionEvent 类型的触控事件,返回触控位置对应 item 的 MotionEvent 信息.通常在由触摸的坐标获取到选择 item 对应的 ViewHolder,由 ViewHolder 返回 ItemDetails.
  • 在 ViewHolder 返回 ItemDetails 实际上是继承实现 ItemDetailsLookup.ItemDetails<K> 的 getPosition() 和 getSelectionKey() 方法.
  • 最后是托管到一个 SelectionTracker ,SelectionTracker 通常是在 fragment/activity 中.实例由 SelectionTracker.Builder 生成.

nowakelock 示例

在 BaseItem::getID() 中可以返回 item 的唯一 String 标识,所以选择实现 ItemKeyProvider<String>

1
2
3
4
5
6
7
8
class StringKeyProvider(private val adapter: RecycleAdapter) :
ItemKeyProvider<String>(SCOPE_CACHED) {
override fun getKey(position: Int): String? =
adapter.currentList[position].getID()

override fun getPosition(key: String): Int =
adapter.currentList.indexOfFirst { it.getID() == key }
}

在 ViewHoder 中实现 getItemDetails() 返回 ItemDetails<String> 实例

1
2
3
4
5
fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
object : ItemDetailsLookup.ItemDetails<String>() {
override fun getPosition(): Int = adapterPosition
override fun getSelectionKey(): String? = getItem(adapterPosition).getID()
}

实现 StringDetailsLookup

1
2
3
4
5
6
7
8
9
class ItemDetailsLookup(private val recyclerView: RecyclerView) :
ItemDetailsLookup<String>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<String>? {
return when (val view = recyclerView.findChildViewUnder(event.x, event.y)) {
null -> null
else -> (recyclerView.getChildViewHolder(view) as RecycleAdapter.ViewHolder).getItemDetails()
}
}
}

在 Adapter 中增加一个 SelectionTracker<String> 类型变量

1
var tracker: SelectionTracker<String>? = null

在 fragment/activity 实例化 tracker,实例化 tracker 是 SelectionTracker.Builder,有几个参数:

  • selectionId: tracker 的名称,随意的字符串.
  • recyclerView: 传入一个 RecyclerView.
  • keyProvider: 与类型对应的 ItemKeyProvider 这里是 StringKeyProvider 实例.
  • detailsLookup: 与类型对应的 ItemDetailsLookup 这里是 StringDetailsLookup 实例.
  • storage: 与 key 类型有关,String 类型即传入 StorageStrategy.createStringStorage().
1
2
3
4
5
6
7
8
9
tracker = SelectionTracker.Builder<String>(
"MySelection",
recyclerView,
StringKeyProvider(adapter),
StringDetailsLookup(recyclerView),
StorageStrategy.createStringStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()

实例化后传入 adapter.

选中高亮

最直观的反馈是选中后 item 背景高亮.此处可以配置 item 的背景颜色,再设置 view 的 isActivated 完成.

定义一份颜色 item_background,可以通过 view 的 isActivated 切换.

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/SelectBack" android:state_activated="true" />
<item android:drawable="@color/UserApp" />
</selector>

在 View 中设置

1
android:background="@drawable/item_background"

为了应付不需要多选的操作,在 ViewHolder 中实现两个 bind,重点是 binding.root.isActivated = isActivated

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inner class ViewHolder(var binding: ViewDataBinding, private val handler: BaseHandler) :
RecyclerView.ViewHolder(binding.root) {
fun bind(itemInfo: BaseItem?) {
binding.setVariable(BR.item, itemInfo)
binding.setVariable(BR.handler, handler)
binding.executePendingBindings()
}
fun bind(itemInfo: BaseItem?, isActivated: Boolean) {
binding.setVariable(BR.item, itemInfo)
binding.setVariable(BR.handler, handler)
binding.executePendingBindings()
binding.root.isActivated = isActivated
}
fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
object : ItemDetailsLookup.ItemDetails<String>() {
override fun getPosition(): Int = adapterPosition
override fun getSelectionKey(): String? = getItem(adapterPosition).getID()
}
}

在 onBindViewHolder 中对 tracker 判断应该调用那个 bind.

1
2
3
4
5
6
7
8
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (tracker != null) {
val item = getItem(position)
holder.bind(item, tracker!!.isSelected(item.getID()))
} else {
holder.bind(getItem(position))
}
}

绑定长按事件

方式也非常简单,但就是太啰嗦了.

1
2
3
4
5
6
7
tracker.addObserver(
object : SelectionTracker.SelectionObserver<String>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
// do something with tracker.selection
}
})

与 ActionMode 配合

详情见 menus#context-menu