RecyclerView Selection With String Key and SelectionPredicate
June 19, 2020Only allow certain items to be selected
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
- algolia
- analytics
- android
- android-ktx
- android-permission
- android-studio
- apps-script
- bash
- bootstrap
- bootstrapvue
- chartjs
- chrome
- cloud-functions
- coding-interview
- coroutines
- crashlytics
- css
- dagger2
- datastore
- datetime
- docker
- eslint
- firebase
- firebase-auth
- firebase-hosting
- firestore
- firestore-security-rules
- flask
- fontawesome
- fresco
- git
- github
- glide
- google-app-engine
- google-cloud-storage
- google-colab
- google-drive
- google-maps
- google-places
- google-play
- google-sheets
- gradle
- html
- hugo
- inkscape
- java
- java-time
- javascript
- jetson-nano
- kotlin
- layout
- lets-encrypt
- lifecycle
- linux
- logging
- lubuntu
- markdown
- mate
- material-design
- matplotlib
- md5
- mongodb
- moshi
- mplfinance
- mysql
- navigation
- nginx
- nodejs
- npm
- nuxtjs
- nvm
- payment
- pip
- pwa
- pyenv
- python
- recylerview
- regex
- room
- rxjava
- scoped-storage
- selenium
- social-media
- ssh
- ssl
- static-site-generator
- static-website-hosting
- sublime-text
- ubuntu
- unit-test
- uwsgi
- viewmodel
- viewpager2
- virtualbox
- vue-cli
- vue-router
- vuejs
- vuelidate
- vuepress
- web-development
- web-hosting
- webpack
- windows
- workmanager
- wsl
- yarn