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:
parent
02e6887c45
commit
a9c76680e4
4 changed files with 103 additions and 110 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
return 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 {
|
||||
override fun LongSparseArray<Int>.write(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(size)
|
||||
forEach { key, value ->
|
||||
parcel.writeLong(key)
|
||||
parcel.writeInt(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
Loading…
Reference in a new issue