1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

refactor MultiChoiceHelper to allow changing its underlying adapter

This commit is contained in:
Christophe Beyls 2020-03-27 22:47:42 +01:00
parent 02e6887c45
commit a9c76680e4
4 changed files with 103 additions and 110 deletions

View file

@ -1,5 +1,6 @@
package be.digitalia.fosdem.adapters package be.digitalia.fosdem.adapters
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.text.SpannableString import android.text.SpannableString
@ -10,16 +11,15 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.SimpleArrayMap import androidx.collection.SimpleArrayMap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.set import androidx.core.text.set
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.savedstate.SavedStateRegistryOwner
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.EventDetailsActivity import be.digitalia.fosdem.activities.EventDetailsActivity
import be.digitalia.fosdem.api.FosdemApi import be.digitalia.fosdem.api.FosdemApi
@ -29,11 +29,12 @@ import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.widgets.MultiChoiceHelper import be.digitalia.fosdem.widgets.MultiChoiceHelper
import java.text.DateFormat import java.text.DateFormat
class BookmarksAdapter(activity: AppCompatActivity, owner: SavedStateRegistryOwner, class BookmarksAdapter(context: Context, owner: LifecycleOwner,
multiChoiceModeListener: MultiChoiceHelper.MultiChoiceModeListener? = null) private val multiChoiceHelper: MultiChoiceHelper)
: ListAdapter<Event, BookmarksAdapter.ViewHolder>(DIFF_CALLBACK) { : ListAdapter<Event, BookmarksAdapter.ViewHolder>(DIFF_CALLBACK) {
private val timeDateFormat = DateUtils.getTimeDateFormat(activity) private val timeDateFormat = DateUtils.getTimeDateFormat(context)
@ColorInt @ColorInt
private val errorColor: Int private val errorColor: Int
private val observers = SimpleArrayMap<AdapterDataObserver, BookmarksDataObserverWrapper>() private val observers = SimpleArrayMap<AdapterDataObserver, BookmarksDataObserverWrapper>()
@ -41,20 +42,16 @@ class BookmarksAdapter(activity: AppCompatActivity, owner: SavedStateRegistryOwn
init { init {
setHasStableIds(true) setHasStableIds(true)
with(activity.theme.obtainStyledAttributes(R.styleable.ErrorColors)) { with(context.theme.obtainStyledAttributes(R.styleable.ErrorColors)) {
errorColor = getColor(R.styleable.ErrorColors_colorError, 0) errorColor = getColor(R.styleable.ErrorColors_colorError, 0)
recycle() recycle()
} }
FosdemApi.getRoomStatuses(activity).observe(owner) { statuses -> FosdemApi.getRoomStatuses(context).observe(owner) { statuses ->
roomStatuses = statuses roomStatuses = statuses
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD) notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
} }
} }
val multiChoiceHelper = MultiChoiceHelper(activity, owner, this).apply {
setMultiChoiceModeListener(multiChoiceModeListener)
}
override fun getItemId(position: Int) = getItem(position).id override fun getItemId(position: Int) = getItem(position).id
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {

View file

@ -32,8 +32,8 @@ import be.digitalia.fosdem.widgets.MultiChoiceHelper
class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback { class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
private val viewModel: BookmarksViewModel by viewModels() private val viewModel: BookmarksViewModel by viewModels()
private val adapter: BookmarksAdapter by lazy(LazyThreadSafetyMode.NONE) { private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
val multiChoiceModeListener = object : MultiChoiceHelper.MultiChoiceModeListener { MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.action_mode_bookmarks, menu) mode.menuInflater.inflate(R.menu.action_mode_bookmarks, menu)
@ -41,7 +41,7 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
} }
private fun updateSelectedCountDisplay(mode: ActionMode) { private fun updateSelectedCountDisplay(mode: ActionMode) {
val count = adapter.multiChoiceHelper.checkedItemCount val count = multiChoiceHelper.checkedItemCount
mode.title = resources.getQuantityString(R.plurals.selected, count, count) mode.title = resources.getQuantityString(R.plurals.selected, count, count)
} }
@ -53,7 +53,7 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem) = when (item.itemId) { override fun onActionItemClicked(mode: ActionMode, item: MenuItem) = when (item.itemId) {
R.id.delete -> { R.id.delete -> {
// Remove multiple bookmarks at once // Remove multiple bookmarks at once
viewModel.removeBookmarks(adapter.multiChoiceHelper.checkedItemIds) viewModel.removeBookmarks(multiChoiceHelper.checkedItemIds)
mode.finish() mode.finish()
true true
} }
@ -65,9 +65,10 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
} }
override fun onDestroyActionMode(mode: ActionMode) {} override fun onDestroyActionMode(mode: ActionMode) {}
})
} }
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
BookmarksAdapter((requireActivity() as AppCompatActivity), this, multiChoiceModeListener) BookmarksAdapter(requireContext(), this, multiChoiceHelper)
} }
private var filterMenuItem: MenuItem? = null private var filterMenuItem: MenuItem? = null
private var upcomingOnlyMenuItem: MenuItem? = null private var upcomingOnlyMenuItem: MenuItem? = null
@ -95,6 +96,7 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks -> viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks) adapter.submitList(bookmarks)
multiChoiceHelper.setAdapter(adapter, this)
isProgressBarVisible = false isProgressBarVisible = false
} }
} }

