Handle nested scroll state in shelf

This commit is contained in:
Koitharu
2023-04-14 18:22:40 +03:00
parent 277d575485
commit c4ba311087
6 changed files with 94 additions and 5 deletions

View File

@@ -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)
}
}
}
}

View File

@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
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.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
@@ -47,6 +48,7 @@ class ShelfFragment :
@Inject
lateinit var settings: AppSettings
private var nestedScrollStateHandle: NestedScrollStateHandle? = null
private val viewModel by viewModels<ShelfViewModel>()
private var adapter: ShelfAdapter? = null
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
@@ -60,6 +62,7 @@ class ShelfFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nestedScrollStateHandle = NestedScrollStateHandle(savedInstanceState, KEY_NESTED_SCROLL)
val sizeResolver = ItemSizeResolver(resources, settings)
selectionController = SectionedSelectionController(
activity = requireActivity(),
@@ -72,6 +75,7 @@ class ShelfFragment :
listener = this,
sizeResolver = sizeResolver,
selectionController = checkNotNull(selectionController),
nestedScrollStateHandle = checkNotNull(nestedScrollStateHandle),
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
@@ -82,10 +86,16 @@ class ShelfFragment :
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
nestedScrollStateHandle?.onSaveInstanceState(outState)
}
override fun onDestroyView() {
super.onDestroyView()
adapter = null
selectionController = null
nestedScrollStateHandle = null
}
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
@@ -133,6 +143,8 @@ class ShelfFragment :
companion object {
private const val KEY_NESTED_SCROLL = "nested_scroll"
fun newInstance() = ShelfFragment()
}
}

View File

@@ -11,14 +11,16 @@ class ScrollKeepObserver(
get() = recyclerView.layoutManager as LinearLayoutManager
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
if (position == 0 || position < layoutManager.findFirstVisibleItemPosition()) {
if (firstVisiblePosition != RecyclerView.NO_POSITION && (position == 0 || position < firstVisiblePosition)) {
postScroll(position)
}
}
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)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
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.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
@@ -24,6 +25,7 @@ class ShelfAdapter(
listener: ShelfListEventListener,
sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<ShelfSectionModel>,
nestedScrollStateHandle: NestedScrollStateHandle,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
init {
@@ -37,6 +39,7 @@ class ShelfAdapter(
sizeResolver = sizeResolver,
selectionController = selectionController,
listener = listener,
nestedScrollStateHandle = nestedScrollStateHandle,
),
)
.addDelegate(loadingStateAD())

View File

@@ -7,16 +7,17 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
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.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
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.setTextAndVisible
@@ -27,6 +28,7 @@ fun shelfGroupAD(
sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<ShelfSectionModel>,
listener: ShelfListEventListener,
nestedScrollStateHandle: NestedScrollStateHandle,
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
) {
@@ -48,21 +50,25 @@ fun shelfGroupAD(
MangaItemDiffCallback(),
mangaGridItemAD(coil, lifecycleOwner, listenerAdapter, sizeResolver),
)
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
adapter.registerAdapterDataObserver(ScrollKeepObserver(binding.recyclerView))
binding.recyclerView.setRecycledViewPool(sharedPool)
binding.recyclerView.adapter = adapter
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
binding.buttonMore.setOnClickListener(listenerAdapter)
val stateController = nestedScrollStateHandle.attach(binding.recyclerView)
bind {
selectionController.attachToRecyclerView(item, binding.recyclerView)
binding.textViewTitle.text = item.getTitle(context.resources)
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
adapter.items = item.items
stateController.onBind(bindingAdapterPosition)
}
onViewRecycled {
stateController.onRecycled()
adapter.items = emptyList()
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
}

View File

@@ -7,6 +7,8 @@ import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import androidx.core.content.IntentCompat
import androidx.core.os.BundleCompat
import androidx.lifecycle.SavedStateHandle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import java.io.Serializable
@@ -14,11 +16,11 @@ import java.io.Serializable
// https://issuetracker.google.com/issues/240585930
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? {
return getParcelableExtra(key) as T?
return IntentCompat.getParcelableExtra(this, key, T::class.java)
}
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? {