I would like to do multi selection in my android app. I have done it before, but only with an ArrayAdapter.
I have a Flow as my DataSet and I use a PagingDataAdapter with a ViewHolder.
My question is, how to do multi selection if the dataset ist not just a List and i can't really access it that easy.
If your want to lookup the code, rewiew this:
I have implemented a custom way of doing it.
I used an observable list in the adapter and expose methods to the viewholders to select themselfs.
You could of course create a base class for this. I should eventually do that too :)
My Code:
class PhotoAdapter(
private val context: Context,
private val photoRepository: PhotoRepository,
private val viewPhotoCallback: KFunction1<Int, Unit>,
val lifecycleOwner: LifecycleOwner
) : PagingDataAdapter<Photo, PhotoItemViewHolder>(differCallback) {
/**
* Holds the layout positions of the selected items.
*/
val selectedItems = ObservableArrayList<Int>()
/**
* Holds a Boolean indicating if multi selection is enabled. In a LiveData.
*/
var isMultiSelectMode: MutableLiveData<Boolean> = MutableLiveData(false)
override fun onBindViewHolder(holderItem: PhotoItemViewHolder, position: Int) {
holderItem.bindTo(this, getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoItemViewHolder =
PhotoItemViewHolder(parent, context, photoRepository)
/**
* Called by ui. On Click.
*/
fun viewPhoto(position: Int) {
viewPhotoCallback.invoke(getItem(position)?.id!!)
}
/**
* Disables multi selection.
*/
fun disableSelection() {
selectedItems.clear()
isMultiSelectMode.postValue(false)
}
/**
* Enables multi selection.
*/
fun enableSelection() {
isMultiSelectMode.postValue(true)
}
/**
* Add an item it the selection.
*/
fun addItemToSelection(position: Int): Boolean = selectedItems.add(position)
/**
* Remove an item to the selection.
*/
fun removeItemFromSelection(position: Int) = selectedItems.remove(position)
/**
* Indicate if an item is already selected.
*/
fun isItemSelected(position: Int) = selectedItems.contains(position)
/**
* Indicate if an item is the last selected.
*/
fun isLastSelectedItem(position: Int) = isItemSelected(position) && selectedItems.size == 1
/**
* Select all items.
*/
fun selectAll() {
for (i in 0 until itemCount) {
if (!isItemSelected(i)) {
addItemToSelection(i)
}
}
}
/**
* Get all items that are selected.
*/
fun getAllSelected(): List<Photo> {
val items = mutableListOf<Photo>()
for(position in selectedItems) {
val photo = getItem(position)
if (photo != null) {
items.add(photo)
}
}
return items
}
companion object {
private val differCallback = object : DiffUtil.ItemCallback<Photo>() {
override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean =
oldItem == newItem
}
}
}
class PhotoItemViewHolder(
parent: ViewGroup,
private val context: Context,
private val photoRepository: PhotoRepository
) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.photo_item, parent, false)
) {
private val imageView: ImageView = itemView.findViewById(R.id.photoItemImageView)
private val checkBox: CheckBox = itemView.findViewById(R.id.photoItemCheckBox)
var photo: Photo? = null
private lateinit var adapter: PhotoAdapter
/**
* Binds the parent adapter and the photo to the ViewHolder.
*/
fun bindTo(adapter: PhotoAdapter, photo: Photo?) {
this.photo = photo
this.adapter = adapter
imageView.setOnClickListener {
if (adapter.isMultiSelectMode.value!!) {
// If the item clicked is the last selected item
if (adapter.isLastSelectedItem(layoutPosition)) {
adapter.disableSelection()
return@setOnClickListener
}
// Set checked if not already checked
setItemChecked(!adapter.isItemSelected(layoutPosition))
} else {
adapter.viewPhoto(layoutPosition)
}
}
imageView.setOnLongClickListener {
if (!adapter.isMultiSelectMode.value!!) {
adapter.enableSelection()
setItemChecked(true)
}
true
}
adapter.isMultiSelectMode.observe(adapter.lifecycleOwner, {
if (it) { // When selection gets enabled, show the checkbox
checkBox.show()
} else {
checkBox.hide()
}
})
adapter.selectedItems.addOnListChangedCallback(onSelectedItemsChanged)
listChanged()
loadThumbnail()
}
/**
* Listener for changes in selected images.
* Calls [listChanged] whatever happens.
*/
private val onSelectedItemsChanged =
object : ObservableList.OnListChangedCallback<ObservableList<Int>>() {
override fun onChanged(sender: ObservableList<Int>?) {
listChanged()
}
override fun onItemRangeChanged(
sender: ObservableList<Int>?,
positionStart: Int,
itemCount: Int
) {
listChanged()
}
override fun onItemRangeInserted(
sender: ObservableList<Int>?,
positionStart: Int,
itemCount: Int
) {
listChanged()
}
override fun onItemRangeMoved(
sender: ObservableList<Int>?,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
listChanged()
}
override fun onItemRangeRemoved(
sender: ObservableList<Int>?,
positionStart: Int,
itemCount: Int
) {
listChanged()
}
}
private fun listChanged() {
val isSelected = adapter.isItemSelected(layoutPosition)
val padding = if (isSelected) 20 else 0
checkBox.isChecked = isSelected
imageView.setPadding(padding)
}
private fun setItemChecked(checked: Boolean) {
layoutPosition.let {
if (checked) {
adapter.addItemToSelection(it)
} else {
adapter.removeItemFromSelection(it)
}
}
}
/**
* Load the thumbnail for the [photo].
*/
private fun loadThumbnail() {
GlobalScope.launch(Dispatchers.IO) {
val thumbnailBytes =
photoRepository.readPhotoThumbnailFromInternal(context, photo?.id!!)
if (thumbnailBytes == null) {
Timber.d("Error loading thumbnail for photo: $photo.id")
return@launch
}
val thumbnailBitmap =
BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.size)
runOnMain { // Set thumbnail in main thread
imageView.setImageBitmap(thumbnailBitmap)
}
}
}
}