HapeLee 1 месяц назад
Родитель
Сommit
8a37f41a68

+ 5 - 2
app/src/main/java/io/legado/app/core/ui/morph/BookMorphHost.kt

@@ -128,7 +128,7 @@ fun BookMorphHost(
                     BookCoverMorphAnchors.setActiveMorph(anchorKey, morph)
                 }
                 BookCoverMorphAnchors.get(anchorKey)?.let(morph::updateAnchor)
-                morph.animateTo(0f)
+                morph.animateTo(0f, initialVelocity = morph.consumeCollapseVelocity())
                 if (!currentDismiss()) morph.animateTo(1f)
             }
         }
@@ -177,6 +177,9 @@ fun BookMorphHost(
             }
             if (!predictiveBackEnabled) morph.progress.stop()
             val requestBack = currentBackRequested
+            // 手势速度先寄存:业务授权后的收起由 collapse 取走,未授权(弹确认框)则由过期的
+            // 进度判断丢弃,不会把旧动量带到下一次收起。
+            morph.recordCollapseVelocity(releaseVelocity)
             if (requestBack != null) {
                 // Reader business logic authorizes the exit through Finish. It then calls
                 // collapse; the animation completion must never issue another close request.
@@ -281,7 +284,7 @@ fun BookMorphHost(
 
             // 3. 飞行封面:
             // 若页面有封面终点(如详情页),封面从起点连续飞向页面封面位置,全程保持连续;
-            // 若页面无封面终点(如小说/漫画阅读),封面随卡片展开并在前期(0.10f..0.25f)平滑淡出。
+            // 若页面无封面终点(如小说/漫画阅读),封面随卡片展开并在中段(0.10f..0.50f)平滑淡出。
             val coverAlpha by remember { derivedStateOf { morph.coverAlpha } }
             val flyingCoverVisible by remember {
                 derivedStateOf { !morph.expanded && !morph.fadeOnlyOpening && coverAlpha > 0.001f }

+ 44 - 7
app/src/main/java/io/legado/app/core/ui/morph/BookMorphState.kt

@@ -215,6 +215,31 @@ class BookMorphState(
         )
     }
 
+    /**
+     * 等待授权关闭继承的手势结算速度(进度/秒)。
+     *
+     * 返回手势结束后业务逻辑(是否真的能关)要绕一圈才知道结果,宿主先把速度寄存在这里,
+     * 授权后的收起再取走,避免收起丢掉手指的动量。
+     */
+    var pendingCollapseVelocity by mutableFloatStateOf(0f)
+        private set
+
+    fun recordCollapseVelocity(velocity: Float) {
+        pendingCollapseVelocity = velocity
+    }
+
+    /**
+     * 取出并清空待继承的手势速度,只生效一次。
+     *
+     * 只有当形变仍在收起途中(progress < 1)才继承:进度若已回到 1,
+     * 说明这次手势被业务回退过(例如弹出了「加入书架」确认框),寄存的速度已经过期。
+     */
+    fun consumeCollapseVelocity(): Float {
+        val velocity = pendingCollapseVelocity
+        pendingCollapseVelocity = 0f
+        return if (progress.value < 1f) velocity else 0f
+    }
+
     suspend fun animateTo(target: Float, initialVelocity: Float = 0f) {
         isCollapsing = target < 0.5f
         progress.animateTo(
@@ -229,20 +254,32 @@ class BookMorphState(
     }
 }
 
+/** 飞行封面开始淡出 / 开始渐显的进度点。 */
+const val BOOK_COVER_FADE_START = 0.10f
+
+/** 飞行封面完全交给内容 / 完全回到书架卡片的进度点。 */
+const val BOOK_COVER_FADE_END = 0.50f
+
 /**
- * 小说/漫画封面过渡透明度:在动画前期(0.10f..0.25f)完成封面淡出,保持卡片精致尺寸,避免展开中途封面过大。
+ * 小说/漫画封面过渡透明度:在几何飞行的中段([BOOK_COVER_FADE_START]..[BOOK_COVER_FADE_END])完成封面淡出,
+ * 避免封面在展开中途与内容双重叠加。
+ *
+ * 打开时(isCollapsing = false):封面随卡片展开,在 0.10f..0.50f 平滑淡出交接给内容。
+ * 收起时(isCollapsing = true):内容在中后期(0.65f..0.20f)渐变消失,在中段(0.50f..0.10f)封面平滑渐显,完美回到书架卡片。
  *
- * 打开时(isCollapsing = false):封面随卡片展开,在 0.10f..0.25f 平滑淡出交接给内容。
- * 收起时(isCollapsing = true):内容在中后期(0.65f..0.20f)渐变消失,在末段(0.25f..0.10f)封面平滑渐显,完美回到书架卡片。
+ * 窗口换算到 [BookMorphState.animateTo] 的临界阻尼弹簧(stiffness = 260)约对应 33 ms → 104 ms;
+ * 预测性返回期间进度由手指驱动,这段渐变随之被手势距离拉长。
  */
 fun computeBookCoverAlpha(progress: Float, isCollapsing: Boolean): Float {
     val t = progress.coerceIn(0f, 1f)
     return if (isCollapsing) {
-        if (t >= 0.25f) 0f
-        else ((0.25f - t) / 0.15f).coerceIn(0f, 1f)
+        if (t >= BOOK_COVER_FADE_END) 0f
+        else ((BOOK_COVER_FADE_END - t) / (BOOK_COVER_FADE_END - BOOK_COVER_FADE_START))
+            .coerceIn(0f, 1f)
     } else {
-        if (t <= 0.10f) 1f
-        else (1f - (t - 0.10f) / 0.15f).coerceIn(0f, 1f)
+        if (t <= BOOK_COVER_FADE_START) 1f
+        else (1f - (t - BOOK_COVER_FADE_START) / (BOOK_COVER_FADE_END - BOOK_COVER_FADE_START))
+            .coerceIn(0f, 1f)
     }
 }
 

+ 4 - 0
app/src/test/java/io/legado/app/core/ui/morph/BookMorphHostInteractionTest.kt

@@ -115,11 +115,15 @@ class BookMorphHostInteractionTest {
             frames()
             assertEquals(1, requests)
             assertEquals("A back request must wait for Finish", 3, attempts)
+            // 手势寄存的结算速度必须由授权后的收起取走,且只生效一次:
+            // 收起(animateTo(0f))跑完后寄存位归零,不会带到下一次收起。
+            requireNotNull(morph).recordCollapseVelocity(-4f)
             requireNotNull(collapse).invoke()
             frames()
             assertEquals(4, attempts)
             assertEquals("Committing Finish must not request close again", 1, requests)
             assertEquals(0f, requireNotNull(morph).progress.value)
+            assertEquals(0f, requireNotNull(morph).pendingCollapseVelocity, 0.001f)
         } finally {
             controller.pause().stop().destroy()
         }

+ 26 - 4
app/src/test/java/io/legado/app/core/ui/morph/BookMorphTest.kt

@@ -154,21 +154,43 @@ class BookMorphTest {
 
     @Test
     fun bookCoverAlphaTransitionsEarlyInAnimation() {
-        // 打开时:在动画前期(0.10 -> 0.25)平滑淡出,避免封面在展开中途过大
+        // 打开时:在几何飞行中段(0.10 -> 0.50)平滑淡出,避免封面在展开中途与内容双重叠加
         assertEquals(1f, computeBookCoverAlpha(0f, isCollapsing = false), 0.001f)
         assertEquals(1f, computeBookCoverAlpha(0.10f, isCollapsing = false), 0.001f)
-        assertEquals(0f, computeBookCoverAlpha(0.25f, isCollapsing = false), 0.001f)
+        assertEquals(0.5f, computeBookCoverAlpha(0.30f, isCollapsing = false), 0.001f)
         assertEquals(0f, computeBookCoverAlpha(0.50f, isCollapsing = false), 0.001f)
         assertEquals(0f, computeBookCoverAlpha(1f, isCollapsing = false), 0.001f)
 
-        // 收起时:在动画末段(0.25 -> 0.10)渐显回到书架卡片
+        // 收起时:在动画中段(0.50 -> 0.10)渐显回到书架卡片
         assertEquals(0f, computeBookCoverAlpha(1f, isCollapsing = true), 0.001f)
         assertEquals(0f, computeBookCoverAlpha(0.50f, isCollapsing = true), 0.001f)
-        assertEquals(0f, computeBookCoverAlpha(0.25f, isCollapsing = true), 0.001f)
+        assertEquals(0.5f, computeBookCoverAlpha(0.30f, isCollapsing = true), 0.001f)
         assertEquals(1f, computeBookCoverAlpha(0.10f, isCollapsing = true), 0.001f)
         assertEquals(1f, computeBookCoverAlpha(0f, isCollapsing = true), 0.001f)
     }
 
+    @Test
+    fun pendingCollapseVelocityIsInheritedOnceWhileCollapsing() = runTest {
+        val state = BookMorphState(Animatable(1f), Density(1f))
+
+        state.progress.snapTo(0.73f)
+        state.recordCollapseVelocity(-2.5f)
+        assertEquals(-2.5f, state.consumeCollapseVelocity(), 0.001f)
+        // 只生效一次:重复收起不会再吃到上一次手势的动量
+        assertEquals(0f, state.consumeCollapseVelocity(), 0.001f)
+    }
+
+    @Test
+    fun pendingCollapseVelocityExpiresWhenGestureWasRolledBack() = runTest {
+        val state = BookMorphState(Animatable(1f), Density(1f))
+
+        // 手势结束但业务未授权关闭(例:弹出加入书架确认框),进度被恢复回 1
+        state.progress.snapTo(0.73f)
+        state.recordCollapseVelocity(-2.5f)
+        state.progress.snapTo(1f)
+        assertEquals(0f, state.consumeCollapseVelocity(), 0.001f)
+    }
+
     @Test
     fun bookBadgeAlphaTransitionsGradually() {
         // 打开时:在极前期(0.0 -> 0.15)平滑淡出,交接给阅读/详情页