完善播放器进度条与媒体加载态
This commit is contained in:
1 parent
3b322012cf
commit
27154a3a4e
9 files changed
+197
-14
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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 通过。
|
||||
@@ -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` 成功。
|
||||
Reference in new issue
Block a user