Fix pages bottom sheet scroll

This commit is contained in:
Koitharu
2023-06-04 15:42:15 +03:00
parent da4aedca97
commit b48c6d7d38
7 changed files with 84 additions and 44 deletions

View File

@@ -3,8 +3,10 @@ package org.koitharu.kotatsu.core.ui.list
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) :
RecyclerView.OnScrollListener() {
abstract class BoundsScrollListener(
@JvmField protected val offsetTop: Int,
@JvmField protected val offsetBottom: Int
) : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@@ -24,9 +26,16 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
if (firstVisibleItemPosition <= offsetTop) {
onScrolledToStart(recyclerView)
}
onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
}
abstract fun onScrolledToStart(recyclerView: RecyclerView)
abstract fun onScrolledToEnd(recyclerView: RecyclerView)
protected open fun onPostScrolled(
recyclerView: RecyclerView,
firstVisibleItemPosition: Int,
visibleItemCount: Int
) = Unit
}

View File

@@ -1,30 +0,0 @@
package org.koitharu.kotatsu.core.ui.list
import androidx.recyclerview.widget.RecyclerView
class ScrollListenerInvalidationObserver(
private val recyclerView: RecyclerView,
private val scrollListener: RecyclerView.OnScrollListener,
) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
invalidateScroll()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
invalidateScroll()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
invalidateScroll()
}
private fun invalidateScroll() {
recyclerView.post {
scrollListener.onScrolled(recyclerView, 0, 0)
}
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.util
class CompositeRunnable(
private val children: List<Runnable>,
) : Runnable, Collection<Runnable> by children {
override fun run() {
for (child in children) {
child.run()
}
}
}

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.core.util.ext
import org.koitharu.kotatsu.core.util.CompositeRunnable
@Suppress("UNCHECKED_CAST")
fun <T> Class<T>.castOrNull(obj: Any?): T? {
if (obj == null || !isInstance(obj)) {
@@ -7,3 +9,15 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
}
return obj as T
}
/* CompositeRunnable */
operator fun Runnable.plus(other: Runnable): Runnable {
val list = ArrayList<Runnable>(this.size + other.size)
if (this is CompositeRunnable) list.addAll(this) else list.add(this)
if (other is CompositeRunnable) list.addAll(other) else list.add(other)
return CompositeRunnable(list)
}
private val Runnable.size: Int
get() = if (this is CompositeRunnable) size else 1

View File

@@ -16,23 +16,25 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.ScrollListenerInvalidationObserver
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.plus
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.TargetScrollObserver
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class PagesThumbnailsSheet :
@@ -79,14 +81,8 @@ class PagesThumbnailsSheet :
spanResolver?.setGridSize(settings.gridSize / 100f, this)
addOnScrollListener(ScrollListener().also { scrollListener = it })
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
thumbnailsAdapter?.registerAdapterDataObserver(
ScrollListenerInvalidationObserver(this, checkNotNull(scrollListener)),
)
thumbnailsAdapter?.registerAdapterDataObserver(TargetScrollObserver(this))
}
viewModel.thumbnails.observe(viewLifecycleOwner) {
thumbnailsAdapter?.setItems(it, listCommitCallback)
}
viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
viewModel.branch.observe(viewLifecycleOwner, ::updateTitle)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
}
@@ -124,6 +120,28 @@ class PagesThumbnailsSheet :
}
}
private fun onThumbnailsChanged(list: List<ListModel>) {
val adapter = thumbnailsAdapter ?: return
if (adapter.itemCount == 0) {
var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent }
if (position > 0) {
val spanCount = spanResolver?.spanCount ?: 0
val offset = if (position > spanCount + 1) {
(resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt()
} else {
position = 0
0
}
val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset)
adapter.setItems(list, listCommitCallback + scrollCallback)
} else {
adapter.setItems(list, listCommitCallback)
}
} else {
adapter.setItems(list, listCommitCallback)
}
}
private inner class ScrollListener : BoundsScrollListener(3, 3) {
override fun onScrolledToStart(recyclerView: RecyclerView) {
@@ -133,6 +151,13 @@ class PagesThumbnailsSheet :
override fun onScrolledToEnd(recyclerView: RecyclerView) {
viewModel.loadNextChapter()
}
override fun onPostScrolled(recyclerView: RecyclerView, firstVisibleItemPosition: Int, visibleItemCount: Int) {
super.onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
if (firstVisibleItemPosition > offsetTop) {
viewModel.allowLoadAbove()
}
}
}
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {

View File

@@ -39,6 +39,7 @@ class PagesThumbnailsViewModel @Inject constructor(
private var loadingJob: Job? = null
private var loadingPrevJob: Job? = null
private var loadingNextJob: Job? = null
private var isLoadAboveAllowed = false
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
val branch = MutableStateFlow<String?>(null)
@@ -51,8 +52,17 @@ class PagesThumbnailsViewModel @Inject constructor(
}
}
fun allowLoadAbove() {
if (!isLoadAboveAllowed) {
loadingJob = launchJob(Dispatchers.Default) {
isLoadAboveAllowed = true
updateList()
}
}
}
fun loadPrevChapter() {
if (loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
if (!isLoadAboveAllowed || loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
return
}
loadingPrevJob = loadPrevNextChapter(isNext = false)
@@ -74,7 +84,7 @@ class PagesThumbnailsViewModel @Inject constructor(
private suspend fun updateList() {
val snapshot = chaptersLoader.snapshot()
val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty()
val hasPrevChapter = snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id
val hasPrevChapter = isLoadAboveAllowed && snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id
val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
if (hasPrevChapter) {

View File

@@ -19,7 +19,7 @@
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="@dimen/grid_spacing"
android:scrollIndicators="top"