Handle nested scroll state in shelf
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
import androidx.core.view.doOnNextLayout
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
class NestedScrollStateHandle(
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
private val key: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val storage: SparseArray<Parcelable?> = savedInstanceState?.let {
|
||||||
|
BundleCompat.getSparseParcelableArray(it, key, Parcelable::class.java)
|
||||||
|
} ?: SparseArray<Parcelable?>()
|
||||||
|
private val controllers = Collections.newSetFromMap<Controller>(WeakHashMap())
|
||||||
|
|
||||||
|
fun attach(recycler: RecyclerView) = Controller(recycler).also(controllers::add)
|
||||||
|
|
||||||
|
fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
controllers.forEach {
|
||||||
|
it.saveState()
|
||||||
|
}
|
||||||
|
outState.putSparseParcelableArray(key, storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Controller(
|
||||||
|
private val recycler: RecyclerView
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var lastPosition: Int = -1
|
||||||
|
|
||||||
|
fun onBind(position: Int) {
|
||||||
|
if (position != lastPosition) {
|
||||||
|
saveState()
|
||||||
|
lastPosition = position
|
||||||
|
storage[position]?.let {
|
||||||
|
restoreState(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRecycled() {
|
||||||
|
saveState()
|
||||||
|
lastPosition = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveState() {
|
||||||
|
if (lastPosition != -1) {
|
||||||
|
storage[lastPosition] = recycler.layoutManager?.onSaveInstanceState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreState(state: Parcelable) {
|
||||||
|
recycler.doOnNextLayout {
|
||||||
|
recycler.layoutManager?.onRestoreInstanceState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
|
||||||
@@ -47,6 +48,7 @@ class ShelfFragment :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
|
private var nestedScrollStateHandle: NestedScrollStateHandle? = null
|
||||||
private val viewModel by viewModels<ShelfViewModel>()
|
private val viewModel by viewModels<ShelfViewModel>()
|
||||||
private var adapter: ShelfAdapter? = null
|
private var adapter: ShelfAdapter? = null
|
||||||
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
|
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
|
||||||
@@ -60,6 +62,7 @@ class ShelfFragment :
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
nestedScrollStateHandle = NestedScrollStateHandle(savedInstanceState, KEY_NESTED_SCROLL)
|
||||||
val sizeResolver = ItemSizeResolver(resources, settings)
|
val sizeResolver = ItemSizeResolver(resources, settings)
|
||||||
selectionController = SectionedSelectionController(
|
selectionController = SectionedSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
@@ -72,6 +75,7 @@ class ShelfFragment :
|
|||||||
listener = this,
|
listener = this,
|
||||||
sizeResolver = sizeResolver,
|
sizeResolver = sizeResolver,
|
||||||
selectionController = checkNotNull(selectionController),
|
selectionController = checkNotNull(selectionController),
|
||||||
|
nestedScrollStateHandle = checkNotNull(nestedScrollStateHandle),
|
||||||
)
|
)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
@@ -82,10 +86,16 @@ class ShelfFragment :
|
|||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
nestedScrollStateHandle?.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
adapter = null
|
adapter = null
|
||||||
selectionController = null
|
selectionController = null
|
||||||
|
nestedScrollStateHandle = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
|
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
|
||||||
@@ -133,6 +143,8 @@ class ShelfFragment :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_NESTED_SCROLL = "nested_scroll"
|
||||||
|
|
||||||
fun newInstance() = ShelfFragment()
|
fun newInstance() = ShelfFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ class ScrollKeepObserver(
|
|||||||
get() = recyclerView.layoutManager as LinearLayoutManager
|
get() = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
|
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
val position = minOf(toPosition, fromPosition) // if items are swapping positions may be swapped too
|
val position = minOf(toPosition, fromPosition) // if items are swapping positions may be swapped too
|
||||||
if (position == 0 || position < layoutManager.findFirstVisibleItemPosition()) {
|
if (firstVisiblePosition != RecyclerView.NO_POSITION && (position == 0 || position < firstVisiblePosition)) {
|
||||||
postScroll(position)
|
postScroll(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
if (positionStart == 0 || positionStart < layoutManager.findFirstVisibleItemPosition()) {
|
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
if (firstVisiblePosition != RecyclerView.NO_POSITION && (positionStart == 0 || positionStart < firstVisiblePosition)) {
|
||||||
postScroll(positionStart)
|
postScroll(positionStart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
@@ -24,6 +25,7 @@ class ShelfAdapter(
|
|||||||
listener: ShelfListEventListener,
|
listener: ShelfListEventListener,
|
||||||
sizeResolver: ItemSizeResolver,
|
sizeResolver: ItemSizeResolver,
|
||||||
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||||
|
nestedScrollStateHandle: NestedScrollStateHandle,
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -37,6 +39,7 @@ class ShelfAdapter(
|
|||||||
sizeResolver = sizeResolver,
|
sizeResolver = sizeResolver,
|
||||||
selectionController = selectionController,
|
selectionController = selectionController,
|
||||||
listener = listener,
|
listener = listener,
|
||||||
|
nestedScrollStateHandle = nestedScrollStateHandle,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.addDelegate(loadingStateAD())
|
.addDelegate(loadingStateAD())
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import coil.ImageLoader
|
|||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
|
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ fun shelfGroupAD(
|
|||||||
sizeResolver: ItemSizeResolver,
|
sizeResolver: ItemSizeResolver,
|
||||||
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||||
listener: ShelfListEventListener,
|
listener: ShelfListEventListener,
|
||||||
|
nestedScrollStateHandle: NestedScrollStateHandle,
|
||||||
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
|
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
|
||||||
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
@@ -48,21 +50,25 @@ fun shelfGroupAD(
|
|||||||
MangaItemDiffCallback(),
|
MangaItemDiffCallback(),
|
||||||
mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver),
|
mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver),
|
||||||
)
|
)
|
||||||
|
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||||
adapter.registerAdapterDataObserver(ScrollKeepObserver(binding.recyclerView))
|
adapter.registerAdapterDataObserver(ScrollKeepObserver(binding.recyclerView))
|
||||||
binding.recyclerView.setRecycledViewPool(sharedPool)
|
binding.recyclerView.setRecycledViewPool(sharedPool)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||||
binding.buttonMore.setOnClickListener(listenerAdapter)
|
binding.buttonMore.setOnClickListener(listenerAdapter)
|
||||||
|
val stateController = nestedScrollStateHandle.attach(binding.recyclerView)
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
||||||
binding.textViewTitle.text = item.getTitle(context.resources)
|
binding.textViewTitle.text = item.getTitle(context.resources)
|
||||||
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
|
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
|
||||||
adapter.items = item.items
|
adapter.items = item.items
|
||||||
|
stateController.onBind(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewRecycled {
|
onViewRecycled {
|
||||||
|
stateController.onRecycled()
|
||||||
adapter.items = emptyList()
|
adapter.items = emptyList()
|
||||||
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
@@ -14,11 +16,11 @@ import java.io.Serializable
|
|||||||
// https://issuetracker.google.com/issues/240585930
|
// https://issuetracker.google.com/issues/240585930
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
|
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
|
||||||
return getParcelable(key) as T?
|
return BundleCompat.getParcelable(this, key, T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
|
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
|
||||||
return getParcelableExtra(key) as T?
|
return IntentCompat.getParcelableExtra(this, key, T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? {
|
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? {
|
||||||
|
|||||||
Reference in New Issue
Block a user