完善播放器进度条与媒体加载态

This commit is contained in:
BoatingCloud committed 2026-08-27 22:59:12 +08:00
1 parent 3b322012cf
commit 27154a3a4e
9 files changed
+197 -14

No files matched your search

-1
View File
@@ -45,7 +45,6 @@ dependencies {
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
implementation("io.coil-kt:coil-compose:2.7.0")
implementation("androidx.palette:palette-ktx:1.0.0")
implementation("androidx.activity:activity-compose:1.13.0")
@@ -71,7 +71,7 @@ import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaSkeleton
import com.example.myapplication.ui.design.LumaMotion
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaSpacing
@@ -658,8 +658,25 @@ fun HomeScreen(
EmptyHomeState()
}
} else if (isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaLoading(label = stringResource(Res.string.loading_home))
Column(
Modifier
.fillMaxSize()
.padding(horizontal = LumaSpacing.md, vertical = LumaSpacing.xl),
verticalArrangement = Arrangement.spacedBy(LumaSpacing.md),
) {
LumaSkeleton(Modifier.fillMaxWidth().height(290.dp), RoundedCornerShape(LumaShapes.large))
LumaSkeleton(Modifier.fillMaxWidth(0.48f).height(22.dp), RoundedCornerShape(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
repeat(2) {
LumaSkeleton(Modifier.weight(1f).height(170.dp), RoundedCornerShape(LumaShapes.small))
}
}
LumaSkeleton(Modifier.fillMaxWidth(0.34f).height(22.dp), RoundedCornerShape(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
repeat(3) {
LumaSkeleton(Modifier.weight(1f).height(150.dp), RoundedCornerShape(LumaShapes.small))
}
}
}
} else if (loadError) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -61,7 +61,7 @@ import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaSkeleton
import com.example.myapplication.ui.design.LumaShapes
import top.yukonga.miuix.kmp.basic.Text
@@ -130,8 +130,16 @@ fun RecordsScreen(
subtitle = stringResource(Res.string.empty_calendar_sub),
)
} else if (loading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaLoading(label = stringResource(Res.string.loading_favorites))
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(6) {
LumaSkeleton(Modifier.fillMaxWidth().aspectRatio(0.68f), RoundedCornerShape(LumaShapes.small))
}
}
} else if (loadError) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -50,7 +50,7 @@ import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaSkeleton
import kotlinx.coroutines.delay
import top.yukonga.miuix.kmp.basic.Text
@@ -124,8 +124,19 @@ fun SearchScreen(
title = stringResource(Res.string.search_title),
subtitle = stringResource(Res.string.search_empty_input),
)
loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaLoading(label = stringResource(Res.string.loading_search))
loading -> LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = 16.dp,
bottom = bottomBarContentPadding(86.dp).calculateBottomPadding(),
),
verticalArrangement = Arrangement.spacedBy(14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(6) {
LumaSkeleton(Modifier.fillMaxWidth().aspectRatio(0.68f), RoundedCornerShape(16.dp))
}
}
loadError -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaErrorState(
@@ -34,8 +34,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -77,6 +75,7 @@ import com.example.myapplication.R as Res
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaSeekBar
import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.LumaShapes
@@ -391,7 +390,7 @@ fun VideoPlayerScreen(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(formatMs(currentPositionMs), color = Color.White, fontSize = 12.sp)
Slider(
LumaSeekBar(
value = if (isSeeking) seekPosition else progress,
onValueChange = { v -> isSeeking = true; seekPosition = v },
onValueChangeFinished = {
@@ -400,7 +399,9 @@ fun VideoPlayerScreen(
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, "TimeUpdate")
},
modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
colors = SliderDefaults.colors(thumbColor = Color.White, activeTrackColor = Color.White)
activeColor = Color.White,
trackColor = Color.White.copy(alpha = 0.32f),
thumbColor = Color.White,
)
Text(formatMs(totalDurationMs), color = Color.White, fontSize = 12.sp)
}
@@ -1,7 +1,13 @@
package com.example.myapplication.ui.design
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.LinearEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
@@ -15,21 +21,42 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setProgress
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import top.yukonga.miuix.kmp.basic.Text
import kotlin.math.abs
@Composable
fun LumaAsyncImage(
@@ -122,6 +149,118 @@ fun LumaSectionTitle(
}
}
@Composable
fun LumaSeekBar(
value: Float,
onValueChange: (Float) -> Unit,
onValueChangeFinished: () -> Unit,
modifier: Modifier = Modifier,
activeColor: Color = LumaColors.accent,
trackColor: Color = LumaColors.divider,
thumbColor: Color = activeColor,
) {
val density = LocalDensity.current
var trackWidthPx by remember { mutableFloatStateOf(0f) }
var dragging by remember { mutableStateOf(false) }
val currentValue by rememberUpdatedState(value.coerceIn(0f, 1f))
val currentOnValueChange by rememberUpdatedState(onValueChange)
val currentOnValueChangeFinished by rememberUpdatedState(onValueChangeFinished)
val thumbRadius = with(density) { if (dragging) 7.dp.toPx() else 4.dp.toPx() }
fun updateFromPosition(x: Float) {
if (trackWidthPx > 0f) currentOnValueChange((x / trackWidthPx).coerceIn(0f, 1f))
}
Canvas(
modifier
.heightIn(min = 48.dp)
.onSizeChanged { trackWidthPx = it.width.toFloat() }
.semantics {
progressBarRangeInfo = ProgressBarRangeInfo(currentValue, 0f..1f)
setProgress { requestedValue ->
currentOnValueChange(requestedValue.coerceIn(0f, 1f))
currentOnValueChangeFinished()
true
}
}
.pointerInput(trackWidthPx) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
dragging = true
updateFromPosition(down.position.x)
var lastX = down.position.x
var moved = false
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull() ?: break
if (change.changedToUp()) {
updateFromPosition(change.position.x)
change.consume()
dragging = false
currentOnValueChangeFinished()
break
}
val delta = change.position.x - lastX
if (!moved && abs(change.position.x - down.position.x) > viewConfiguration.touchSlop) {
moved = true
}
if (moved || delta != 0f) {
updateFromPosition(change.position.x)
change.consume()
}
lastX = change.position.x
}
dragging = false
}
},
) {
val centerY = size.height / 2f
val trackHeight = 4.dp.toPx()
val progressWidth = size.width * currentValue
val radius = trackHeight / 2f
drawRoundRect(
color = trackColor,
topLeft = Offset(0f, centerY - radius),
size = Size(size.width, trackHeight),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius),
)
drawRoundRect(
color = activeColor,
topLeft = Offset(0f, centerY - radius),
size = Size(progressWidth, trackHeight),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius),
)
drawCircle(
color = thumbColor,
radius = thumbRadius,
center = Offset(progressWidth.coerceIn(0f, size.width), centerY),
)
}
}
@Composable
fun LumaSkeleton(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(LumaShapes.small),
) {
val transition = rememberInfiniteTransition(label = "lumaSkeleton")
val alpha by transition.animateFloat(
initialValue = 0.52f,
targetValue = 0.86f,
animationSpec = infiniteRepeatable(
animation = tween(900, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
label = "lumaSkeletonAlpha",
)
Box(
modifier
.clip(shape)
.background(LumaColors.backgroundElevated)
.graphicsLayer { this.alpha = alpha },
)
}
@Composable
fun LumaLoading(
modifier: Modifier = Modifier,
+6
View File
@@ -124,3 +124,9 @@
文件变更: MainApp.kt、screens/LibraryScreen.kt、screens/SettingsScreen.kt、ui/LiquidGlass.kt、ui/design/LumaComponents.kt、ui/design/LumaDesignSystem.kt、memory/*。
教训: 模拟器继续禁用 Kyant RenderEffect 采样,截图中的玻璃为安全降级态;真实 Backdrop 仅在设备安全门和帧率门控同时通过时启用。
标签: #lumaplayer #glass #fallback #dark-mode #verification
[PROJECT:lumaplayer] 播放器进度与媒体加载态
结论: 用自绘可访问进度条替代 Material Slider,并为 Home、Vision 收藏、Search 增加统一脉冲骨架;完整 Debug/Lint/Unit 构建通过,参考素材未跟踪。
文件变更: app/build.gradle.kts、screens/HomeScreen.kt、screens/RecordsScreen.kt、screens/SearchScreen.kt、screens/VideoPlayerScreen.kt、ui/design/LumaComponents.kt、memory/*。
教训: 启动截图需等待启动流程完成后再判断加载态;当前 API35 模拟器仍只验证安全降级玻璃,不能代替真机 Backdrop 逐帧比对。
标签: #lumaplayer #loading #skeleton #player #accessibility #build
+1
View File
@@ -9,3 +9,4 @@
- 构建验证:Backdrop 层级收敛、模拟器安全降级、播放器采样隔离和 GlassSheet 收尾后 `assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功;已在 `sdk_gphone64_arm64` API 35 模拟器安装启动并点击 Vision/Settings/语言菜单,未出现 `SIGSEGV/RenderThread`,尚未做真机录屏比对。
- 详细状态见 [projects.md](projects.md),踩坑见 [lessons.md](lessons.md)。
- 最新收尾:公共分段控件、按钮、输入框、主题/语言/资源库弹层统一走同一套降级毛玻璃;状态栏/导航栏图标随深色主题同步;Debug/Lint/Unit 与 API35 模拟器回归通过。
- 最新改动:播放器进度条改为自绘可访问 SeekBar,媒体首页、Vision 收藏和搜索加载态改为统一骨架占位;移除未使用的 Material 图标依赖,完整 Debug/Lint/Unit 通过。
+1
View File
@@ -34,3 +34,4 @@
- 2026-08-27:首页增加按服务器身份隔离的进程内内容缓存,普通页面返回不重复请求,显式重试才刷新;玻璃运行时增加 Choreographer 帧率门控,底栏高帧率安全设备走本地 AndroidLiquidGlass-kmp 的 Lens,模拟器/低帧率使用连续胶囊毛玻璃和选中高光;`assembleDebug`、`lintDebug`、模拟器安装启动复核成功。
- 2026-08-27:底栏普通图标调整为 33dp、搜索图标 37dp;降级玻璃增加最终 Capsule 裁剪并减弱蓝色选中填充,原生路径空闲态保留轻微 Lens;详情/播放器/剧集加载和播放地址失败时回落到错误态;最终 APK 在 API 35 模拟器启动、Vision/Settings/语言菜单点击复核成功。
- 2026-08-27:收敛公共分段控件、弹出菜单、按钮、输入框及语言菜单的降级玻璃路径,移除纯色方框;补充深色主题系统栏图标同步和操作标题 48dp 点击区域。`assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功,API 35 模拟器验证设置/语言/主题/关于/服务器添加链路无崩溃;本地参考素材仍未跟踪。
- 2026-08-27:播放器进度条替换为自绘拖拽控件并补充无障碍进度调节,Home/Vision/Search 的媒体加载态改为统一骨架占位;移除未使用的 Material 图标依赖。`assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功。