Dependencies
dependencies {
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
// implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01'
}
Fragment / Activity
We are using the item position (Long) as Id Key, where everything is selectable.
class TestSelectionFragment : Fragment() { private lateinit var adapter: LocalAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.testselection, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) initList() } private fun initList() { val layoutManager = GridLayoutManager(context, 3) list.layoutManager = layoutManager if (!::adapter.isInitialized) { adapter = LocalAdapter() adapter.setHasStableIds(true) list.adapter = adapter val tracker = SelectionTracker.Builder<Long>( "number-selection", list, StableIdKeyProvider(list), LocalItemDetailsLookup(list), StorageStrategy.createLongStorage() ).withSelectionPredicate( SelectionPredicates.createSelectAnything() ).build() adapter.tracker = tracker tracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() { override fun onSelectionChanged() { super.onSelectionChanged() Timber.d("selection=${tracker.selection}") } }) } val items = mutableListOf<ViewItem>() for (number in 1..10) { items.add(ViewItem.NumberItem(number.toString())) } adapter.submitList(items) } private class LocalItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() { override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? { val view = recyclerView.findChildViewUnder(event.x, event.y) if (view != null) { return (recyclerView.getChildViewHolder(view) as LocalAdapter.ViewHolder) .getItemDetails() } return null } } sealed class ViewItem(open val id: String, val resource: Int) { data class NumberItem(override val id: String) : ViewItem(id, R.layout.testselection_item) } class LocalAdapter() : ListAdapter<ViewItem, LocalAdapter.ViewHolder>(DiffCallback()) { lateinit var tracker: SelectionTracker<Long> override fun getItemId(position: Int): Long = position.toLong() override fun getItemViewType(position: Int): Int { return getItem(position).resource } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(viewType, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { Timber.d("position=$position, selected=${tracker.isSelected(position.toLong())}, selection=${tracker.selection}") holder.bind(getItem(position), tracker.isSelected(position.toLong())) } private class DiffCallback: DiffUtil.ItemCallback<ViewItem>() { override fun areItemsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean { if (oldItem.resource != newItem.resource) return false // check if id is the same return oldItem.id == newItem.id } @SuppressLint("DiffUtilEquals") override fun areContentsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean { // check if content is the same // equals using data class return oldItem == newItem } } inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer { // why update ui here? easier to access view without need to holder.titleTextView fun bind(item: ViewItem, isSelected: Boolean) { when(item) { is ViewItem.NumberItem -> { numberTextView.text = item.id val res = if (isSelected) R.drawable.ic_baseline_check_circle_24 else R.drawable.ic_baseline_radio_button_unchecked_24 selectionImageView.setImageResource(res) } } } fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> = object : ItemDetailsLookup.ItemDetails<Long>() { override fun getPosition(): Int = adapterPosition override fun getSelectionKey(): Long? = itemId } } }}
Layout
R.layout.testselection
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.luasoftware.garden.view.TestSelectionFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
R.layout.testselection_item
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
tools:layout_height="500dp">
<TextView
android:id="@+id/numberTextView"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Test">
</TextView>
<ImageView
android:id="@+id/selectionImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_baseline_radio_button_unchecked_24"
android:tint="@android:color/black"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note
- Long press an item to trigger item selection, where the second item can be selected/deselected via click.
- When no items is selected (after deselect), a long press is required again to trigger item selection
SelectionTracker
will triggerAdapter.onBindViewHolder
for UI update, and you can listen to selection event viaSelectionTracker.addObserver
.- The example above using item postion (Long) as Id Key
- If you accidentally build
SelectionTracker
and assigned toAdapter.tracker
more than once, there will be strange UI bug whereAdapter.onBindViewHolder
is not triggered accurately. - Refer to RecyclerView Selection With String Key and SelectionPredicate (allow certain items to be selected)
References: