Fix pages bottom sheet scroll
This commit is contained in:
@@ -3,8 +3,10 @@ package org.koitharu.kotatsu.core.ui.list
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) :
|
abstract class BoundsScrollListener(
|
||||||
RecyclerView.OnScrollListener() {
|
@JvmField protected val offsetTop: Int,
|
||||||
|
@JvmField protected val offsetBottom: Int
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
@@ -24,9 +26,16 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
|
|||||||
if (firstVisibleItemPosition <= offsetTop) {
|
if (firstVisibleItemPosition <= offsetTop) {
|
||||||
onScrolledToStart(recyclerView)
|
onScrolledToStart(recyclerView)
|
||||||
}
|
}
|
||||||
|
onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
||||||
|
|
||||||
abstract fun onScrolledToEnd(recyclerView: RecyclerView)
|
abstract fun onScrolledToEnd(recyclerView: RecyclerView)
|
||||||
|
|
||||||
|
protected open fun onPostScrolled(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
firstVisibleItemPosition: Int,
|
||||||
|
visibleItemCount: Int
|
||||||
|
) = Unit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||||
if (obj == null || !isInstance(obj)) {
|
if (obj == null || !isInstance(obj)) {
|
||||||
@@ -7,3 +9,15 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
|||||||
}
|
}
|
||||||
return obj as 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
|
||||||
|
|||||||
@@ -16,23 +16,25 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
|
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
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.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
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.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
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.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.TargetScrollObserver
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PagesThumbnailsSheet :
|
class PagesThumbnailsSheet :
|
||||||
@@ -79,14 +81,8 @@ class PagesThumbnailsSheet :
|
|||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
||||||
addOnScrollListener(ScrollListener().also { scrollListener = it })
|
addOnScrollListener(ScrollListener().also { scrollListener = it })
|
||||||
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
(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.branch.observe(viewLifecycleOwner, ::updateTitle)
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
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) {
|
private inner class ScrollListener : BoundsScrollListener(3, 3) {
|
||||||
|
|
||||||
override fun onScrolledToStart(recyclerView: RecyclerView) {
|
override fun onScrolledToStart(recyclerView: RecyclerView) {
|
||||||
@@ -133,6 +151,13 @@ class PagesThumbnailsSheet :
|
|||||||
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||||
viewModel.loadNextChapter()
|
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() {
|
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private var loadingPrevJob: Job? = null
|
private var loadingPrevJob: Job? = null
|
||||||
private var loadingNextJob: Job? = null
|
private var loadingNextJob: Job? = null
|
||||||
|
private var isLoadAboveAllowed = false
|
||||||
|
|
||||||
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
|
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
|
||||||
val branch = MutableStateFlow<String?>(null)
|
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() {
|
fun loadPrevChapter() {
|
||||||
if (loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
|
if (!isLoadAboveAllowed || loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadingPrevJob = loadPrevNextChapter(isNext = false)
|
loadingPrevJob = loadPrevNextChapter(isNext = false)
|
||||||
@@ -74,7 +84,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
private suspend fun updateList() {
|
private suspend fun updateList() {
|
||||||
val snapshot = chaptersLoader.snapshot()
|
val snapshot = chaptersLoader.snapshot()
|
||||||
val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty()
|
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 hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id
|
||||||
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
|
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
|
||||||
if (hasPrevChapter) {
|
if (hasPrevChapter) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:padding="@dimen/grid_spacing"
|
android:padding="@dimen/grid_spacing"
|
||||||
android:scrollIndicators="top"
|
android:scrollIndicators="top"
|
||||||
|
|||||||
Reference in New Issue
Block a user