diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8db8ad..69072a4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt index dcd6a81..8d877cf 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt @@ -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) { diff --git a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt index 13f0e7e..b41a9f7 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt @@ -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) { diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt index d9f27df..3725cc3 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt index 2765a01..6d3585c 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt @@ -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) } diff --git a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt index 9b4eb43..8b9863c 100644 --- a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt +++ b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt @@ -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, diff --git a/memory/2026-08-27.md b/memory/2026-08-27.md index 88a4975..2fe98cd 100644 --- a/memory/2026-08-27.md +++ b/memory/2026-08-27.md @@ -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 diff --git a/memory/MEMORY.md b/memory/MEMORY.md index 60cf5a6..2fa3215 100644 --- a/memory/MEMORY.md +++ b/memory/MEMORY.md @@ -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 通过。 diff --git a/memory/projects.md b/memory/projects.md index 8ff0c57..b2125f8 100644 --- a/memory/projects.md +++ b/memory/projects.md @@ -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` 成功。