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

feat: 依旧听书和有声书界面优化

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

+ 18 - 31
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayScreen.kt

@@ -12,12 +12,10 @@ import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.statusBars
 import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.foundation.pager.VerticalPager
@@ -34,8 +32,6 @@ import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.KeyboardArrowUp
 import androidx.compose.material.icons.filled.Lock
 import androidx.compose.material.icons.filled.Login
-import androidx.compose.material.icons.filled.Pause
-import androidx.compose.material.icons.filled.PlayArrow
 import androidx.compose.material.icons.filled.Repeat
 import androidx.compose.material.icons.filled.RepeatOne
 import androidx.compose.material.icons.filled.Shuffle
@@ -46,7 +42,6 @@ import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material.icons.filled.Timer
 import androidx.compose.material.icons.filled.VolumeUp
 import androidx.compose.material.icons.filled.WbTwilight
-import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
@@ -57,6 +52,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -80,7 +76,6 @@ import io.legado.app.ui.theme.hazeStyle.HazeLegado
 import io.legado.app.ui.util.rememberBlurBackdrop
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.button.series.MediumPlainButton
-import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.button.series.SmallAnimatedButton
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.image.cover.BookCoverImage
@@ -90,6 +85,7 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
+import io.legado.app.ui.widget.components.player.AnimatedPlayPauseButton
 import io.legado.app.ui.widget.components.player.PlayerAdjustmentSlider
 import io.legado.app.ui.widget.components.player.PlayerBackground
 import io.legado.app.ui.widget.components.player.PlayerProgressSlider
