Fix webtoon scroll dispatching

(cherry picked from commit a0a72b1192)
This commit is contained in:
Koitharu
2024-01-24 11:26:29 +02:00
parent 850f6c2f3e
commit 5ec2eab6b8
5 changed files with 50 additions and 41 deletions

View File

@@ -262,7 +262,7 @@ class ReaderViewModel @Inject constructor(
} }
@MainThread @MainThread
fun onCurrentPageChanged(position: Int) { fun onCurrentPageChanged(lowerPos: Int, upperPos: Int) {
val prevJob = stateChangeJob val prevJob = stateChangeJob
val pages = content.value.pages // capture immediately val pages = content.value.pages // capture immediately
stateChangeJob = launchJob(Dispatchers.Default) { stateChangeJob = launchJob(Dispatchers.Default) {
@@ -271,7 +271,8 @@ class ReaderViewModel @Inject constructor(
if (pages.size != content.value.pages.size) { if (pages.size != content.value.pages.size) {
return@launchJob // TODO return@launchJob // TODO
} }
pages.getOrNull(position)?.let { page -> val centerPos = (lowerPos + upperPos) / 2
pages.getOrNull(centerPos)?.let { page ->
currentState.update { cs -> currentState.update { cs ->
cs?.copy(chapterId = page.chapterId, page = page.index) cs?.copy(chapterId = page.chapterId, page = page.index)
} }
@@ -281,14 +282,14 @@ class ReaderViewModel @Inject constructor(
return@launchJob return@launchJob
} }
ensureActive() ensureActive()
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) { if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.last().chapterId, isNext = true) loadPrevNextChapter(pages.last().chapterId, isNext = true)
} }
if (position <= BOUNDS_PAGE_OFFSET) { if (lowerPos <= BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.first().chapterId, isNext = false) loadPrevNextChapter(pages.first().chapterId, isNext = false)
} }
if (pageLoader.isPrefetchApplicable()) { if (pageLoader.isPrefetchApplicable()) {
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT)) pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
} }
} }
} }

View File

@@ -172,7 +172,8 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
} }
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(reversed(page)) val pos = reversed(page)
viewModel.onCurrentPageChanged(pos, pos)
} }
private fun reversed(position: Int): Int { private fun reversed(position: Int): Int {

View File

@@ -170,7 +170,7 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
} }
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page) viewModel.onCurrentPageChanged(page, page)
} }
companion object { companion object {

View File

@@ -24,7 +24,8 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>() { class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>(),
WebtoonRecyclerView.OnWebtoonScrollListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@@ -46,7 +47,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = readerAdapter adapter = readerAdapter
addOnPageScrollListener(PageScrollListener()) addOnPageScrollListener(this@WebtoonReaderFragment)
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also { recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
addOnScrollListener(it) addOnScrollListener(it)
} }
@@ -70,6 +71,15 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
exceptionResolver = exceptionResolver, exceptionResolver = exceptionResolver,
) )
override fun onScrollChanged(
recyclerView: WebtoonRecyclerView,
dy: Int,
firstVisiblePosition: Int,
lastVisiblePosition: Int,
) {
viewModel.onCurrentPageChanged(firstVisiblePosition, lastVisiblePosition)
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope { override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val setItems = launch { val setItems = launch {
requireAdapter().setItems(pages) requireAdapter().setItems(pages)
@@ -91,7 +101,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
?.restoreScroll(pendingState.scroll) ?.restoreScroll(pendingState.scroll)
} }
} }
notifyPageChanged(position) viewModel.onCurrentPageChanged(position, position)
} else { } else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show() .show()
@@ -121,10 +131,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
viewBinding?.frame?.onZoomOut() viewBinding?.frame?.onZoomOut()
} }
private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page)
}
override fun switchPageBy(delta: Int) { override fun switchPageBy(delta: Int) {
with(requireViewBinding().recyclerView) { with(requireViewBinding().recyclerView) {
if (isAnimationEnabled()) { if (isAnimationEnabled()) {
@@ -147,12 +153,4 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
} }
return true return true
} }
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {
super.onPageChanged(recyclerView, index)
notifyPageChanged(index)
}
}
} }

View File

@@ -6,8 +6,8 @@ import android.view.View
import androidx.core.view.ViewCompat.TYPE_TOUCH import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import java.util.LinkedList import java.util.LinkedList
import java.util.WeakHashMap import java.util.WeakHashMap
@@ -15,7 +15,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) { ) : RecyclerView(context, attrs, defStyleAttr) {
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>()
private val scrollDispatcher = WebtoonScrollDispatcher()
private val detachedViews = WeakHashMap<View, Unit>() private val detachedViews = WeakHashMap<View, Unit>()
private var isFixingScroll: Boolean = false private var isFixingScroll: Boolean = false
@@ -103,22 +104,20 @@ class WebtoonRecyclerView @JvmOverloads constructor(
return 0 return 0
} }
fun addOnPageScrollListener(listener: OnPageScrollListener) { fun addOnPageScrollListener(listener: OnWebtoonScrollListener) {
val list = onPageScrollListeners ?: LinkedList<OnPageScrollListener>().also { onPageScrollListeners = it } onPageScrollListeners.add(listener)
list.add(listener)
} }
fun removeOnPageScrollListener(listener: OnPageScrollListener) { fun removeOnPageScrollListener(listener: OnWebtoonScrollListener) {
onPageScrollListeners?.remove(listener) onPageScrollListeners.remove(listener)
} }
private fun notifyScrollChanged(dy: Int) { private fun notifyScrollChanged(dy: Int) {
val listeners = onPageScrollListeners val listeners = onPageScrollListeners
if (listeners.isNullOrEmpty()) { if (listeners.isEmpty()) {
return return
} }
val centerPosition = findCenterViewPosition() scrollDispatcher.dispatchScroll(this, dy)
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
} }
fun relayoutChildren() { fun relayoutChildren() {
@@ -162,20 +161,30 @@ class WebtoonRecyclerView @JvmOverloads constructor(
else -> false else -> false
} }
abstract class OnPageScrollListener { private class WebtoonScrollDispatcher {
private var lastPosition = NO_POSITION private var firstPos = NO_POSITION
private var lastPos = NO_POSITION
fun dispatchScroll(recyclerView: WebtoonRecyclerView, dy: Int, centerPosition: Int) { fun dispatchScroll(rv: WebtoonRecyclerView, dy: Int) {
onScroll(recyclerView, dy) val lm = rv.layoutManager as? LinearLayoutManager
if (centerPosition != NO_POSITION && centerPosition != lastPosition) { if (lm == null) {
lastPosition = centerPosition firstPos = NO_POSITION
onPageChanged(recyclerView, centerPosition) lastPos = NO_POSITION
return
}
val newFirstPos = lm.findFirstVisibleItemPosition()
val newLastPos = lm.findLastVisibleItemPosition()
if (newFirstPos != firstPos || newLastPos != lastPos) {
firstPos = newFirstPos
lastPos = newLastPos
rv.onPageScrollListeners.forEach { it.onScrollChanged(rv, dy, newFirstPos, newLastPos) }
} }
} }
}
open fun onScroll(recyclerView: WebtoonRecyclerView, dy: Int) = Unit interface OnWebtoonScrollListener {
open fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) = Unit fun onScrollChanged(recyclerView: WebtoonRecyclerView, dy: Int, firstVisiblePosition: Int, lastVisiblePosition: Int)
} }
} }