UI 美化实施手册(完整代码)
本文档是
Plans/ui-polish-apple-music.md的配套实施参考。
每个章节给出完整、可直接粘贴的 Kotlin 代码,保持项目已有风格:
@HiltViewModel + @Inject constructor,ViewModel 写在同一文件的 Composable 之上- 状态模式:
private val _state = MutableStateFlow(...)+val state = _state.asStateFlow()- 中文注释,私有工具函数放文件底部
collectAsStateWithLifecycle()收集 StateFlow,hiltViewModel()注入 VM- URI 字符串转换统一用
androidx.core.net.toUri
总览:改动清单
| 顺序 | 文件 | 操作 |
|---|---|---|
| 1 | gradle/libs.versions.toml |
已有 palette,确认版本 |
| 1 | app/build.gradle.kts |
已有 implementation(libs.androidx.palette) |
| 2 | ui/theme/MotionDefaults.kt |
🆕 新建 |
| 3 | ui/theme/PaletteExtractor.kt |
🆕 新建 |
| 4 | ui/component/MusicListItem.kt |
🆕 新建 |
| 5 | ui/screen/LibraryScannerViewModel.kt |
🆕 新建 |
| 6 | ui/component/AlbumArt.kt |
✏️ 加 shape 参数(可选) |
| 7 | ui/component/MiniPlayer.kt |
✏️ 布局微调 |
| 8 | ui/screen/SongListScreen.kt |
✏️ LargeTopAppBar + MusicListItem |
| 9 | ui/screen/AlbumListScreen.kt |
✏️ LargeTopAppBar + 网格圆角 |
| 10 | ui/screen/ArtistListScreen.kt |
✏️ LargeTopAppBar + MusicListItem |
| 11 | ui/screen/FolderBrowserScreen.kt |
✏️ LargeTopAppBar |
| 12 | ui/screen/AlbumDetailScreen.kt |
✏️ 大封面头 + 渐变背景 |
| 13 | ui/screen/ArtistDetailScreen.kt |
✏️ 大头像头部 + 渐变 |
| 14 | ui/screen/NowPlayingScreen.kt |
✏️ 沉浸式渐变 + spring |
| 15 | ui/navigation/AppNavigation.kt |
✏️ 底部 TabBar + slide 转场 |
| 16 | ui/screen/LibraryScreen.kt |
🗑️ 删除 |
1. 依赖(已有,确认即可)
gradle/libs.versions.toml:
1 | [versions] |
app/build.gradle.kts:
1 | // palette |
2. ui/theme/MotionDefaults.kt 🆕
全局 spring 参数,避免散落各处自定义 animationSpec。
1 | package io.kyonqi.musicplayer.ui.theme |
3. ui/theme/PaletteExtractor.kt 🆕
从封面 Bitmap 提取主色。利用 Coil 共享 ImageLoader,避免重复解码;allowHardware(false) 是为了 Palette 能读到像素。
1 | package io.kyonqi.musicplayer.ui.theme |
4. ui/component/MusicListItem.kt 🆕
统一圆角卡片样式,替代 Material ListItem 的分隔线风格。
1 | package io.kyonqi.musicplayer.ui.component |
5. ui/screen/LibraryScannerViewModel.kt 🆕
把原 LibraryViewModel 的扫描逻辑抽出来,独立于任何 Tab 页。AppNavigation 顶层通过 hiltViewModel() 实例化一次,供所有 tab 共享。
1 | package io.kyonqi.musicplayer.ui.screen |
6. ui/component/AlbumArt.kt ✏️(可选扩展)
给封面加可选的 shape 参数,方便各处复用。保持原签名兼容(size + modifier 不变),只新增 shape 默认为 RoundedCornerShape(8.dp)。
1 | package io.kyonqi.musicplayer.ui.component |
7. ui/component/MiniPlayer.kt ✏️
布局微调:加圆角 + 更大内边距,封面加圆角。功能不变。
1 | package io.kyonqi.musicplayer.ui.component |
8. ui/screen/SongListScreen.kt ✏️
改动:LargeTopAppBar + MusicListItem 圆角卡片 + contentPadding 处理顶栏折叠。
1 | package io.kyonqi.musicplayer.ui.screen |
9. ui/screen/AlbumListScreen.kt ✏️
改动:LargeTopAppBar + 网格 item 视觉收紧(移除外层 Card,只保留封面圆角 + 下方文字)。
1 | package io.kyonqi.musicplayer.ui.screen |
10. ui/screen/ArtistListScreen.kt ✏️
改动:LargeTopAppBar + MusicListItem 圆角卡片。
1 | package io.kyonqi.musicplayer.ui.screen |
11. ui/screen/FolderBrowserScreen.kt ✏️
改动:LargeTopAppBar + 展开项用 MusicListItem。
1 | package io.kyonqi.musicplayer.ui.screen |
12. ui/screen/AlbumDetailScreen.kt ✏️
改动:大封面头部 + 基于封面的渐变背景(用 rememberDominantColor)。
1 | package io.kyonqi.musicplayer.ui.screen |
13. ui/screen/ArtistDetailScreen.kt ✏️
改动:大头像头部 + 专辑行用横向滚动(保留原单独专辑 ListItem,视觉更紧凑)+ 歌曲用 SongListItem。
1 | package io.kyonqi.musicplayer.ui.screen |
14. ui/screen/NowPlayingScreen.kt ✏️
沉浸式渐变 + spring 封面缩放 + 透明 TopAppBar。
1 | package io.kyonqi.musicplayer.ui.screen |
15. ui/navigation/AppNavigation.kt ✏️
完整重构:底部 TabBar(4 个 library tab) + MiniPlayer 在 Scaffold.bottomBar 里 + now_playing 走 slide-up/down 转场。扫描触发通过 LibraryScannerViewModel 在顶层激活。
1 | package io.kyonqi.musicplayer.ui.navigation |
16. ui/screen/LibraryScreen.kt 🗑️
删除此文件。Tab 状态和扫描触发都已迁移:
- Tab index →
AppNavigation+NavController initLibrary() / reflashLibrary() / scanProgress→LibraryScannerViewModelTopAppBar("音乐库")被四个LargeTopAppBar替代
实施顺序建议(与 plan 的"实施顺序"章节对齐)
- Day 1 — 新建
MotionDefaults.kt、PaletteExtractor.kt、MusicListItem.kt。编译通过即可 - Day 2 — 改
SongListScreen.kt,验证 LargeTopAppBar + 圆角卡片观感 - Day 3 — 改
AlbumListScreen.kt/ArtistListScreen.kt/FolderBrowserScreen.kt(批量应用 LargeTopAppBar) - Day 4-5 — 新建
LibraryScannerViewModel.kt,重构AppNavigation.kt(底部 TabBar),删除LibraryScreen.kt - Day 6 — 改
MiniPlayer.kt布局 - Day 7-8 — 改
NowPlayingScreen.kt(渐变 + spring),确认颜色过渡与按钮可见性 - Day 9 — 改
AlbumDetailScreen.kt/ArtistDetailScreen.kt(大头 + 渐变) - Day 10 —
AlbumArt.kt加 shape 参数(可选,如果前面没提前做) - Day 11-14 — 联调、深浅色主题验证、空数据边界、长字符串 Ellipsis
常见坑
rememberDominantColor返回 null — 说明 Coil 拿不到 Bitmap。检查AudioCoverFetcher是否生效(之前踩过的coil3.Urivsandroid.net.Uri坑)LargeTopAppBar下面内容被盖住 — 记得给 LazyColumn 用contentPadding = padding,不要用Modifier.padding(padding)加外层(前者让 TopAppBar 与内容有叠加效果,后者会把列表硬压下去)- 内层 Column 用
fillMaxSize()— 同样的 bug 之前踩过两次(SongListScreen、NowPlayingScreen 的按钮消失)。只有最外层容器能用fillMaxSize(),内层一律fillMaxWidth() - Tab 切换后再回来滚动位置丢了 —
navigateToTab必须带saveState = true / restoreState = true,见AppNavigation.navigateToTab - 渐变背景跳变 — 背景色一定要走
animateColorAsState,否则切歌瞬间闪白或闪黑 MiniPlayer在now_playing页仍可见 — 确认HiddenBottomBarRoutes包含Routes.NOW_PLAYING,并且AnimatedVisibility(visible = showBottomBar)包裹整个 bottomBar
验收清单
- [ ] 冷启动首次进入,扫描进度条能在顶部显示,扫完自动消失
- [ ] 底部 4 个 Tab 可切换,切换有短暂动画,且每个 tab 独立滚动位置
- [ ] 从 MiniPlayer 点击进入 NowPlaying,页面从下往上滑入;返回时滑下去消失
- [ ] NowPlaying 在
now_playing路由下完全不显示 MiniPlayer 和 NavigationBar - [ ] 切歌时 NowPlaying 背景颜色平滑过渡(
animateColorAsState),封面有 spring 缩放 - [ ] 专辑详情顶部有根据封面的渐变
- [ ] 长歌名 / 长艺术家名 / 长专辑名都正确 ellipsize
- [ ] 深色模式和浅色模式都能正常工作(尤其确认 NowPlayingScreen 的白字在所有取色下可读;如果某些浅色封面下白字看不清,可以根据
dominant.luminance()动态切换前景色)
说些什么吧!