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

View file

@ -32,8 +32,8 @@ import be.digitalia.fosdem.widgets.MultiChoiceHelper
class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
private val viewModel: BookmarksViewModel by viewModels()
private val adapter: BookmarksAdapter by lazy(LazyThreadSafetyMode.NONE) {
val multiChoiceModeListener = object : MultiChoiceHelper.MultiChoiceModeListener {
private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.action_mode_bookmarks, menu)
@ -41,7 +41,7 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
}
private fun updateSelectedCountDisplay(mode: ActionMode) {
val count = adapter.multiChoiceHelper.checkedItemCount
val count = multiChoiceHelper.checkedItemCount
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) {
R.id.delete -> {
// Remove multiple bookmarks at once
viewModel.removeBookmarks(adapter.multiChoiceHelper.checkedItemIds)
viewModel.removeBookmarks(multiChoiceHelper.checkedItemIds)
mode.finish()
true
}
@ -65,9 +65,10 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
}
override fun onDestroyActionMode(mode: ActionMode) {}
}
BookmarksAdapter((requireActivity() as AppCompatActivity), this, multiChoiceModeListener)
})
}
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
BookmarksAdapter(requireContext(), this, multiChoiceHelper)
}
private var filterMenuItem: MenuItem? = null
private var upcomingOnlyMenuItem: MenuItem? = null
@ -95,6 +96,7 @@ class BookmarksListFragment : RecyclerViewFragment(), CreateNfcAppDataCallback {
viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks)
multiChoiceHelper.setAdapter(adapter, this)
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)
}
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()
return if (size >= 0) {
LongSparseArray<Int>(size).apply {
for (i in 0 until size) {
val key = parcel.readLong()
val value = parcel.readInt()
append(key, value)
}
}
} else null
}
override fun LongSparseArray<Int>?.write(parcel: Parcel, flags: Int) {
if (this == null) {
parcel.writeInt(-1)
} else {
parcel.writeInt(size)
forEach { key, value ->
parcel.writeLong(key)
parcel.writeInt(value)
return LongSparseArray<Int>(size).apply {
for (i in 0 until size) {
val key = parcel.readLong()
val value = parcel.readInt()
append(key, value)
}
}
}
override fun LongSparseArray<Int>.write(parcel: Parcel, flags: Int) {
parcel.writeInt(size)
forEach { key, value ->
parcel.writeLong(key)
parcel.writeInt(value)
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.appcompat.view.ActionMode
import androidx.core.util.set
import androidx.core.util.size
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
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.
* 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
*/
class MultiChoiceHelper(private val activity: AppCompatActivity,
owner: SavedStateRegistryOwner,
private val adapter: RecyclerView.Adapter<*>) {
class MultiChoiceHelper(private val activity: AppCompatActivity, owner: SavedStateRegistryOwner, listener: MultiChoiceModeListener) {
/**
* A handy ViewHolder base class which works with the MultiChoiceHelper
* 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.
*
* @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 id Adapter ID of the item that was checked or unchecked
* @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)
}
private val multiChoiceModeCallback: MultiChoiceModeListener = MultiChoiceModeWrapper(listener)
val checkedItemPositions: SparseBooleanArray
private val checkedIdStates: LongSparseArray<Int>?
private val checkedIdStates: LongSparseArray<Int>
var checkedItemCount: Int
private set
private var multiChoiceModeCallback: MultiChoiceModeWrapper? = null
var adapter: RecyclerView.Adapter<*>? = null
private set
private var adapterLifecycle: Lifecycle? = 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.
*/
init {
adapter.registerAdapterDataObserver(AdapterDataSetObserver())
val restoreBundle = owner.savedStateRegistry.consumeRestoredStateForKey(STATE_KEY)
if (restoreBundle == null) {
checkedItemCount = 0
checkedItemPositions = SparseBooleanArray(0)
checkedIdStates = if (adapter.hasStableIds()) LongSparseArray(0) else null
checkedIdStates = LongSparseArray(0)
} else {
val savedState: SavedState = restoreBundle.getParcelable(PARCELABLE_KEY)!!
checkedItemCount = savedState.checkedItemCount
checkedItemPositions = savedState.checkedItemPositions
checkedIdStates = savedState.checkedIdStates
// Try early restoration, otherwise do it when items are inserted
if (adapter.itemCount > 0) {
onAdapterPopulated()
}
}
owner.savedStateRegistry.registerSavedStateProvider(STATE_KEY) {
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?) {
multiChoiceModeCallback = if (listener == null) null else MultiChoiceModeWrapper(listener)
private val adapterDataSetObserver = object : AdapterDataObserver() {
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 {
@ -143,7 +175,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
val checkedItemIds: LongArray
get() {
val idStates = checkedIdStates ?: return LongArray(0)
val idStates = checkedIdStates
return LongArray(idStates.size()) { idStates.keyAt(it) }
}
@ -152,10 +184,10 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
val start = checkedItemPositions.keyAt(0)
val end = checkedItemPositions.keyAt(checkedItemPositions.size() - 1)
checkedItemPositions.clear()
checkedIdStates?.clear()
checkedIdStates.clear()
checkedItemCount = 0
adapter.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD)
adapter?.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD)
choiceActionMode?.finish()
}
@ -171,9 +203,9 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedItemPositions[position] = 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) {
checkedIdStates[id] = position
} else {
@ -187,13 +219,12 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedItemCount--
}
adapter.notifyItemChanged(position, SELECTION_PAYLOAD)
adapter?.notifyItemChanged(position, SELECTION_PAYLOAD)
val actionMode = choiceActionMode
if (actionMode != null) {
multiChoiceModeCallback?.onItemCheckedStateChanged(actionMode, position, id, value)
choiceActionMode?.let {
multiChoiceModeCallback.onItemCheckedStateChanged(it, position, id, value)
if (checkedItemCount == 0) {
actionMode.finish()
it.finish()
}
}
}
@ -203,21 +234,14 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
setItemChecked(position, !isItemChecked(position))
}
private fun onAdapterPopulated() {
confirmCheckedPositions()
if (checkedItemCount > 0) {
startSupportActionModeIfNeeded()
}
}
private fun startSupportActionModeIfNeeded() {
if (choiceActionMode == null) {
val callback = checkNotNull(multiChoiceModeCallback) { "No callback set" }
choiceActionMode = activity.startSupportActionMode(callback)
choiceActionMode = activity.startSupportActionMode(multiChoiceModeCallback)
}
}
fun confirmCheckedPositions() {
val adapter = this.adapter ?: return
if (checkedItemCount == 0) {
return
}
@ -228,10 +252,10 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
if (itemCount == 0) {
// Optimized path for empty adapter: remove all items.
checkedItemPositions.clear()
checkedIdStates?.clear()
checkedIdStates.clear()
checkedItemCount = 0
checkedCountChanged = true
} else if (checkedIdStates != null) {
} else if (adapter.hasStableIds()) {
// Clear out the positional check states, we'll rebuild it below from IDs.
checkedItemPositions.clear()
@ -260,10 +284,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
checkedIndex--
checkedItemCount--
checkedCountChanged = true
val actionMode = choiceActionMode
if (actionMode != null) {
multiChoiceModeCallback?.onItemCheckedStateChanged(actionMode, lastPos, id, false)
}
choiceActionMode?.let { multiChoiceModeCallback.onItemCheckedStateChanged(it, lastPos, id, false) }
}
} else {
checkedItemPositions[lastPos] = true
@ -285,12 +306,11 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
}
}
val actionMode = choiceActionMode
if (checkedCountChanged && actionMode != null) {
if (checkedCountChanged) {
if (checkedItemCount == 0) {
actionMode.finish()
choiceActionMode?.finish()
} else {
actionMode.invalidate()
choiceActionMode?.invalidate()
}
}
}
@ -298,27 +318,7 @@ class MultiChoiceHelper(private val activity: AppCompatActivity,
@Parcelize
class SavedState(val checkedItemCount: Int,
val checkedItemPositions: SparseBooleanArray,
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()
}
}
val checkedIdStates: @WriteWith<IntLongSparseArrayParceler> LongSparseArray<Int>) : Parcelable
private inner class MultiChoiceModeWrapper(private val wrapped: MultiChoiceModeListener) : MultiChoiceModeListener by wrapped {