Просмотр исходного кода

feat: 漫画阅读器相关优化与修复

HapeLee 2 месяцев назад
Родитель
Сommit
dd68689ef0

+ 12 - 2
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderInteraction.kt

@@ -67,7 +67,9 @@ internal fun nextPageItemIndex(
  * last one promotes the session directly to that chapter's final visible page. Once the current
  * chapter has left the viewport, use the first visible page when entering a later chapter and
  * the last visible page when entering an earlier one. Those are the pages adjacent to the
- * boundary in reading order.
+ * boundary in reading order. If that boundary's transition card is still visible, defer the
+ * promotion: adjacent content may only just have been appended after loading, without a user
+ * scroll past the card.
  */
 internal fun mangaWebtoonFocusedPageIndex(
     items: List<MangaReaderItemUi>,
@@ -81,11 +83,19 @@ internal fun mangaWebtoonFocusedPageIndex(
     if (visiblePages.any { (_, page) -> page.chapterIndex == currentChapterIndex }) {
         return visiblePages.last().first
     }
-    return when {
+    val focusedPage = when {
         visiblePages.first().second.chapterIndex > currentChapterIndex -> visiblePages.first().first
         visiblePages.last().second.chapterIndex < currentChapterIndex -> visiblePages.last().first
         else -> visiblePages.last().first
     }
+    val focusedChapterIndex = (items[focusedPage] as MangaReaderItemUi.Page).chapterIndex
+    // 相邻章节从 Loading 变 Ready 时,新的首/末页会被追加到仍停在过渡卡片上的视口。
+    // 这不是用户继续翻过章节边界,不能因此直接切章;等过渡卡片离开视口后再上报。
+    val transitionStillVisible = visibleItemIndices.any { index ->
+        (items.getOrNull(index) as? MangaReaderItemUi.ChapterTransition)
+            ?.targetChapterIndex == focusedChapterIndex
+    }
+    return focusedPage.takeUnless { transitionStillVisible }
 }
 
 /** A programmatic position restore must not be overwritten by the old viewport's first callback. */

+ 13 - 7
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderScreen.kt

@@ -377,6 +377,10 @@ private fun WebtoonMangaList(
     imageLoader: ImageLoader,
 ) {
     val listState = rememberLazyListState(initialFirstVisibleItemIndex = state.currentItemIndex)
+    // 单张图片的加载状态会更新整个 UiState.pages。可见页监听不能因此重启:重启会
+    // 丢失 distinctUntilChanged 的上一条记录,并把 LazyColumn 尚未完成重测时保留的
+    // 旧视口重新上报为当前页,进而使章节/页码跳回去。
+    val latestReaderState by rememberUpdatedState(state)
     val coroutineScope = rememberCoroutineScope()
     var pendingWebtoonTap by remember { mutableStateOf<Job?>(null) }
     var lastWebtoonTapAt by remember { mutableStateOf(0L) }
@@ -469,19 +473,20 @@ private fun WebtoonMangaList(
             }
         }
 
-    LaunchedEffect(listState, state.pages, state.navigationId) {
+    LaunchedEffect(listState, state.navigationId) {
         snapshotFlow {
+            val currentState = latestReaderState
             val visibleItems = listState.layoutInfo.visibleItemsInfo
             val visibleItemIndices = visibleItems.map { it.index }
             val currentChapterVisible = visibleItems.any { visibleItem ->
-                (state.pages.getOrNull(visibleItem.index) as? MangaReaderItemUi.Page)
-                    ?.chapterIndex == state.chapterIndex
+                (currentState.pages.getOrNull(visibleItem.index) as? MangaReaderItemUi.Page)
+                    ?.chapterIndex == currentState.chapterIndex
             }
             val firstItemIndex = visibleItems.firstOrNull()?.index
             val focusedItemIndex = mangaWebtoonFocusedPageIndex(
-                items = state.pages,
+                items = currentState.pages,
                 visibleItemIndices = visibleItemIndices,
-                currentChapterIndex = state.chapterIndex,
+                currentChapterIndex = currentState.chapterIndex,
             )
             if (firstItemIndex == null || focusedItemIndex == null) null
             else Triple(focusedItemIndex, firstItemIndex, currentChapterVisible)
@@ -489,13 +494,14 @@ private fun WebtoonMangaList(
             .distinctUntilChanged()
             .collect { entry ->
                 entry?.let { (focusedIndex, firstIndex, currentChapterVisible) ->
-                    when (val item = state.pages.getOrNull(focusedIndex)) {
+                    val currentState = latestReaderState
+                    when (val item = currentState.pages.getOrNull(focusedIndex)) {
                         is MangaReaderItemUi.Page -> onIntent(MangaReaderIntent.VisibleItemChanged(
                             itemIndex = focusedIndex,
                             firstItemIndex = firstIndex,
                             lastItemIndex = focusedIndex,
                             currentChapterVisible = currentChapterVisible,
-                            navigationId = state.navigationId,
+                            navigationId = currentState.navigationId,
                         ))
                         is MangaReaderItemUi.ChapterTransition -> Unit
                         is MangaReaderItemUi.ChapterEdge, null -> Unit

+ 35 - 2
app/src/test/java/io/legado/app/ui/book/manga/MangaReaderInteractionTest.kt

@@ -133,7 +133,7 @@ class MangaReaderInteractionTest {
     }
 
     @Test
-    fun `webtoon enters later chapter at its first visible page not its last`() {
+    fun `webtoon enters later chapter at its first visible page after transition leaves viewport`() {
         val items = listOf(
             page(8, chapter = 20),
             MangaReaderItemUi.ChapterTransition(
@@ -153,7 +153,7 @@ class MangaReaderInteractionTest {
             2,
             mangaWebtoonFocusedPageIndex(
                 items = items,
-                visibleItemIndices = listOf(1, 2, 3, 4),
+                visibleItemIndices = listOf(2, 3, 4),
                 currentChapterIndex = 20,
             ),
         )
@@ -185,6 +185,39 @@ class MangaReaderInteractionTest {
         )
     }
 
+    @Test
+    fun `webtoon waits for transition card to leave viewport before promoting loaded chapter`() {
+        val items = listOf(
+            page(8, chapter = 20),
+            MangaReaderItemUi.ChapterTransition(
+                key = "transition",
+                direction = MangaChapterTransitionDirection.NEXT,
+                targetChapterIndex = 21,
+                currentChapterName = "20",
+                targetChapterName = "21",
+                targetStatus = MangaChapterTransitionStatus.READY,
+            ),
+            page(0, chapter = 21),
+        )
+
+        // 下一章刚插入列表,过渡卡片还在可视区域:不能因布局更新自动切章。
+        assertNull(
+            mangaWebtoonFocusedPageIndex(
+                items = items,
+                visibleItemIndices = listOf(1, 2),
+                currentChapterIndex = 20,
+            ),
+        )
+        assertEquals(
+            2,
+            mangaWebtoonFocusedPageIndex(
+                items = items,
+                visibleItemIndices = listOf(2),
+                currentChapterIndex = 20,
+            ),
+        )
+    }
+
     @Test
     fun `pending restored position rejects old viewport callback`() {
         assertFalse(acceptsMangaVisibleItem(requestedItemIndex = 14, reportedItemIndex = 0))