A guide on how to implement modal selection using recyclerview-selection from Google’s Android Jetpack.
Gradle
ItemKeyProvider
For many data sets, getting a position from a key requires traversing the set until the item is found. It’ll vary a lot so I leave it just as an extension function here with the implementation up to you.
Use the MotionEvent to find the View interacted with and fetch relevant ItemDetails.
Assign values to ItemDetails and mark the View activated state according to whether it is selected or not.
SelectionPredicate has two provided implementations, one for single selection, and another for multi selection. If you have an item set which has some items that can not be selected, you’ll have to implement SelectionPredicate yourself.
If you followed the above you now have a RecyclerView with single or multi-selection. However, you have no way to toggle this behaviour on or off.
Methods are added to enter and exit the selection mode. Adding a SelectionObserver is exposed so the Activity or Fragment can update toolbars and the like. The usecase for exiting the selection mode when 0 items are selected is handled by using another SelectionObserver.
You can also hook up the enter and exit actions to toolbar menu items or however you like.
Now you have a RecyclerView with modal multi-selection. There is also band selection just like Google Photos and many other apps. The next step is to hook up the Toolbar for an action mode while in the selection mode. Another time.
Gradle
implement "androidx.recyclerview:recycler-selection:$version"
Initial simple implementation
Implement collaborator classes
You can use Long, String, or Parcelable for the key types.ItemKeyProvider
For many data sets, getting a position from a key requires traversing the set until the item is found. It’ll vary a lot so I leave it just as an extension function here with the implementation up to you.
private class KeyProvider(
private val adapter: RecyclerView.Adapter
) : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int) = adapter.getItemId(position)
override fun getPosition(key: Long) = adapter.getPosition(key) ?: NO_POSITION
}
fun RecyclerView.Adapter<*>.getPosition(key: Long): Int? = ...
ItemDetailsclass ItemDetails : ItemDetailsLookup.ItemDetails<Long>() {
var pos = 0
var key: Long? = null
override fun getPosition() = pos
override fun getSelectionKey() = key
// Mark the whole area of matching item view as interactable for selection.
override fun inSelectionHotspot(e: MotionEvent) = true
}
ItemDetailsLookupUse the MotionEvent to find the View interacted with and fetch relevant ItemDetails.
private class DetailsLookup(
private val recyclerView: RecyclerView
) : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? =
recyclerView.findChildViewUnder(e.x, e.y)
?.let { (recyclerView.getChildViewHolder(it) as MyViewHolder).details }
}
ViewHolderAssign values to ItemDetails and mark the View activated state according to whether it is selected or not.
class MyViewHolder(
itemView: View,
val selectionTracker: SelectionTracker<Long>
) : RecyclerView.ViewHolder(itemView) {
...
fun bind(itemData: ItemData, position: int) {
... // bindings or how ever you set your view values.
itemView.isActivated = selectionTracker.isSelected(itemData.id)
details.position = position
details.key = itemData.id
}
}
Create SelectionTracker
The SelectionTracker.Builder creates the selection controller and registers it with the RecyclerView. This will have to be worked around later on.SelectionPredicate has two provided implementations, one for single selection, and another for multi selection. If you have an item set which has some items that can not be selected, you’ll have to implement SelectionPredicate yourself.
selectionTracker = SelectionTracker
.Builder(
"singleSelection",
recyclerView,
keyProvider,
detailsLookup,
StorageStrategy.createLongStorage()
)
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build()
.apply { addObserver(selectionObserver) }
You can implement a SelectionObserver to receive callbacks on selection events.If you followed the above you now have a RecyclerView with single or multi-selection. However, you have no way to toggle this behaviour on or off.
Implementing Modal Selection
To implement modal selection the above will be changed such that long clicks on items will enable selection. Selection will be disabled should zero items be selected, or should the user request it, say from a Toolbar menu item.Touch events
To control the behaviour of SelectionTracker a shim will used. The shim will ensure the tracker receives all of the touch events, but the selectionModelEnabled flag controls whether the events are captured by SelectionTracker. Using this method allows the ncessary events to propagate through to the item views where long and single clicks will be handled.class TouchPassthruListener(
private val selectionListener: OnItemTouchListener
) : OnItemTouchListener by selectionListener {
var selectionModeEnabled = false
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
return selectionModeEnabled &&
selectionListener.onInterceptTouchEvent(rv, e) &&
e.action != MotionEvent.ACTION_CANCEL
}
}
Encapsulate selection tracker
The next problem to deal with is isolating the callback SelectionTracker uses to intercept touch events. There are two approaches, use a throwaway RecyclerView subclass to capture the listener, or use the subclass as the actual RecyclerView. I did the second so I’ll put that here. To make it easy, this will all be done when the adapter is assigned.Methods are added to enter and exit the selection mode. Adding a SelectionObserver is exposed so the Activity or Fragment can update toolbars and the like. The usecase for exiting the selection mode when 0 items are selected is handled by using another SelectionObserver.
class MyRecyclerView : RecyclerView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyle: Int
) : super(context, attrs, defStyle)
lateinit var selectionTracker: SelectionTracker<Long>
private var captureOnItemTouchListener = false
private var capturedOnItemTouchListener: OnItemTouchListener? = null
private lateinit var multiSelectionTouchListener: TouchPassthruListener
fun setSelectionObserver(observer: SelectionObserver) {
selectionTracker.addObserver(observer)
}
fun enterMultiSelectionMode(itemId: Long? = null) {
multiSelectionTouchListener.selectionModeEnabled = true
}
fun exitMultiSelectionMode() {
selectionTracker.clearSelection()
multiSelectionTouchListener.selectionModeEnabled = false
}
override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
super.setAdapter(adapter)
createMultiSelection()
(adapter as? HoldsRecyclerView)?.setRecycler(this)
?: throw IllegalArgumentException( "Adapter must implement HoldsRecyclerView.")
}
private fun createMultiSelection() {
captureOnItemTouchListener = true
selectionTracker = SelectionTracker
.Builder(
"singleSelection",
this,
KeyProvider(adapter),
DetailsLookup(this),
StorageStrategy.createLongStorage()
)
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build()
.apply { addObserver(ExitObserver(this)) }
multiSelectionTouchListener = TouchPassthruListener(capturedOnItemTouchListener!!)
addOnItemTouchListener(multiSelectionTouchListener)
}
override fun addOnItemTouchListener(listener: OnItemTouchListener) {
// If capture is set, then store the listener rather than passing it along.
if (captureOnItemTouchListener) {
capturedOnItemTouchListener = listener
captureOnItemTouchListener = false
} else {
super.addOnItemTouchListener(listener)
}
}
private inner class ExitObserver(
private val tracker: SelectionTracker<Long>
) : SelectionTracker.SelectionObserver<Long>() {
override fun onItemStateChanged(key: Long, selected: Boolean) {
if (tracker.selection.size() == 0) exitMultiSelectionMode()
}
}
}
Final touches
The final step is to add the listeners that will trigger entering and exiting the selection mode. This can be done in the ViewHolderYou can also hook up the enter and exit actions to toolbar menu items or however you like.
class MyViewHolder(
itemView: View,
val recyclerView: MyRecyclerView
) : MyRecyclerView.ViewHolder(itemView) {
...
init {
view.setOnLongClickListener {
recyclerView.enterMultiSelectionMode(details.selectionKey)
true
}
view.setOnClickListener {
val itemId = details.selectionKey?.toInt() ?: return@setOnClickListener
it.findNavController()
.navigate(MyFragmentDirections.ActionOpenItem().setItemId(itemId))
recyclerView.exitMultiSelectionMode()
}
}
fun bind(itemData: ItemData, position: int) {
... // bindings or how ever you set your view values.
itemView.isActivated = recyclerView.selectionTracker.isSelected(itemData.id)
details.position = position
details.key = itemData.id
}
}
Now you have a RecyclerView with modal multi-selection. There is also band selection just like Google Photos and many other apps. The next step is to hook up the Toolbar for an action mode while in the selection mode. Another time.
Nice article, Which you have share here about the recyclerview. Your article is very informative and useful to know more about the modal selection of recyclerview in kotlin. Thanks for sharing this article here.
ReplyDelete