Dependencies
dependencies {
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
// implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01'
}
Fragment / Activity
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<String>( "number-selection", list, // StableIdKeyProvider(list), LocalItemKeyProvider(adapter), LocalItemDetailsLookup(list), StorageStrategy.createStringStorage() ).withSelectionPredicate( // SelectionPredicates.createSelectAnything() // selectionPredicates LocalSelectionPredicate(adapter) ).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 0..10) { if (number % 2 == 0) { val key = "number-$number" items.add(ViewItem.NumberItem(key)) } else { val key = "noselect-$number" items.add(ViewItem.NotSelectable(key)) } } adapter.submitList(items) } private class LocalItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<String>() { override fun getItemDetails(event: MotionEvent): ItemDetails<String>? { val view = recyclerView.findChildViewUnder(event.x, event.y) if (view != null) { return (recyclerView.getChildViewHolder(view) as LocalAdapter.ViewHolder) .getItemDetails() } return null } } private class LocalSelectionPredicate(private val adapter: LocalAdapter): SelectionTracker.SelectionPredicate<String>() { override fun canSelectMultiple(): Boolean { return true } override fun canSetStateForKey(key: String, nextState: Boolean): Boolean { val item = adapter.currentList.find { it.id == key } return item is ViewItem.NumberItem } override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean { val item = adapter.currentList[position] return item is ViewItem.NumberItem } } private class LocalItemKeyProvider(private val adapter: LocalAdapter) : ItemKeyProvider<String>(ItemKeyProvider.SCOPE_MAPPED) { override fun getKey(position: Int): String { return adapter.currentList[position].id } override fun getPosition(key: String): Int { // RecyclerView.NO_POSITION = -1 return adapter.currentList.indexOfFirst { it.id == key } } } sealed class ViewItem(open val id: String, val resource: Int) { data class NumberItem(override val id: String) : ViewItem(id, R.layout.testselection_item) data class NotSelectable(override val id: String) : ViewItem(id, R.layout.testselection_static_item) } class LocalAdapter() : ListAdapter<ViewItem, LocalAdapter.ViewHolder>(DiffCallback()) { lateinit var tracker: SelectionTracker<String> // 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) { val item = getItem(position) Timber.d("position=$position, selected=${tracker.isSelected(item.id)}, selection=${tracker.selection}") holder.bind(getItem(position), tracker.isSelected(item.id)) } 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<String> = object : ItemDetailsLookup.ItemDetails<String>() { override fun getPosition(): Int = adapterPosition override fun getSelectionKey(): String? = getItem(adapterPosition).id } } }}
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>
R.layout.testselection_static_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: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"
android:text="NO SELECT">
</TextView>
</androidx.constraintlayout.widget.ConstraintLayout>
NOTE: Refer to some RecyclerView Selection Behaviour Notes