View file

@ -17,30 +17,24 @@ object DateParceler : Parceler<Date?> {
override fun Date?.write(parcel: Parcel, flags: Int) = parcel.writeLong(this?.time ?: -1L) override fun Date?.write(parcel: Parcel, flags: Int) = parcel.writeLong(this?.time ?: -1L)
} }
object IntLongSparseArrayParceler : Parceler<LongSparseArray<Int>?> { object IntLongSparseArrayParceler : Parceler<LongSparseArray<Int>> {
override fun create(parcel: Parcel): LongSparseArray<Int>? { override fun create(parcel: Parcel): LongSparseArray<Int> {
val size = parcel.readInt() val size = parcel.readInt()
return if (size >= 0) { return LongSparseArray<Int>(size).apply {
LongSparseArray<Int>(size).apply {
for (i in 0 until size) { for (i in 0 until size) {
val key = parcel.readLong() val key = parcel.readLong()
val value = parcel.readInt() val value = parcel.readInt()
append(key, value) append(key, value)
} }
} }
} else null
} }
override fun LongSparseArray<Int>?.write(parcel: Parcel, flags: Int) { override fun LongSparseArray<Int>.write(parcel: Parcel, flags: Int) {
if (this == null) {
parcel.writeInt(-1)
} else {
parcel.writeInt(size) parcel.writeInt(size)
forEach { key, value -> forEach { key, value ->
parcel.writeLong(key) parcel.writeLong(key)
parcel.writeInt(value) parcel.writeInt(value)
} }
} }
}
} }

View file

