优化播放器界面

This commit is contained in:
beyond committed 2026-09-06 19:47:26 +08:00
1 parent 29601803f5
commit e3e8120e8b
3 files changed
+69 -30

No files matched your search

@@ -64,9 +64,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -126,12 +129,12 @@ import top.yukonga.miuix.kmp.basic.Text
import kotlin.math.abs
import kotlin.math.roundToInt
private fun formatMs(ms: Long): String {
val totalSec = ms / 1000
internal fun formatMs(ms: Long): String {
val totalSec = ms.coerceAtLeast(0L) / 1000
val h = totalSec / 3600
val m = (totalSec % 3600) / 60
val s = totalSec % 60
return if (h > 0) String.format("%d:%02d:%02d", h, m, s) else String.format("%02d:%02d", m, s)
return String.format(java.util.Locale.ROOT, "%02d:%02d:%02d", h, m, s)
}
private fun formatNetworkSpeed(bps: Long): String {
@@ -338,6 +341,7 @@ fun VideoPlayerScreen(
var stopReported by remember(playbackUrl) { mutableStateOf(false) }
var progress by remember { mutableFloatStateOf(0f) }
var isSeeking by remember { mutableStateOf(false) }
var isPlaying by remember { mutableStateOf(false) }
var playerError by remember(playbackUrl) { mutableStateOf<String?>(null) }
var networkSpeed by remember { mutableLongStateOf(0L) }
@@ -392,7 +396,7 @@ fun VideoPlayerScreen(
LaunchedEffect(Unit) {
while (true) {
if (player.duration > 0 && seekPreviewPositionMs == null) {
if (player.duration > 0 && !isSeeking && seekPreviewPositionMs == null) {
progress = player.currentPosition.toFloat() / player.duration.toFloat()
}
isPlaying = player.isPlaying
@@ -821,6 +825,8 @@ fun VideoPlayerScreen(
) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val capsuleInset = maxWidth * 0.075f + 20.dp
// The 40dp button surface is centered in a 48dp touch target.
val buttonInset = maxWidth * 0.075f - 4.dp
Box(
Modifier
.fillMaxSize()
@@ -842,7 +848,7 @@ fun VideoPlayerScreen(
),
active = isScreenLocked,
size = 40.dp,
modifier = Modifier.align(Alignment.CenterStart).padding(start = capsuleInset),
modifier = Modifier.align(Alignment.CenterStart).padding(start = buttonInset),
) {
isScreenLocked = !isScreenLocked
controlsVisible = true
@@ -855,7 +861,7 @@ fun VideoPlayerScreen(
contentDescription = stringResource(Res.string.player_rotate_screen),
active = false,
size = 40.dp,
modifier = Modifier.align(Alignment.CenterEnd).padding(end = capsuleInset),
modifier = Modifier.align(Alignment.CenterEnd).padding(end = buttonInset),
) {
activity?.requestedOrientation = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
@@ -865,7 +871,7 @@ fun VideoPlayerScreen(
}
Row(
Modifier.align(Alignment.BottomStart).padding(start = capsuleInset, bottom = 14.dp),
Modifier.align(Alignment.BottomStart).padding(start = buttonInset, bottom = 14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -923,7 +929,7 @@ fun VideoPlayerScreen(
Spacer(Modifier.height(2.dp))
// 第二排:关闭按钮(左)|Emby 艺术字(紧跟关闭按钮右侧)|字幕/剧集按钮(右)
Row(
Modifier.fillMaxWidth().padding(horizontal = capsuleInset),
Modifier.fillMaxWidth().padding(horizontal = buttonInset),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -1030,7 +1036,7 @@ fun VideoPlayerScreen(
borderColor = Color.White.copy(alpha = 0.22f),
shadowElevation = 4.dp,
)
.padding(horizontal = 12.dp, vertical = 0.dp),
.padding(horizontal = 8.dp, vertical = 0.dp),
) {
var totalDurationMs by remember(player) {
mutableLongStateOf(player.duration.coerceAtLeast(0L))
@@ -1038,21 +1044,8 @@ fun VideoPlayerScreen(
var currentPositionMs by remember(player) {
mutableLongStateOf(player.currentPosition.coerceAtLeast(0L))
}
var isSeeking by remember { mutableStateOf(false) }
var seekPosition by remember { mutableFloatStateOf(0f) }
var bufferProgress by remember { mutableFloatStateOf(0f) }
var seekCompletedVersion by remember { mutableIntStateOf(0) }
// Reset isSeeking after seek completes to prevent jitter
LaunchedEffect(seekCompletedVersion) {
if (seekCompletedVersion > 0) {
delay(300)
// Sync currentPositionMs to the seek target before resuming polling
// to prevent a visual jump when player position hasn't caught up yet
currentPositionMs = (progress * totalDurationMs).toLong()
isSeeking = false
}
}
LaunchedEffect(player) {
while (true) {
@@ -1068,9 +1061,20 @@ fun VideoPlayerScreen(
}
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
val swipePositionMs = seekPreviewPositionMs
Text(formatMs(swipePositionMs ?: currentPositionMs), color = Color.White, fontSize = 11.sp)
// Reserve space independently of the digits: device fonts may ignore monospace.
val timeWidth = with(LocalDensity.current) {
(formatMs(totalDurationMs).length * 7).sp.toDp()
}
Text(
formatMs(swipePositionMs ?: currentPositionMs),
modifier = Modifier.width(timeWidth),
color = Color.White,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
maxLines = 1,
)
LumaSeekBar(
value = when {
isSeeking -> seekPosition
@@ -1088,13 +1092,11 @@ fun VideoPlayerScreen(
player.seekTo(targetPosition)
progress = finalValue
currentPositionMs = targetPosition
isSeeking = false
controlsInteractionLocked = false
latestOnPlaybackProgress(targetPosition, !player.isPlaying, "TimeUpdate", true)
// Keep isSeeking true briefly to prevent polling loop from
// overwriting currentPositionMs before player catches up
seekCompletedVersion++
},
modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
modifier = Modifier.weight(1f).height(28.dp),
progressContentDescription = stringResource(Res.string.player_progress),
activeColor = Color.White,
trackColor = Color.White.copy(alpha = 0.12f),
@@ -1102,7 +1104,15 @@ fun VideoPlayerScreen(
bufferedValue = bufferProgress,
thumbColor = Color.White,
)
Text(formatMs(totalDurationMs), color = Color.White, fontSize = 11.sp)
Text(
formatMs(totalDurationMs),
modifier = Modifier.width(timeWidth),
color = Color.White,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.End,
maxLines = 1,
)
}
}
}
@@ -509,6 +509,7 @@ fun LumaSeekBar(
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
down.consume()
val horizontalInset = 7.dp.toPx()
val usableWidth = (size.width - horizontalInset * 2f).coerceAtLeast(1f)
var gestureValue = ((down.position.x - horizontalInset) / usableWidth).coerceIn(0f, 1f)
@@ -521,8 +522,8 @@ fun LumaSeekBar(
if (change.pressed) {
gestureValue = ((change.position.x - horizontalInset) / usableWidth).coerceIn(0f, 1f)
currentOnValueChange(gestureValue)
change.consume()
}
change.consume()
}
dragging = false
currentOnValueChangeFinished(gestureValue)
@@ -0,0 +1,28 @@
package com.example.myapplication.screens
import org.junit.Assert.assertEquals
import org.junit.Test
class VideoPlayerTimeFormatTest {
@Test
fun alwaysDisplaysHoursMinutesAndSeconds() {
assertEquals("00:00:00", formatMs(0L))
assertEquals("00:00:09", formatMs(9_999L))
assertEquals("00:01:00", formatMs(60_000L))
assertEquals("00:59:59", formatMs(3_599_999L))
assertEquals("01:00:00", formatMs(3_600_000L))
assertEquals("12:34:56", formatMs(45_296_000L))
}
@Test
fun unknownDurationDisplaysZero() {
assertEquals("00:00:00", formatMs(-1L))
assertEquals("00:00:00", formatMs(Long.MIN_VALUE + 1L))
}
@Test
fun hoursDoNotWrapAfterOneDay() {
assertEquals("25:00:00", formatMs(90_000_000L))
assertEquals("100:00:00", formatMs(360_000_000L))
}
}