WebUI 1:1 移植工程文档
目标:把
WebUI/文件夹里的 React 原型完全复刻到 Android Kotlin Compose 原生实现,达到视觉 pixel-level 一致。
唯一"不完全复刻"的地方是:动态强调色(dynamic accent)会真实影响按钮、进度条、滑动条的颜色(WebUI 已这么做,原生要保留这个行为)。
0. 总览
0.1 WebUI 的视觉语言(要复刻的核心)
| 要素 | WebUI 现状 | 当前原生实现 |
|---|---|---|
| 字体 | DM Sans(全屏使用) | 系统默认 |
| Tab 结构 | 底部 2 个 tab(Library / Search),Library 页内部有 4 个 chip 子 tab(Albums / Artists / Songs / Playlists) | 底部 4 个 Material NavigationBar(歌曲/专辑/艺术家/文件夹) |
| 底栏外观 | 悬浮玻璃胶囊(pill),滚动时横向形变(200dp + MiniPlayer → 54dp + MiniPlayer 同行) | Material3 NavigationBar,无悬浮、无形变 |
| MiniPlayer | 玻璃胶囊(border-radius 999),进度以 SVG 弧线贴着胶囊底边 | Surface 圆角 16dp + LinearProgressIndicator |
| 专辑封面 | 线性渐变 + 装饰 SVG 图案 + glow 阴影(无真实图时的 fallback),有真实图时真实图优先 | MusicNote 占位图标 |
| 动态强调色 | 从当前歌曲所属专辑的 palette.accent 取色,染按钮、chip、进度条、曲名高亮等 |
固定 Material 配色 |
| NowPlaying 背景 | 三种 visual style(noir/vivid/frosted),vivid 从封面梯度做大面积渐变背景 |
固定黑色 |
| NowPlaying 布局 | 顶部 drag handle + context header + 278dp 封面(暂停 scale(0.86))+ 标题/艺术家/心 + 自定义 scrubber + 3-按钮 transport(76dp 白色 FAB)+ 音量条 + 底部 4-图标工具行(Shuffle/Lyrics/Queue/Repeat) | 简化版 Material 播放界面 |
0.2 移植策略
- 完全替换(不保留旧 UI):
LibraryScreen/ 底栏 / MiniPlayer / NowPlayingScreen / AlbumDetailScreen - 新建:
SearchScreen/PlaylistsListTab/GenreGrid - 保留后端:Room、ExoPlayer、Hilt、扫描器不改
- 仅改"视图 + 导航 + 主题":逻辑层(ViewModel 数据流)保持不动,只把 UI 拆装成 WebUI 的样子
0.3 Tab 重构决策
结论:采用 WebUI 原结构(2 底部 tab + 4 chip 子 tab)。
- 旧结构:底部 4 tab(歌曲/专辑/艺术家/文件夹)
- 新结构:
- 底部 2 tab:Library / Search
- Library 内顶部 chip tab:Albums / Artists / Songs / Playlists
- 文件夹浏览降级为 Library 页的次要入口(顶部小按钮或隐藏到设置)——WebUI 里没有文件夹概念,文件夹作为"不在 WebUI 中但工程需要保留"的特例
1. 设计 Token 体系
1.1 颜色
新建 ui/theme/WebUiPalette.kt:
1 | package io.kyonqi.musicplayer.ui.theme |
1.2 动态强调色的 CompositionLocal
新建 ui/theme/LocalAccent.kt:
1 | package io.kyonqi.musicplayer.ui.theme |
1.3 DM Sans 字体
- 下载 DM Sans 字体文件(Regular 400, Medium 500, SemiBold 600, Bold 700)
- https://fonts.google.com/specimen/DM+Sans → Download family
- 放到
app/src/main/res/font/:dm_sans_regular.ttfdm_sans_medium.ttfdm_sans_semibold.ttfdm_sans_bold.ttf
- 改造
ui/theme/Type.kt:
1 | package io.kyonqi.musicplayer.ui.theme |
1.4 主题入口
改造 ui/theme/Theme.kt(关键点:关闭 dynamicColor,使用黑底为主 + 动态 accent 覆盖):
1 |
|
2. 核心组件
2.1 AlbumArt(渐变 + 装饰图案 fallback)
完全重写 ui/component/AlbumArt.kt:
1 | package io.kyonqi.musicplayer.ui.component |
图案 variant 选择策略:对 album.id % 4 取模即可得到四种 pattern 之一,让不同专辑看起来不同。
2.2 MiniPlayer(玻璃胶囊 + 进度弧)
完全重写 ui/component/MiniPlayer.kt:
1 | package io.kyonqi.musicplayer.ui.component |
注意:
MiniPlayerViewModel保留现在的,无需改(playbackState/togglePlayPause/next已够用)。
2.3 浮动玻璃胶囊 BottomTabBar(2 tab)
新建 ui/component/FloatingTabBar.kt:
1 | package io.kyonqi.musicplayer.ui.component |
2.4 Flex-morph 组合容器
新建 ui/component/FloatingBottomArea.kt:底部 flex-wrap 的等价:MiniPlayer + 底栏在一块。
1 | package io.kyonqi.musicplayer.ui.component |
2.5 滚动 → 收缩的 NestedScrollConnection
复用现有 prototype/CollapsibleBottomBarState.kt 的思路,但简化成布尔:collapsed: State<Boolean>。
新建 ui/component/FloatingBarScroll.kt:
1 | package io.kyonqi.musicplayer.ui.component |
3. 屏幕移植
3.1 LibraryScreen(4 chip 子 tab)
新建 ui/screen/LibraryScreen.kt:
1 | package io.kyonqi.musicplayer.ui.screen |
3.2 SongRow(通用)
新建 ui/component/SongRow.kt:
1 |
|
3.3 AlbumDetailScreen(重写)
关键点:顶部居中大封面 180dp + Back 按钮 + 标题 + artist(accent) + year/N songs + Play All 胶囊(accent 实色)+ 曲目列表。
1 |
|
3.4 SearchScreen(全新)
新建 ui/screen/SearchScreen.kt:
- 顶部 “Search” 28sp 标题
- 搜索 input(玻璃底色 + 圆角 12dp + 左放大镜)
- 未输入时:Browse Genres(2 列 grid,6 个渐变色块,每块 68dp 高,左下显示文字)
- 输入时:SECTION “Results” 大写粗体 + SongRow showAlbumArt 列表
Genre 颜色映射:
1 | val Genres = listOf( |
每个格子背景:linear-gradient(135deg, ${color}33, ${color}88),边框 ${color}44。
3.5 NowPlayingScreen(完整重写)
1 | package io.kyonqi.musicplayer.ui.screen |
NowPlayingViewModel 需要新增:
var isLiked: Boolean、fun toggleLike()var volume: Float、fun setVolume(v: Float)(内部调用 AudioManager)var shuffle/repeat: Boolean、toggleShuffle()/toggleRepeat()fun seekTo(ms: Long)
4. 导航与顶层壳
4.1 AppNavigation 重写
1 |
|
4.2 路由变更总结
| 旧路由 | 新路由 | 说明 |
|---|---|---|
library_song/library_albums/library_artists/library_folders |
library |
合并为 Library 页,chip 子 tab 切换 |
| — | search |
新增 |
album_detail/{id} |
album_detail/{id} |
保留,重写 UI |
artist_detail/{id} |
artist_detail/{id} |
保留,重写 UI |
now_playing |
now_playing |
保留,重写 UI |
4.3 动态 Palette 提取
新建 ui/theme/PaletteExtractor.kt(已存在,需改造为:从 AudioFile 取):
1 |
|
5. 文件变更清单
5.1 新建(17 个)
| 路径 | 说明 |
|---|---|
ui/theme/WebUiPalette.kt |
AlbumPalette / WebUiColors |
ui/theme/LocalAccent.kt |
LocalAccentColor / WebUi 快捷访问 |
ui/component/FloatingTabBar.kt |
2-tab 玻璃胶囊 |
ui/component/FloatingBottomArea.kt |
flex-wrap 容器 |
ui/component/FloatingBarScroll.kt |
NestedScroll → collapsed State |
ui/component/SongRow.kt |
通用曲目行 |
ui/component/ChipTab.kt |
子 tab chip |
ui/component/GenreCard.kt |
Search 页面 genre 格子 |
ui/screen/LibraryScreen.kt |
大标题 + chip + 4 子页(Albums/Artists/Songs/Playlists) |
ui/screen/SearchScreen.kt |
Search 新页 |
app/src/main/res/font/dm_sans_regular.ttf |
字体 |
app/src/main/res/font/dm_sans_medium.ttf |
字体 |
app/src/main/res/font/dm_sans_semibold.ttf |
字体 |
app/src/main/res/font/dm_sans_bold.ttf |
字体 |
5.2 重写(10 个)
| 路径 | 改动 |
|---|---|
ui/component/AlbumArt.kt |
渐变 + SVG 图案 fallback + 真实封面覆盖 |
ui/component/MiniPlayer.kt |
玻璃胶囊 + 进度弧 |
ui/screen/AlbumDetailScreen.kt |
大居中封面 + Play All 胶囊 + 曲目列表 |
ui/screen/ArtistDetailScreen.kt |
渐变圆头像 + 专辑 + 曲目,按 WebUI 结构重排 |
ui/screen/NowPlayingScreen.kt |
drag handle + context + 278dp 封面 + 3-按钮 + 音量 + 4 图标工具行 |
ui/navigation/AppNavigation.kt |
2 tab + 悬浮底栏,替换 Scaffold+NavigationBar |
ui/theme/Type.kt |
切到 DM Sans |
ui/theme/Theme.kt |
关闭 dynamicColor,深色优先 |
ui/theme/PaletteExtractor.kt |
从 track 取封面 bitmap → androidx.palette 提取 |
ui/screen/NowPlayingScreen.kt 的 ViewModel |
新增 isLiked/volume/shuffle/repeat/seekTo |
5.3 废弃(5 个)
| 路径 | 处置 |
|---|---|
ui/screen/SongListScreen.kt |
合并进 LibraryScreen 的 Songs chip |
ui/screen/AlbumListScreen.kt |
合并进 LibraryScreen 的 Albums chip |
ui/screen/ArtistListScreen.kt |
合并进 LibraryScreen 的 Artists chip |
ui/screen/FolderBrowserScreen.kt |
降级为 Library 页内次要入口(或移入设置) |
ui/screen/FileBrowseScreen.kt |
删除 |
prototype/*.kt |
删除原型 |
6. 实施阶段
Phase A — 视觉基底(1 日)
- 下载字体 →
res/font/ - 新增
WebUiPalette.kt/LocalAccent.kt - 改造
Type.kt/Theme.kt - 重写
AlbumArt.kt(渐变 + 图案 fallback) - 验收:启动 app,所有现有屏字体变 DM Sans;
AlbumArt(uri = null, palette = Default)能看到紫色渐变封面。
Phase B — 浮动底栏(1-2 日)
- 新增
FloatingTabBar.kt/FloatingBottomArea.kt/FloatingBarScroll.kt - 重写
MiniPlayer.kt(玻璃胶囊 + 进度弧) - 重写
AppNavigation.kt:Scaffold → Box + 悬浮底栏,2 tab 路由 - 合并
SongListScreen/AlbumListScreen/ArtistListScreen→LibraryScreen(先用当前 ViewModel 直接塞进去,chip tab 切换) - 验收:底部悬浮胶囊,向下滚动收缩为单行,向上回弹展开;点击 tab 切换 Library ↔ Search。
Phase C — Library / AlbumDetail 完整样式(2 日)
- 完成 LibraryScreen 四个 chip 子 tab 的 item 样式(Albums grid / Artists list / Songs / Playlists 占位)
- 重写 AlbumDetailScreen(大封面 + Play All)
- 重写 ArtistDetailScreen(渐变头像 + 列表)
- 新增 SongRow / ChipTab
- 验收:Library 页切四个 chip,item 视觉与 WebUI 比对;AlbumDetail 进入后大封面+按钮样式一致。
Phase D — SearchScreen(1 日)
- 新增
SearchScreen.kt+GenreCard.kt - 空态显示 6 格 genre;输入触发
debounce(300)搜索并切换成结果列表 - 复用
SearchViewModel(如不存在则新建,注入 TrackDao/AlbumDao/ArtistDao) - 验收:空态 / 输入 / 无结果三个状态都能复现。
Phase E — NowPlaying 完整重写(2-3 日)
- 重写
NowPlayingScreen.kt全部组件(drag handle / context header / 278dp 封面 + scale 动画 / 标题行 + heart / Scrubber / Transport 3-btn / Volume / 4 UtilBtn) - 扩展 NowPlayingViewModel(isLiked/volume/shuffle/repeat/seekTo)
- 实现 VisualStyle enum 切换(noir/vivid/frosted)——可以做到 设置页一个 chip 切
- 验收:进入 NowPlaying 逐项比对截图。
Phase F — 动态 Accent 提取(1-2 日)
- 改造
PaletteExtractor.kt:从track.uri读取 cover bitmap →androidx.palette.graphics.Palette提取vibrantSwatch/darkVibrantSwatch→ 组装AlbumPalette - 在 AppNavigation 里切歌时重算 palette,推给
ProvideAlbumPalette - 异步:用
rememberCoroutineScope+Dispatchers.IO读 bitmap,避免卡 UI - 验收:播放不同封面的歌,底栏高亮/曲名 accent 明显变化。
Phase G — 清理(0.5 日)
- 删除废弃文件
- 删除
prototype/*.kt - 删
FileBrowseScreen.kt/MusicListItem.kt(如未被 Library 新 row 复用) - 运行一次
./gradlew :app:lint/./gradlew :app:assembleDebug验收
7. 与 WebUI 的视觉对照表(像素级 checklist)
| 组件 | 尺寸 / 颜色 | WebUI 源 |
|---|---|---|
| 底部悬浮底栏容器 | 距底 10dp,左右 padding 12dp | MikuPlayer.html :227 |
| TabBar 展开宽度 | 200dp,高 54dp,radius 999 | player-components.jsx :78 |
| TabBar 收起宽度 | 54dp,radius 999 | 同上 |
| MiniPlayer 高度 | 54dp radius 999 | :160 |
| MiniPlayer 内 padding | left 5 right 6 top/bot 5 | :167 |
| MiniPlayer 曲名 | DM Sans 13.5sp SemiBold | :173 |
| MiniPlayer 艺术家 | DM Sans 11.5sp | :177 |
| MiniPlayer play 按钮 | 40dp 圆形 半透明底 | :182 |
| 进度弧 | stroke 1.5px accent 0.85 alpha | :201 |
| AlbumArt 渐变角度 | 145deg | :37 |
| AlbumArt 阴影 | 0 (size*0.06) (size*0.2) glow |
:38 |
| Library 大标题 | 28sp Bold -0.5 letter-spacing | player-screens.jsx :15 |
| Chip tab | padding 6/14, radius 100 | :22 |
| Chip 活动色 | accent 底 + 白字 | :24 |
| Albums grid | 2 列,gap 16dp | :37 |
| Artist 头像 | 52dp 圆,grad 135deg | :59 |
| AlbumDetail 封面 | 180dp radius 18 | :129 |
| Play All | radius 100, padding 10/28, shadow 6 20 accent66 | :139 |
| NowPlaying drag handle | 36x5 radius 3 alpha 0.25 | player-nowplaying.jsx :82 |
| NowPlaying 封面 | 278dp radius 14 shadow 24 50 .5 | :113 |
| NowPlaying play FAB | 76dp 圆 白底 shadow 10 32 .35 | :193 |
| NowPlaying prev/next | 38dp icon | :188/203 |
| UtilBtn 活动底 | 44dp radius 12 alpha 0.12 | :56 |
| UtilBtn label | 10sp Bold 0.3 letter | :62 |
8. 风险与备选
-
玻璃模糊(backdrop-filter):Compose 没有直接对应。三档备选:
- A(推荐起步):用半透明
Color(alpha=0.72)+border(alpha=0.12)模拟——视觉上近似,零成本 - B:接入 haze 库,真 GPU 模糊
- C:Android 12+ 用
RenderEffect.createBlurEffect,兼容差 - 本文档选 A,验收期可再升级
- A(推荐起步):用半透明
-
字体授权:DM Sans 是 OFL 开源字体,可直接打包进 APK
-
Palette 提取性能:切歌时 IO 线程读 bitmap ≈ 50-200ms,动画上会有 palette 延迟切换——可先用默认 palette 播放,读完再 swap
-
AlbumArt patternVariant 复用:真实 App 数据里 albumId 是 Long(非 a1/a2),取
albumId.toInt() % 4即可,视觉多样性够用 -
ArtistDetailScreen 在 WebUI 里没有实现:文档只写了 onViewArtist 钩子;原生要自己补一个(渐变头像大图 + 专辑 grid + 曲目 list)
-
Playlists tab WebUI 有静态数据:原生需要 Phase C 之后做 Playlist Room 表(已在 phase2-music-library.md 里规划)
9. 验收最终项
- [ ] 所有屏字体都是 DM Sans
- [ ] 底栏在 Library/Search 页显示,在 NowPlaying 隐藏
- [ ] 底栏向下滚动时收缩成单行;向上滚动时展开
- [ ] MiniPlayer 胶囊贴底边有一条 accent 色进度弧,随播放进度延长
- [ ] 切不同专辑播放,按钮/进度条/高亮文字颜色可见地变化
- [ ] Library 页顶部 4 个 chip 可切,活动 chip 背景 = accent
- [ ] Album 页大封面 180dp 居中 + Play All 胶囊按钮
- [ ] NowPlaying 有 drag handle,向下拽或点击可关
- [ ] NowPlaying 暂停时封面有缩小动画 scale(0.86)
- [ ] NowPlaying 进度条可拖动 seek
- [ ] 底部 4 图标 Shuffle/Repeat 点击有 active bg,颜色变 accent
- [ ] Search 空态显示 6 格 genre;输入文字展示 Results section
说些什么吧!