@ -11,6 +11,7 @@ import androidx.appcompat.view.ActionMode
import androidx.core.util.set import androidx.core.util.set
import androidx.core.util.size import androidx.core.util.size
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
@ -21,13 +22,11 @@ import kotlinx.android.parcel.WriteWith
/** /**
* Helper class to reproduce ListView's modal MultiChoice mode with a RecyclerView. * Helper class to reproduce ListView's modal MultiChoice mode with a RecyclerView.
* Declare and use this class from inside your Adapter. * Call setAdapter(adapter, adapterLifecycleOwner) on it as soon as the adapter data is populated.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
class MultiChoiceHelper(private val activity: AppCompatActivity, class MultiChoiceHelper(private val activity: AppCompatActivity, owner: SavedStateRegistryOwner, listener: MultiChoiceModeListener) {
owner: SavedStateRegistryOwner,
private val adapter: RecyclerView.Adapter<*>) {
/** /**
* A handy ViewHolder base class which works with the MultiChoiceHelper * A handy ViewHolder base class which works with the MultiChoiceHelper
* and reproduces the default behavior of a ListView. * and reproduces the default behavior of a ListView.
@ -84,7 +83,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
/** /**
* Called when an item is checked or unchecked during selection mode. * Called when an item is checked or unchecked during selection mode.
* *
* @param mode The [ActionMode] providing the selection startSupportActionModemode * @param mode The [ActionMode] providing the selection mode
* @param position Adapter position of the item that was checked or unchecked * @param position Adapter position of the item that was checked or unchecked
* @param id Adapter ID of the item that was checked or unchecked * @param id Adapter ID of the item that was checked or unchecked
* @param checked `true` if the item is now checked, `false` * @param checked `true` if the item is now checked, `false`
@ -93,11 +92,14 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean)
} }
private val multiChoiceModeCallback: MultiChoiceModeListener = MultiChoiceModeWrapper(listener)
val checkedItemPositions: SparseBooleanArray val checkedItemPositions: SparseBooleanArray
private val checkedIdStates: LongSparseArray<Int>? private val checkedIdStates: LongSparseArray<Int>
var checkedItemCount: Int var checkedItemCount: Int
private set private set
private var multiChoiceModeCallback: MultiChoiceModeWrapper? = null var adapter: RecyclerView.Adapter<*>? = null
private set
private var adapterLifecycle: Lifecycle? = null
private var choiceActionMode: ActionMode? = null private var choiceActionMode: ActionMode? = null
/** /**
@ -105,36 +107,66 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
* so this class will be notified before the RecyclerView in case of data set changes. * so this class will be notified before the RecyclerView in case of data set changes.
*/ */
init { init {
adapter.registerAdapterDataObserver(AdapterDataSetObserver())
val restoreBundle = owner.savedStateRegistry.consumeRestoredStateForKey(STATE_KEY) val restoreBundle = owner.savedStateRegistry.consumeRestoredStateForKey(STATE_KEY)
if (restoreBundle == null) { if (restoreBundle == null) {
checkedItemCount = 0 checkedItemCount = 0
checkedItemPositions = SparseBooleanArray(0) checkedItemPositions = SparseBooleanArray(0)
checkedIdStates = if (adapter.hasStableIds()) LongSparseArray(0) else null checkedIdStates = LongSparseArray(0)
} else { } else {
val savedState: SavedState = restoreBundle.getParcelable(PARCELABLE_KEY)!! val savedState: SavedState = restoreBundle.getParcelable(PARCELABLE_KEY)!!
checkedItemCount = savedState.checkedItemCount checkedItemCount = savedState.checkedItemCount
checkedItemPositions = savedState.checkedItemPositions checkedItemPositions = savedState.checkedItemPositions
checkedIdStates = savedState.checkedIdStates checkedIdStates = savedState.checkedIdStates
// Try early restoration, otherwise do it when items are inserted
if (adapter.itemCount > 0) {
onAdapterPopulated()
}
} }
owner.savedStateRegistry.registerSavedStateProvider(STATE_KEY) { owner.savedStateRegistry.registerSavedStateProvider(STATE_KEY) {
Bundle(1).apply { Bundle(1).apply {
putParcelable(PARCELABLE_KEY, SavedState(checkedItemCount, checkedItemPositions.clone(), checkedIdStates?.clone())) putParcelable(PARCELABLE_KEY, SavedState(checkedItemCount, checkedItemPositions.clone(), checkedIdStates.clone()))
} }
} }
owner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
clearChoices()
}
})
} }
fun setMultiChoiceModeListener(listener: MultiChoiceModeListener?) { private val adapterDataSetObserver = object : AdapterDataObserver() {
multiChoiceModeCallback = if (listener == null) null else MultiChoiceModeWrapper(listener) override fun onChanged() {
confirmCheckedPositions()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
confirmCheckedPositions()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
confirmCheckedPositions()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
confirmCheckedPositions()
}
}
private val adapterLifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
adapter = null
adapterLifecycle = null
choiceActionMode?.finish()
}
}
fun setAdapter(adapter: RecyclerView.Adapter<*>, adapterLifecycleOwner: LifecycleOwner) {
if (this.adapter !== adapter) {
this.adapter?.unregisterAdapterDataObserver(adapterDataSetObserver)
this.adapterLifecycle?.removeObserver(adapterLifecycleObserver)
this.adapter = adapter
this.adapterLifecycle = adapterLifecycleOwner.lifecycle
adapter.registerAdapterDataObserver(adapterDataSetObserver)
adapterLifecycleOwner.lifecycle.addObserver(adapterLifecycleObserver)
if (!adapter.hasStableIds()) {
checkedIdStates.clear()
}
confirmCheckedPositions()
if (checkedItemCount > 0) {
startSupportActionModeIfNeeded()
}
}
} }
fun isItemChecked(position: Int): Boolean { fun isItemChecked(position: Int): Boolean {
@ -143,7 +175,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
val checkedItemIds: LongArray val checkedItemIds: LongArray
get() { get() {
val idStates = checkedIdStates ?: return LongArray(0) val idStates = checkedIdStates
return LongArray(idStates.size()) { idStates.keyAt(it) } return LongArray(idStates.size()) { idStates.keyAt(it) }
} }
@ -152,10 +184,10 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
val start = checkedItemPositions.keyAt(0) val start = checkedItemPositions.keyAt(0)
val end = checkedItemPositions.keyAt(checkedItemPositions.size() - 1) val end = checkedItemPositions.keyAt(checkedItemPositions.size() - 1)
checkedItemPositions.clear() checkedItemPositions.clear()
checkedIdStates?.clear() checkedIdStates.clear()
checkedItemCount = 0 checkedItemCount = 0
adapter.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD) adapter?.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD)
choiceActionMode?.finish() choiceActionMode?.finish()
} }
@ -171,9 +203,9 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedItemPositions[position] = value checkedItemPositions[position] = value
if (oldValue != value) { if (oldValue != value) {
val id = adapter.getItemId(position) val id = adapter?.getItemId(position) ?: RecyclerView.NO_ID
if (checkedIdStates != null) { if (adapter?.hasStableIds() == true) {
if (value) { if (value) {
checkedIdStates[id] = position checkedIdStates[id] = position
} else { } else {
@ -187,13 +219,12 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedItemCount-- checkedItemCount--
} }
adapter.notifyItemChanged(position, SELECTION_PAYLOAD) adapter?.notifyItemChanged(position, SELECTION_PAYLOAD)
val actionMode = choiceActionMode choiceActionMode?.let {
if (actionMode != null) { multiChoiceModeCallback.onItemCheckedStateChanged(it, position, id, value)
multiChoiceModeCallback?.onItemCheckedStateChanged(actionMode, position, id, value)
if (checkedItemCount == 0) { if (checkedItemCount == 0) {
actionMode.finish() it.finish()
} }
} }
} }
@ -203,21 +234,14 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
setItemChecked(position, !isItemChecked(position)) setItemChecked(position, !isItemChecked(position))
} }
private fun onAdapterPopulated() {
confirmCheckedPositions()
if (checkedItemCount > 0) {
startSupportActionModeIfNeeded()
}
}
private fun startSupportActionModeIfNeeded() { private fun startSupportActionModeIfNeeded() {
if (choiceActionMode == null) { if (choiceActionMode == null) {
val callback = checkNotNull(multiChoiceModeCallback) { "No callback set" } choiceActionMode = activity.startSupportActionMode(multiChoiceModeCallback)
choiceActionMode = activity.startSupportActionMode(callback)
} }
} }
fun confirmCheckedPositions() { fun confirmCheckedPositions() {
val adapter = this.adapter ?: return
if (checkedItemCount == 0) { if (checkedItemCount == 0) {
return return
} }
@ -228,10 +252,10 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
if (itemCount == 0) { if (itemCount == 0) {
// Optimized path for empty adapter: remove all items. // Optimized path for empty adapter: remove all items.
checkedItemPositions.clear() checkedItemPositions.clear()
checkedIdStates?.clear() checkedIdStates.clear()
checkedItemCount = 0 checkedItemCount = 0
checkedCountChanged = true checkedCountChanged = true
} else if (checkedIdStates != null) { } else if (adapter.hasStableIds()) {
// Clear out the positional check states, we'll rebuild it below from IDs. // Clear out the positional check states, we'll rebuild it below from IDs.
checkedItemPositions.clear() checkedItemPositions.clear()
@ -260,10 +284,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedIndex-- checkedIndex--
checkedItemCount-- checkedItemCount--
checkedCountChanged = true checkedCountChanged = true
val actionMode = choiceActionMode choiceActionMode?.let { multiChoiceModeCallback.onItemCheckedStateChanged(it, lastPos, id, false) }
if (actionMode != null) {
multiChoiceModeCallback?.onItemCheckedStateChanged(actionMode, lastPos, id, false)
}
} }
} else { } else {
checkedItemPositions[lastPos] = true checkedItemPositions[lastPos] = true
@ -285,12 +306,11 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
} }
} }
val actionMode = choiceActionMode if (checkedCountChanged) {
if (checkedCountChanged && actionMode != null) {
if (checkedItemCount == 0) { if (checkedItemCount == 0) {
actionMode.finish() choiceActionMode?.finish()
} else { } else {
actionMode.invalidate() choiceActionMode?.invalidate()
} }
} }
} }
@ -298,27 +318,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
@Parcelize @Parcelize
class SavedState(val checkedItemCount: Int, class SavedState(val checkedItemCount: Int,
val checkedItemPositions: SparseBooleanArray, val checkedItemPositions: SparseBooleanArray,
val checkedIdStates: @WriteWith<IntLongSparseArrayParceler> LongSparseArray<Int>?) : Parcelable val checkedIdStates: @WriteWith<IntLongSparseArrayParceler> LongSparseArray<Int>) : Parcelable
private inner class AdapterDataSetObserver : AdapterDataObserver() {
override fun onChanged() {
confirmCheckedPositions()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (itemCount > 0) {
onAdapterPopulated()
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
confirmCheckedPositions()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
confirmCheckedPositions()
}
}
private inner class MultiChoiceModeWrapper(private val wrapped: MultiChoiceModeListener) : MultiChoiceModeListener by wrapped { private inner class MultiChoiceModeWrapper(private val wrapped: MultiChoiceModeListener) : MultiChoiceModeListener by wrapped {