@@ -169,14 +165,14 @@ fun AudioPlayScreenContent(
                     verticalAlignment = Alignment.CenterVertically,
                     horizontalArrangement = Arrangement.SpaceBetween
                 ) {
-                    MediumTonalButton(
+                    MediumPlainButton(
                         onClick = onBack,
                         icon = AppIcons.Back,
                         contentDescription = stringResource(R.string.back),
                     )
 
                     Box {
-                        MediumTonalButton(
+                        MediumPlainButton(
                             onClick = { menuExpanded = true },
                             icon = AppIcons.MoreVert,
                             contentDescription = stringResource(R.string.more),
@@ -334,7 +330,7 @@ fun AudioPlayScreenContent(
                 Row(
                     modifier = Modifier
                         .fillMaxWidth()
-                        .padding(top = 12.dp),
+                        .padding(top = 16.dp, bottom = 4.dp),
                     horizontalArrangement = Arrangement.SpaceEvenly,
                     verticalAlignment = Alignment.CenterVertically,
                 ) {
@@ -344,24 +340,15 @@ fun AudioPlayScreenContent(
                         icon = Icons.Default.SkipPrevious,
                         contentDescription = stringResource(R.string.previous_chapter),
                     )
-                    Box(contentAlignment = Alignment.Center) {
-                        MediumTonalButton(
-                            onClick = { onIntent(AudioPlayIntent.TogglePlay) },
-                            onLongClick = { onIntent(AudioPlayIntent.Stop) },
-                            icon = if (state.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
-                            contentDescription = stringResource(
-                                if (state.isPlaying) R.string.pause else R.string.audio_play
-                            ),
-                            modifier = Modifier.size(72.dp),
-                        )
-                        if (state.isLoading) {
-                            CircularProgressIndicator(
-                                modifier = Modifier.size(56.dp),
-                                strokeWidth = 3.dp,
-                                color = LegadoTheme.colorScheme.onSecondaryContainer,
-                            )
-                        }
-                    }
+                    AnimatedPlayPauseButton(
+                        isPlaying = state.isPlaying,
+                        isLoading = state.isLoading,
+                        contentDescription = stringResource(
+                            if (state.isPlaying) R.string.pause else R.string.audio_play
+                        ),
+                        onClick = { onIntent(AudioPlayIntent.TogglePlay) },
+                        onLongClick = { onIntent(AudioPlayIntent.Stop) },
+                    )
                     MediumPlainButton(
                         onClick = { onIntent(AudioPlayIntent.NextChapter) },
                         enabled = state.canNext,
@@ -634,7 +621,6 @@ private fun formatGain(gainMb: Int): String {
 private const val AUDIO_SPEED_MIN = 0.5f
 private const val AUDIO_SPEED_MAX = 5.0f
 
-
 @Composable
 private fun AudioCoverPage(
     state: AudioPlayUiState,
@@ -657,8 +643,10 @@ private fun AudioCoverPage(
                 path = state.coverPath,
                 sourceOrigin = state.sourceOrigin,
                 modifier = Modifier
-                    .fillMaxWidth(0.64f)
-                    .aspectRatio(5f / 7f)
+                    .fillMaxSize(0.64f)
+                    .shadow(
+                        elevation = 16.dp
+                    )
                     .clip(RoundedCornerShape(8.dp))
             )
         }
@@ -678,7 +666,6 @@ private fun AudioCoverPage(
             )
             AppText(
                 text = state.chapterTitle,
-                modifier = Modifier.padding(top = 16.dp),
                 style = LegadoTheme.typography.titleMediumEmphasized,
                 color = LegadoTheme.colorScheme.onSurface,
                 textAlign = TextAlign.Center,

+ 1 - 9
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -14,15 +14,14 @@ import io.legado.app.constant.PreferKey
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookProgress
-import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.repository.BookRepository
-import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.BookmarkRepository
 import io.legado.app.data.repository.HighlightRuleRepository
 import io.legado.app.data.repository.HttpTtsRepository
 import io.legado.app.data.repository.ReadAloudSettingsRepository
 import io.legado.app.data.repository.ReadPreferences
+import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.data.repository.ReplaceRuleRepository
 import io.legado.app.data.repository.SettingsRepository
@@ -1774,13 +1773,6 @@ class ReadBookViewModel(
                     _readAloudProgress.value = null
                     _effects.tryEmit(ReadBookEffect.UpAloudState)
                 }
-                if (previousStatus != ReadAloudSessionStatus.Paused &&
-                    status == ReadAloudSessionStatus.Paused
-                ) {
-                    _effects.tryEmit(
-                        ReadBookEffect.ShowToast(context.getString(R.string.read_aloud_pause))
-                    )
-                }
                 previousStatus = status
             }
         }

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

@@ -42,8 +42,6 @@ import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
 import androidx.compose.material.icons.filled.FastForward
 import androidx.compose.material.icons.filled.FastRewind
 import androidx.compose.material.icons.filled.KeyboardArrowUp
-import androidx.compose.material.icons.filled.Pause
-import androidx.compose.material.icons.filled.PlayArrow
 import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.SkipNext
 import androidx.compose.material.icons.filled.SkipPrevious
@@ -66,6 +64,7 @@ import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
@@ -95,6 +94,7 @@ import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.image.cover.BookCoverImage
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
+import io.legado.app.ui.widget.components.player.AnimatedPlayPauseButton
 import io.legado.app.ui.widget.components.player.PlayerAdjustmentSlider
 import io.legado.app.ui.widget.components.player.PlayerBackground
 import io.legado.app.ui.widget.components.player.PlayerProgressSlider
@@ -284,7 +284,7 @@ fun ReadAloudPlayerScreenContent(
                     Row(
                         modifier = Modifier
                             .fillMaxWidth()
-                            .padding(top = 12.dp),
+                            .padding(top = 16.dp, bottom = 4.dp),
                         horizontalArrangement = Arrangement.SpaceEvenly,
                         verticalAlignment = Alignment.CenterVertically,
                     ) {
@@ -299,13 +299,12 @@ fun ReadAloudPlayerScreenContent(
                             contentDescription = stringResource(R.string.previous_chapter),
                             modifier = Modifier.size(48.dp),
                         )
-                        MediumTonalButton(
+                        AnimatedPlayPauseButton(
+                            isPlaying = !state.isPaused,
                             onClick = { onIntent(ReadAloudPlayerIntent.TogglePause) },
-                            icon = if (state.isPaused) Icons.Default.PlayArrow else Icons.Default.Pause,
                             contentDescription = stringResource(
                                 if (state.isPaused) R.string.resume else R.string.pause
                             ),
-                            modifier = Modifier.size(64.dp),
                         )
                         MediumTonalButton(
                             onClick = { onIntent(ReadAloudPlayerIntent.NextChapter) },
@@ -528,6 +527,9 @@ private fun CoverPage(
                 modifier = Modifier
                     .fillMaxWidth(0.52f)
                     .aspectRatio(5f / 7f)
+                    .shadow(
+                        elevation = 16.dp
+                    )
                     .clip(RoundedCornerShape(8.dp))
             )
         }

+ 158 - 0
app/src/main/java/io/legado/app/ui/widget/components/player/AnimatedPlayPauseButton.kt

@@ -0,0 +1,158 @@
+package io.legado.app.ui.widget.components.player
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.GenericShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.rotate
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.sin
+
+/** 播放器共用的播放/暂停键,包含图标与容器形变。 */
+@Composable
+fun AnimatedPlayPauseButton(
+    isPlaying: Boolean,
+    contentDescription: String,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    isLoading: Boolean = false,
+    onLongClick: (() -> Unit)? = null,
+) {
+    val transition = updateTransition(targetState = isPlaying, label = "PlayerPlayPause")
+    val morphProgress by transition.animateFloat(
+        transitionSpec = { tween(durationMillis = 300, easing = FastOutSlowInEasing) },
+        label = "PlayerPlayPauseMorph",
+    ) { playing -> if (playing) 1f else 0f }
+    val containerColor by transition.animateColor(
+        transitionSpec = { tween(durationMillis = 300, easing = FastOutSlowInEasing) },
+        label = "PlayerPlayPauseContainer",
+    ) { playing ->
+        if (playing) LegadoTheme.colorScheme.primaryContainer
+        else LegadoTheme.colorScheme.secondaryContainer
+    }
+    val contentColor by transition.animateColor(
+        transitionSpec = { tween(durationMillis = 300, easing = FastOutSlowInEasing) },
+        label = "PlayerPlayPauseContent",
+    ) { playing ->
+        if (playing) LegadoTheme.colorScheme.onPrimaryContainer
+        else LegadoTheme.colorScheme.onSecondaryContainer
+    }
+    val interactionSource = remember { MutableInteractionSource() }
+
+    Box(
+        modifier = modifier
+            .size(72.dp)
+            .semantics { this.contentDescription = contentDescription }
+            .clip(playerButtonContainerShape(morphProgress))
+            .combinedClickable(
+                interactionSource = interactionSource,
+                role = Role.Button,
+                onLongClick = onLongClick,
+                onClick = onClick,
+            ),
+        contentAlignment = Alignment.Center,
+    ) {
+        Canvas(modifier = Modifier.fillMaxSize()) {
+            val radius = min(size.width, size.height) / 2f
+            drawPath(playerButtonContainerPath(radius, morphProgress), containerColor)
+        }
+        Canvas(modifier = Modifier.size(36.dp)) {
+            rotate(degrees = 90f * morphProgress) {
+                drawPath(playPauseIconPath(size.minDimension / 24f, morphProgress), contentColor)
+            }
+        }
+        if (isLoading) {
+            CircularProgressIndicator(
+                modifier = Modifier.size(56.dp),
+                strokeWidth = 3.dp,
+                color = contentColor,
+            )
+        }
+    }
+}
+
+private fun playerButtonContainerPath(radius: Float, progress: Float): Path = Path().apply {
+    val center = radius
+    val baseRadius = radius * (1f - 0.055f * progress)
+    val lobeAmplitude = radius * 0.055f * progress
+    val rotation = 30f * progress / 180f * PI.toFloat()
+    repeat(BUTTON_PATH_SAMPLES + 1) { index ->
+        val sourceAngle =
+            index.toFloat() / BUTTON_PATH_SAMPLES * 2f * PI.toFloat() - PI.toFloat() / 2f
+        val angle = sourceAngle + rotation
+        val animatedRadius = baseRadius + lobeAmplitude * cos(8f * sourceAngle)
+        val x = center + animatedRadius * cos(angle)
+        val y = center + animatedRadius * sin(angle)
+        if (index == 0) moveTo(x, y) else lineTo(x, y)
+    }
+    close()
+}
+
+private fun playerButtonContainerShape(progress: Float) = GenericShape { size, _ ->
+    val radius = min(size.width, size.height) / 2f
+    addPath(playerButtonContainerPath(radius, progress))
+}
+
+private fun playPauseIconPath(scale: Float, progress: Float): Path = Path().apply {
+    for (subPath in PLAY_ICON_POINTS.indices) {
+        val from = PLAY_ICON_POINTS[subPath]
+        val to = PAUSE_ICON_POINTS[subPath]
+        from.indices.forEach { index ->
+            val point = from[index]
+            val target = to[index]
+            val x = (point.x + (target.x - point.x) * progress) * scale
+            val y = (point.y + (target.y - point.y) * progress) * scale
+            if (index == 0) moveTo(x, y) else lineTo(x, y)
+        }
+        close()
+    }
+}
+
+private data class MorphPoint(val x: Float, val y: Float)
+
+private const val BUTTON_PATH_SAMPLES = 128
+
+private val PLAY_ICON_POINTS = arrayOf(
+    arrayOf(
+        MorphPoint(8f, 5f), MorphPoint(8f, 12f), MorphPoint(19f, 12f),
+        MorphPoint(19f, 12f), MorphPoint(8f, 5f),
+    ),
+    arrayOf(
+        MorphPoint(8f, 12f), MorphPoint(8f, 19f), MorphPoint(19f, 12f),
+        MorphPoint(19f, 12f), MorphPoint(8f, 12f),
+    ),
+)
+
+private val PAUSE_ICON_POINTS = arrayOf(
+    arrayOf(
+        MorphPoint(5f, 6f), MorphPoint(5f, 10f), MorphPoint(19f, 10f),
+        MorphPoint(19f, 6f), MorphPoint(5f, 6f),
+    ),
+    arrayOf(
+        MorphPoint(5f, 14f), MorphPoint(5f, 18f), MorphPoint(19f, 18f),
+        MorphPoint(19f, 14f), MorphPoint(5f, 14f),
+    ),
+)