diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt new file mode 100644 index 000000000..80d5310d3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt @@ -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 = savedInstanceState?.let { + BundleCompat.getSparseParcelableArray(it, key, Parcelable::class.java) + } ?: SparseArray() + private val controllers = Collections.newSetFromMap(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) + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index a275957ac..deeaf2c5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -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() private var adapter: ShelfAdapter? = null private var selectionController: SectionedSelectionController? = 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() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt index 4312943e7..322210205 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt @@ -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) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt index 0c3347cf1..aab1daea3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt @@ -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, + nestedScrollStateHandle: NestedScrollStateHandle, ) : AsyncListDifferDelegationAdapter(DiffCallback()), FastScroller.SectionIndexer { init { @@ -37,6 +39,7 @@ class ShelfAdapter( sizeResolver = sizeResolver, selectionController = selectionController, listener = listener, + nestedScrollStateHandle = nestedScrollStateHandle, ), ) .addDelegate(loadingStateAD()) diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt index a85e6de29..bf2c3e0fe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt @@ -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, listener: ShelfListEventListener, + nestedScrollStateHandle: NestedScrollStateHandle, ) = adapterDelegateViewBinding( { 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt index fb7a0bb02..1dcead8e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt @@ -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 Bundle.getParcelableCompat(key: String): T? { - return getParcelable(key) as T? + return BundleCompat.getParcelable(this, key, T::class.java) } inline fun Intent.getParcelableExtraCompat(key: String): T? { - return getParcelableExtra(key) as T? + return IntentCompat.getParcelableExtra(this, key, T::class.java) } inline fun Intent.getSerializableExtraCompat(key: String): T? {