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

fix: 统一听书播放器语速调节范围与经典朗读控制一致 (#2033)

* fix: 统一听书播放器语速调节范围与经典朗读控制一致 (Closes #2032)

* test: 抽取语速范围共享常量并补充单元测试防回归

- 播放器语速范围/显示格式化抽为文件级 internal 纯逻辑(READ_ALOUD_SPEED_MIN/MAX, coerceReadAloudSpeed, formatReadAloudSpeedLabel)
- Screen 引用共享常量,消除硬编码,与经典朗读控制范围一致
- 新增 ReadAloudPlayerSpeedRangeTest 8 个用例:范围一致性、steps、边界钳制、显示格式
kungunier 2 месяцев назад
Родитель
Сommit
ae054f5941

+ 19 - 1
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerCoordinator.kt

@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlin.math.roundToInt
 
 /** Compatibility boundary between the Compose player and the legacy reader/service state. */
 class ReadAloudPlayerCoordinator(
@@ -164,7 +165,7 @@ class ReadAloudPlayerCoordinator(
     fun selectChapter(index: Int) = ReadBook.openChapter(index, durChapterPos = 0)
 
     suspend fun setSpeed(value: Int) {
-        readAloudSettingsGateway.update { it.copy(ttsSpeechRate = value.coerceIn(0, 80)) }
+        readAloudSettingsGateway.update { it.copy(ttsSpeechRate = coerceReadAloudSpeed(value)) }
         ReadAloud.upTtsSpeechRate(application)
     }
 
@@ -237,3 +238,20 @@ data class ReadAloudPlayerSourceState(
     val timerMinutes: Int,
     val finishCurrentChapterAfterTimer: Boolean,
 )
+
+/** 语速调节范围,与经典朗读控制(ReadAloudScreen 的 valueRange = 0f..80f)保持一致。 */
+internal const val READ_ALOUD_SPEED_MIN = 0
+internal const val READ_ALOUD_SPEED_MAX = 80
+
+internal fun coerceReadAloudSpeed(value: Int): Int =
+    value.coerceIn(READ_ALOUD_SPEED_MIN, READ_ALOUD_SPEED_MAX)
+
+/** 语速显示文本,如 20 -> "2.0"、15 -> "1.5",与播放器 valueLabel 格式化一致。 */
+internal fun formatReadAloudSpeedLabel(speed: Int): String {
+    val display = speed / 10f
+    return if (display == display.roundToInt().toFloat()) {
+        "${display.roundToInt()}.0"
+    } else {
+        String.format("%.1f", display)
+    }
+}

+ 7 - 10
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerScreen.kt

@@ -403,22 +403,19 @@ fun ReadAloudPlayerScreenContent(
                     AnimatedVisibility(activeAdjustment == PlayerAdjustment.Speed) {
                         PlayerAdjustmentSlider(
                             title = stringResource(R.string.read_aloud_adjust_speed),
-                            value = speedPreview.coerceIn(5f, 20f),
-                            valueLabel = (speedPreview / 10f).let {
-                                if (it == it.roundToInt().toFloat()) {
-                                    "${it.roundToInt()}.0"
-                                } else {
-                                    String.format("%.1f", it)
-                                }
-                            },
+                            value = speedPreview.coerceIn(
+                                READ_ALOUD_SPEED_MIN.toFloat(),
+                                READ_ALOUD_SPEED_MAX.toFloat(),
+                            ),
+                            valueLabel = formatReadAloudSpeedLabel(speedPreview.roundToInt()),
                             startLabel = stringResource(R.string.fast_rewind),
                             endLabel = stringResource(R.string.fast_forward),
                             onValueChange = { speedPreview = it },
                             onValueChangeFinished = {
                                 onIntent(ReadAloudPlayerIntent.SetSpeed(speedPreview.roundToInt()))
                             },
-                            valueRange = 5f..20f,
-                            steps = 14,
+                            valueRange = READ_ALOUD_SPEED_MIN.toFloat()..READ_ALOUD_SPEED_MAX.toFloat(),
+                            steps = READ_ALOUD_SPEED_MAX - READ_ALOUD_SPEED_MIN - 1,
                         )
                     }
                     AnimatedVisibility(activeAdjustment == PlayerAdjustment.Timer) {

+ 67 - 0
app/src/test/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerSpeedRangeTest.kt

@@ -0,0 +1,67 @@
+package io.legado.app.ui.book.readaloud.player
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * 语速调节范围相关纯逻辑测试。
+ *
+ * 背景:听书播放器与经典朗读控制(ReadAloudScreen.kt 的 valueRange = 0f..80f)应使用
+ * 一致的语速范围;此前播放器为 5f..20f 与经典控制 0f..80f 不一致(issue #2032)。
+ */
+class ReadAloudPlayerSpeedRangeTest {
+
+    @Test
+    fun `speed range matches classic read aloud control`() {
+        // 与 ReadAloudScreen.kt 经典朗读控制的 valueRange = 0f..80f 保持一致
+        assertEquals(0, READ_ALOUD_SPEED_MIN)
+        assertEquals(80, READ_ALOUD_SPEED_MAX)
+    }
+
+    @Test
+    fun `slider steps cover every integer in the range`() {
+        // 0..80 共 81 个刻度点,steps = 刻度点 - 1 = 79
+        assertEquals(79, READ_ALOUD_SPEED_MAX - READ_ALOUD_SPEED_MIN - 1)
+    }
+
+    @Test
+    fun `coerce clamps below minimum`() {
+        assertEquals(READ_ALOUD_SPEED_MIN, coerceReadAloudSpeed(-5))
+        assertEquals(READ_ALOUD_SPEED_MIN, coerceReadAloudSpeed(Int.MIN_VALUE))
+    }
+
+    @Test
+    fun `coerce clamps above maximum`() {
+        assertEquals(READ_ALOUD_SPEED_MAX, coerceReadAloudSpeed(999))
+        assertEquals(READ_ALOUD_SPEED_MAX, coerceReadAloudSpeed(Int.MAX_VALUE))
+    }
+
+    @Test
+    fun `coerce keeps in-range values unchanged`() {
+        assertEquals(0, coerceReadAloudSpeed(0))
+        assertEquals(20, coerceReadAloudSpeed(20))
+        assertEquals(40, coerceReadAloudSpeed(40))
+        assertEquals(80, coerceReadAloudSpeed(80))
+    }
+
+    @Test
+    fun `format label shows one decimal place`() {
+        assertEquals("2.0", formatReadAloudSpeedLabel(20))
+        assertEquals("8.0", formatReadAloudSpeedLabel(80))
+        assertEquals("0.0", formatReadAloudSpeedLabel(0))
+    }
+
+    @Test
+    fun `format label keeps fractional display`() {
+        assertEquals("0.5", formatReadAloudSpeedLabel(5))
+        assertEquals("1.5", formatReadAloudSpeedLabel(15))
+        assertEquals("0.7", formatReadAloudSpeedLabel(7))
+    }
+
+    @Test
+    fun `player slider value beyond old 20 cap is now supported`() {
+        // 回归:旧播放器上限 20,修复后 0..80 内任意值都应可用
+        assertEquals(40, coerceReadAloudSpeed(40))
+        assertEquals("4.0", formatReadAloudSpeedLabel(40))
+    }
+}