设计原则
五条规则,决定每个页面怎么画
正在播放哪首歌,界面就用哪张封面的颜色。浏览页用低彩度的柔和风格,播放页用更饱和的鲜明风格。没有歌在播时,回到品牌松绿或系统壁纸色。
播放、切歌、换源、收藏都在屏幕下半部。迷你播放器左右滑切歌、上滑展开;播放页的次要操作收进底部浮动工具栏。
现在的界面靠 1dp 描边区分卡片。新方案用 surface container 的五级色调区分层级,只有悬浮层(顶栏、迷你播放器、导航栏、底部面板)用 Haze 模糊。
形变和弹簧只出现在播放/暂停、选中、展开这类状态变化上。列表滚动和页面切换保持安静。打开“减少动画”后,弹簧、形变、波浪和共享元素全部关闭。
手机一臂远,平板半臂远,电视三米远。同一个组件在不同窗口尺寸下调整密度、字号和焦点样式,不写三套页面。
对照官方做法
M3 Expressive 的七条做法,逐条落到 myMusic
Google 在《Start building with Material 3 Expressive》里给出七条做法。下表是每一条在 myMusic 里的用法,以及刻意不用的地方。官方研究也提醒:表现力再强也替代不了基本可用性,而且有相当一部分用户更喜欢平静的界面。所以浏览页保持克制,力度集中在播放上。
| 官方做法 | 在 myMusic 里怎么做 | 克制在哪里 |
|---|---|---|
| 混用多种形状 圆与方形成对比;打破周围的形状来吸引注意 | 播放键在圆形与圆角方之间形变;榜首封面、正在播放的封面用 Cookie12Sided 裁切;榜单卡片用 Arch 裁切;选中项变成全圆角 | 抽象形状只用于图片裁切和装饰,不用在按钮和文字多的容器上;列表和卡片用 4 / 16dp 的小圆角 |
| 丰富而有层次的颜色 用 surface 色调建层级;用 primary / secondary / tertiary 拉开主次 | 官方举的例子正是“音乐应用从专辑封面取色”:MaterialKolor 从封面生成整套配色。primary 只给最重要的操作(播放、FAB);tertiary 用在榜首卡片这类对比强调;内容放在不同级别的 surface container 上 | 浏览页默认 TonalSpot,色度低;鲜明的 Vibrant 只用在播放页 |
| 用字体引导注意力 Emphasized 样式用于标题、操作和编辑感的时刻 | 页面大标题、歌名、选中项、主按钮用 Emphasized;排名和时间用 Roboto Flex 窄体粗字重,形成编辑感 | 正文和列表副文字保持常规字重;不用装饰字体 |
| 用容器突出内容 逻辑分组;最重要的内容给最多空间和最亮的 surface | 榜单、搜索结果、自建歌单、设置都用分段列表(2dp 间隙,内 4dp / 外 16dp 圆角);榜首和“最佳匹配”放进更大的彩色容器 | 每屏最多一个彩色容器,其余容器都用中性色调 |
| 流畅自然的动效 形变、表面效果、表现力弹簧 | MotionScheme.expressive():按钮和形变用快速空间弹簧,底部面板用默认弹簧,迷你播放器展开为全屏播放页用慢速弹簧;切歌时整套配色用效果弹簧过渡 | 列表滚动、页面切换不加回弹;“减少动画”改用 standard 方案 |
| 灵活运用组件 按场景和屏幕尺寸调整组件 | 同一功能按窗口尺寸换组件:短导航栏 ↔ Wide navigation rail ↔ 电视抽屉;播放控制在手机是按钮组,在电视是可聚焦的控制条 | 工具栏和导航栏不同时出现;FAB 菜单不和导航轨同屏 |
| 组合出高光时刻 产品里只保留一两个;要有情感冲击,并且是关键交互 | 只有两个:① 沉浸式播放页(铺满封面、流动背景、形变播放键、波浪进度);② 切歌时整个界面随封面换色 | 榜单、歌单、搜索、我的都不做高光,只用容器、字体和颜色分层 |
用到的 Expressive 组件
| 组件 | 用在哪里 | 规格 |
|---|---|---|
| Large flexible top app bar | 榜单、歌单、我的 | 带副标题时 152dp,上滑收成 64dp 小标题栏 |
| Connected button group | 推荐 / 分类、单曲 / 歌单 / 专辑、主题模式、定时时长、平板的歌词 / 队列 / 音源 | 替代旧的分段按钮;2dp 间隙;选中项变成全圆角 |
| Standard button group | 播放页的上一首 / 播放 / 下一首 | 按下的按钮变宽变方,相邻按钮让位;播放键是 L 号宽版(128×96dp),最显眼 |
| Split button | 歌单详情的“播放全部” | M 号 56dp;右侧固定为展开图标,菜单里是随机播放、加入队列、存到本地歌单 |
| FAB menu | “我的”页:新建歌单、导入链接、从文件导入 | 3 项;打开时 FAB 由方变圆,图标旋转成关闭 |
| FAB | 平板导航轨顶部的搜索 | 56dp,16dp 圆角 |
| Short navigation bar / Wide navigation rail | 手机底栏 4 项;平板导航轨收起时 96dp,展开为 220–360dp 的模态导航 | 展开的导航轨替代旧的抽屉 |
| Loading indicator | 榜单、歌单、搜索的加载 | 200ms 内不显示;200ms–5s 用形变指示器;超过 5s(例如下载)用进度条 |
| Wavy progress | 播放进度、迷你播放器的进度环 | 播放时波动,暂停时拉直 |
| Segmented list | 所有歌曲列表、自建歌单、设置 | 2dp 间隙,内 4dp / 外 16dp,选中 16dp |
| Multi-browse carousel | 榜单页的主推榜单 | 项目圆角 28dp |
| Slider | 外观设置的对比度 | XS 轨道 16dp、4dp 宽竖条手柄;不用 XL 滑杆(官方建议只留给高光) |
来源:m3.material.io 的 Building with M3 Expressive、Shape、Motion、Typography、Color roles 和各组件页面;design.google 的 Expressive Material Design 研究文章。核对日期 2026-10-22。
导航与布局
四个入口,设置退到二级
底栏从五项减到四项:榜单、歌单、搜索、我的。设置是低频入口,移到“我的”右上角;平板和电视放在导航栏底部。
现在
- 底栏 5 项,设置占一格
- 贴底迷你播放条,2dp 进度线
- 固定松绿配色,Android 12+ 可用系统取色
- 卡片和面板靠 1dp 描边区分
- ≥840dp 常驻 290dp 侧栏
新方案
- 底栏 4 项(Short navigation bar),设置在“我的”右上角
- 悬浮胶囊迷你播放器,波浪进度环,可滑动切歌
- MaterialKolor 封面取色,所有 Android 版本都可用
- 色调层级 + Haze 悬浮层
- ≥1200dp 才常驻右侧窗格,中等宽度用底部播放条
| 窗口宽度 | 典型设备 | 导航 | 播放入口 | 内容 |
|---|---|---|---|---|
| Compact < 600dp | 手机竖屏 | Short navigation bar,4 项 | 悬浮迷你播放器 → 全屏播放页 | 单列,卡片 2 列 |
| Medium 600–839dp | 折叠屏展开、小平板竖屏 | 收起的 Wide navigation rail | 悬浮迷你播放器,居中,最宽 560dp | 卡片 3 列 |
| Expanded 840–1199dp | 平板横屏 | Rail | 底部播放条,横跨内容区 | 列表与网格并排 |
| Large 1200–1599dp | 大平板、桌面窗口 | Rail,可展开 | 右侧“正在播放”窗格 380dp | 卡片 3–4 列 |
| Extra-large ≥ 1600dp | 外接显示器 | 展开的 Wide rail | 右侧窗格 + 内嵌歌词 | 卡片 5 列 |
| 电视 | UI_MODE_TYPE_TELEVISION | 左侧焦点展开式导航 | 全屏播放,空闲后进入环境模式 | 横向内容行 + Hero |
断点使用 androidx.window 1.5 的 WindowSizeClass(V2 断点含 Large / Extra-large),读取方式为 currentWindowAdaptiveInfoV2()。电视单独判断 uiMode,不看宽度。
动态配色 · MaterialKolor
从封面取一个种子色,生成整套配色
下面的色板由 material-color-utilities 0.4 实时计算,和 MaterialKolor 用的是同一套 HCT 算法与 2025 规范。换封面、风格或对比度,色板和所有样机一起更新。
默认怎么配
| 浏览页 | TonalSpot,色度低,长时间看不累 |
| 播放页 | Vibrant,固定深色,不随系统浅色切换;封面颜色最明显 |
| 电视 | 固定深色;Hero 区跟随焦点卡片的封面 |
| 没有在播 | 品牌松绿 #006B5F,或 Android 12+ 的壁纸色 |
| 黑白封面 | Score 找不到合适颜色时沿用上一个种子色,再不行用品牌色 |
| 切歌 | 先保留旧颜色,取色完成后 450ms 过渡;取色失败不闪烁 |
正在加载取色库…示例封面使用预先算好的色板。
外观设置里新增的选项
配色来源
跟随封面(默认)/ 系统壁纸(Android 12+)/ 品牌松绿 / 自选颜色。MaterialKolor 不依赖系统,Android 7–11 也能用封面取色。
配色风格
柔和、鲜明、表现力、保真四档,对应 PaletteStyle。浏览页和播放页可以分开设,默认柔和 + 鲜明。
对比度
标准 / 中 / 高,对应 contrastLevel 0 / 0.5 / 1.0。低视力用户选“高”后,所有文字和图标对比度达到 7:1 以上。
纯黑背景
深色模式下把 surface 压到纯黑,适合 OLED 屏。电视不提供这个选项,避免拖影。
模糊与层次 · Haze
只有悬浮层才模糊
内容层是 hazeSource,顶栏、迷你播放器、导航栏、底部面板是 hazeBlur。模糊让悬浮层和内容分开,又不完全挡住后面的封面。
| 层 | 材质 | 说明 |
|---|---|---|
| 顶栏 | HazeMaterials.thin() + 渐进模糊 | 只在内容滚到顶栏下面时出现;上方完全模糊,下沿渐隐 |
| 迷你播放器 | HazeMaterials.regular(),secondaryContainer 色调 | 悬浮胶囊,带阴影;是最常被看到的模糊层 |
| 导航栏 | HazeMaterials.thin(),surfaceContainer 色调 | 和迷你播放器共用一个 HazeState |
| 底部面板 | HazeMaterials.thick() | 换源、歌曲操作、队列;背后的播放页仍隐约可见 |
| 封面角标 | HazeMaterials.ultraThin() | 播放量等小标签压在封面上,保证可读 |
| 播放页流动背景 | 不用 Haze | 封面在 Coil 里预模糊成 64px 小图,用 graphicsLayer 缓慢旋转,再叠配色网格渐变。不依赖 RenderEffect,Android 7 起效果一致;息屏、后台和省电模式下停止转动 |
性能约束:每屏最多 3 个模糊层;用默认的 HazePerformanceMode.Default,掉帧时改 Performance;动画元素放在 hazeSource 子树之外。外观设置里的“模糊效果”提供 自动 / 开 / 关,“自动”在电视、省电模式和 API 30 以下关闭实时模糊。
形状 · 字体 · 动效
形状、字体和动效:力度集中在播放上
M3 Expressive 提供 35 种 MaterialShapes、按压形变的按钮、波浪进度条和弹簧动效。官方建议整个产品只做一两个高光时刻,myMusic 把它们都放在播放上;其他页面只用容器、字体和颜色分层。
暂停时是圆形,播放时变成圆角方形(ToggleButton 的选中形状)。点一下试试。
播放时波浪流动,暂停后拉平成直线。对应 LinearWavyProgressIndicator。
形状循环形变,替代现在的“正在读取…”文字。对应 LoadingIndicator。
Button group 里按住的按钮会变宽,挤压相邻按钮。播放页的上一首 / 下一首用这个效果。
抽象形状只用来裁切图片
官方说明:形状不承载语义,抽象形状要少用,适合图片裁切、头像遮罩和装饰,文字多的容器不用。所以 myMusic 只在下面五处用到,用来和周围的圆角矩形形成对比。
形状用在哪里
播放 / 暂停
圆形 ↔ 圆角方,所有播放键一致。
当前播放的曲目
队列里当前歌曲的封面裁成 Cookie12Sided,播放时缓慢旋转,暂停时停住。
“喜欢”磁贴
心形图标放在 Cookie9Sided 容器里,是“我的”页唯一的装饰形状。
空状态
大号 SoftBurst / Clover4Leaf 色块 + 图标 + 一句说明和一个按钮。
加载
LoadingIndicator 在 7 种形状间形变;列表加载用骨架屏。
选中态
Chip、按钮组、导航指示器选中时变成全圆角。
字体
headlineLargeEmphasized榜单titleLargeEmphasized潮汐来信播放页歌名、详情页标题bodyLarge鹿野 Lune · 浅滩列表主文字 15–16sp,副文字 12.5–13spRoboto Flex · wdth 40 · wght 8001 2 3 · 03:52排名数字、时间、计数。用 Google Fonts 可下载字体,不增加 APK 体积官方的 Emphasized 样式只提高字重、不改字号(如 headline 400→500,title-medium 与 label 500→700)。中文在 500 时和常规字重区分不明显,所以 AppTypography 把中文标题的 Emphasized 再加重到 700–800。中文用系统字体(Noto Sans CJK / 厂商字体),不打包中文字体。
动效
| 弹簧(阻尼比 / 刚度) | 快 | 默认 | 慢 |
|---|---|---|---|
| 表现力 · 空间 | 0.6 / 800 | 0.8 / 380 | 0.8 / 200 |
| 标准 · 空间(减少动画) | 0.9 / 1400 | 0.9 / 700 | 0.9 / 300 |
| 效果(两套相同) | 1.0 / 3800 | 1.0 / 1600 | 1.0 / 800 |
| 用在 | 按钮形变、chip、播放键 | 底部面板、FAB 菜单、导航轨展开 | 迷你播放器 → 全屏播放页、歌词状态切换 |
MotionScheme.expressive()
空间弹簧(有回弹)用于位置、尺寸、旋转和圆角;效果弹簧(无回弹)用于颜色和透明度。组件默认读取主题里的 MotionScheme,不需要逐个传参。
共享元素
迷你播放器封面 → 播放页封面;歌单卡片封面 → 详情页头部。返回时缩回原位,支持预测性返回手势(下拉收起播放页)。
减少动画
改用 MotionScheme.standard(),关闭形变、波浪、共享元素和封面旋转,颜色切换改为瞬时。
触感
播放/暂停、收藏、切歌手势完成时给一次轻触感反馈;拖动进度条时每 10 秒给一次刻度反馈。
手机 · Compact
九个关键页面
样机可以直接操作:点底栏切页,点迷你播放器打开播放页,点音源 chip 打开换源面板,在“我的”右上角进入外观设置。左侧列表也能切换页面,并显示每页的设计说明。
全部页面
每个页面,手机、平板、电视各一版
39 个页面和状态,每个都并排给出手机、平板(≥1200dp)和电视三版,下面写明每一端怎么变。规律是:手机的底部面板在平板变成侧边面板或菜单,在电视变成右侧面板;手机逐级进入的子页面在平板和电视变成“列表 + 详情”双栏;电视每页都标出默认焦点。所有样机跟随当前封面配色。
平板 · Expanded / Large
导航在左,播放在右,内容在中间
1200dp 以上把“正在播放”做成右侧浮起的窗格,跟随播放页配色;840–1199dp 窗格收起为底部播放条,把宽度还给内容。全屏播放页同样是沉浸式的:封面铺满左半屏并渐隐进流动背景,右边是歌名、歌词和控制,空闲后控件自动隐藏。
右侧窗格
380dp 宽,surfaceContainerLow 色、28dp 圆角,和内容区之间留 12dp 间隙,看起来是浮起的一块。可以向右拖动收起,收起后变成底部播放条。
内容自适应列数
卡片网格按可用宽度 2–5 列;榜单页左边是榜单曲目,右边是更多榜单。歌单详情用 List-detail:左边列表,右边详情。
鼠标、触控板和键盘
所有可点元素有 hover 态;右键 = 长按菜单;空格播放 / 暂停,← → 快退 / 快进 10 秒,Ctrl+F 聚焦搜索框。
折叠屏
展开时按 Medium / Expanded 处理;半折叠(桌面模式)时播放页上半屏显示封面和歌词,下半屏显示控制。
电视 · 三米距离
焦点在哪里,颜色就跟到哪里
点一下电视画面,然后用方向键移动焦点、回车打开、Esc 返回。焦点移到左侧会展开导航;焦点落在卡片上时,顶部 Hero 换成这张卡片的模糊封面和配色。回车打开全屏播放,4 秒没有操作就进入环境模式,只留歌词。
| 遥控器按键 | 浏览时 | 全屏播放时 |
|---|---|---|
| 方向键 | 移动焦点;移到最左展开导航 | 唤出控制条;控制条隐藏时 ← → 快退 / 快进 10 秒 |
| OK / 确认 | 打开榜单、歌单;在歌曲上直接播放 | 播放 / 暂停 |
| 返回 | 收起导航 → 回到上一层 | 隐藏控制条 → 回到浏览页(继续播放) |
| 菜单 | 当前项的操作:加入歌单、换源、收藏 | 队列 / 换源面板 |
| 媒体键 | 播放 / 暂停、上一首、下一首在任何页面都生效(由 MediaSession 处理) | |
焦点样式
放大 1.1 倍 + 3dp primary 描边 + 阴影。焦点任何时候都能一眼看出,不依赖颜色深浅。
字号与边距
正文不小于 16sp,标题约为手机的 1.25 倍;四周保留 48dp × 27dp 安全边距,防止过扫描裁切。
省资源
固定深色,Haze 默认关闭,模糊封面一次生成静态图;卡片行用懒加载,焦点移动时预取左右各两张封面。
官方文档仍推荐电视端用 androidx.tv:tv-material(当前 1.1.0)。它和 material3 各有一套 MaterialTheme,混用会导致 MaterialKolor 生成的配色只作用一半。本方案电视端也用 material3,焦点放大、描边和抽屉导航自己实现(约 200 行),换来三端同一套主题和组件。
功能微调
改了什么,为什么改
功能范围基本不变,主要调整入口和交互方式;睡眠定时是唯一的新功能,已确认加入。
| 现在 | 调整为 | 理由 |
|---|---|---|
| 底栏 5 个入口,含设置 | 4 个入口;设置在“我的”右上角、平板 / 电视导航栏底部 | 设置是低频入口,不该占拇指区一格 |
| 贴底迷你播放条 + 2dp 进度线 | 悬浮胶囊;播放键外一圈波浪进度环;左右滑切歌,上滑展开,长按打开队列 | 单手可操作,进度更显眼 |
| 固定松绿配色 / 系统取色 | 默认封面取色;可选壁纸、品牌色、自选色;可调风格、对比度、纯黑背景 | 颜色有了意义,也照顾 Android 12 以下设备 |
| 换源结果用 Snackbar 提示 | 音源 chip 内联显示“已从 酷狗 换到 QQ 音乐”;Snackbar 只留给需要用户决定的事,比如“只有试听片段,要播放吗” | 减少打断 |
| 移除、清除前弹确认框 | 可撤销的操作(移出歌单、清除搜索记录、取消收藏)改为 Snackbar 撤销;删除下载文件、删除歌单仍二次确认 | 少一步,误操作也能找回 |
| “播放全部”“收藏歌单”两个按钮并排 | Split button(播放全部 / 随机播放 / 加入队列 / 存到本地歌单)+ 收藏图标按钮 | 主操作更突出 |
| “正在读取…”文字 | LoadingIndicator + 骨架屏;失败时显示原因和重试按钮 | 等待有节奏,失败有出路 |
| 平板 / 电视常驻 290dp 侧栏 | ≥1200dp 才常驻(380dp);840–1199dp 用底部播放条;电视用全屏播放 | 中等宽度把空间还给内容 |
| 分类歌单“上一页 / 下一页” | 手机、平板滚动到底自动加载下一页;电视保留翻页按钮 | 触屏上翻页按钮太远 |
| 歌词只能看 | 点击某一行跳到该处播放;手动滚动 3 秒后回到当前行;电视增加环境模式 | 歌词也是导航 |
| 搜索类型含“链接”按钮 | 类型只留单曲 / 歌单 / 专辑;粘贴链接自动识别并显示“解析链接”行 | 少一个用户需要理解的选项 |
| 播放页是“封面卡片 + 按钮行”的常规布局 | 沉浸式播放页:封面铺满并延伸到状态栏下,背景是流动的封面光,固定深色;控件 4 秒后自动隐藏;封面上左右滑切歌、双击收藏;歌词预览常驻在歌名下 | 播放页是停留最久的页面,让封面和音乐占满屏幕 |
| 歌单详情跟随全局配色 | 头部渐变取该歌单封面的颜色,其余部分仍用全局配色 | 进入不同歌单有区分度 |
| 新增 没有睡眠定时 | 播放页底部月亮按钮打开“定时关闭”:15 / 30 / 45 / 60 分钟、播完当前歌曲再停、结束前 30 秒淡出;进行中在顶部显示倒计时 chip | 已确认加入。倒计时放在 PlaybackService,后台和锁屏都有效 |
无障碍与性能
好看之外的硬约束
状态不只靠颜色
平台徽标:完整是实心圆,试听是空心圆,VIP 是圆角方,没有音源加删除线。读屏合并为一句:“QQ 音乐完整,网易云 VIP,酷狗超时”。
字号缩放
支持到 200%。列表行用最小高度而不是固定高度;播放页在大字号下歌名换行,封面缩小让出空间。
触控目标
所有可点元素至少 48×48dp;迷你播放器的滑动手势同时提供按钮,读屏用户不需要手势。
取色性能
Coil 请求 128px、allowHardware(false) 的缩略图,在 Dispatchers.Default 上取色,按封面 URL 做 64 条 LRU 缓存。整首队列的封面在空闲时预取色。
模糊性能
每屏不超过 3 个模糊层;低内存设备(isLowRamDevice)和电视默认关闭实时模糊。
流动背景耗电
旋转的是一张 64px 的预模糊位图,GPU 开销很小;息屏、进入后台、省电模式或打开“减少动画”时停止转动并保持静止画面。
启动与滚动
Baseline Profile 覆盖冷启动、榜单滚动、打开播放页三条路径;用 Macrobenchmark 在一台低端机和一台电视盒子上测帧时间。
落地
依赖、代码结构和分期
Compose BOM 2026.09.00 带的 material3 1.4.0 没有可用的 Expressive API;1.5.0-alpha26 起又要求 minSdk 24。所以单独把 material3 固定在 1.5.0-alpha25,它是最后一个支持 Android 6.0 的版本,Expressive 组件基本齐全。
代价:这是 alpha 版,个别 API 名字和稳定版不同(例如 TonalToggleButton 在之后改名为 FilledTonalToggleButton)。所有 Expressive 组件只在 ui/components 里调用,等 1.5.0 稳定版发布时再评估是否升 minSdk,迁移只改这一层。
依赖(libs.versions.toml)
# 新增 / 调整
[versions]
material3Expressive = "1.5.0-alpha25" # 覆盖 BOM 里的 1.4.0;最后一个支持 minSdk 23 的版本
materialKolor = "6.0.0"
haze = "2.0.1"
[libraries]
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3Expressive" }
androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" }
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" }
material-kolor = { module = "com.materialkolor:material-kolor-material3", version.ref = "materialKolor" }
material-kolor-palette = { module = "com.materialkolor:material-kolor-palette", version.ref = "materialKolor" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-blur = { module = "dev.chrisbanes.haze:haze-blur", version.ref = "haze" }
haze-blur-materials = { module = "dev.chrisbanes.haze:haze-blur-materials", version.ref = "haze" }
MaterialKolor 6.0 依赖 kotlin-stdlib 2.4.21,会把项目的 2.4.20 顺带升一个补丁版本。它通过 JetBrains 的重定向依赖 material3 1.5.0-alpha22,Gradle 会解析到 alpha25,再用 strictly 固定。Haze 2.x 的 API 与 1.x 不同:HazeStyle → HazeBlurStyle,hazeEffect → hazeBlur,haze-materials → haze-blur-materials。
主题入口
// build.gradle.kts:防止传递依赖把 material3 拉到要求 minSdk 24 的版本
implementation(libs.androidx.compose.material3) { version { strictly("1.5.0-alpha25") } }
// MaterialKolor 6.0 编译时依赖 material3 1.5.0-alpha22,实际解析为 alpha25;已核对字节码引用的方法在 alpha25 中签名一致
@Composable
fun MyMusicTheme(appearance: Appearance, coverSeed: Color?, content: @Composable () -> Unit) {
val seed = when (appearance.colorSource) {
ColorSource.Cover -> coverSeed ?: appearance.lastSeed ?: BrandSeed
ColorSource.Wallpaper -> wallpaperSeed() ?: BrandSeed // Android 12+
ColorSource.Brand -> BrandSeed // #006B5F
ColorSource.Custom -> appearance.customSeed
}
val scheme = rememberDynamicColorScheme( // com.materialkolor.material3
seedColor = seed,
isDark = appearance.isDark(),
isAmoled = appearance.amoled,
style = appearance.browseStyle, // 默认 PaletteStyle.TonalSpot
contrastLevel = appearance.contrast, // 0.0 / 0.5 / 1.0
specVersion = ColorSpec.SpecVersion.SPEC_2025, // 显式传入,默认值不是 2025
)
MaterialExpressiveTheme( // alpha25 中为公开 API,无需 opt-in
colorScheme = if (appearance.reduceMotion) scheme else animateColorScheme(scheme),
motionScheme = if (appearance.reduceMotion) MotionScheme.standard() else MotionScheme.expressive(),
typography = AppTypography,
content = content,
)
}
// 播放页在内部再包一层:style = appearance.playerStyle(默认 Vibrant),isDark = true
没有用 DynamicMaterialExpressiveTheme:它默认 Expressive 风格,并且仍标注实验性。自己组合 rememberDynamicColorScheme + animateColorScheme + MaterialExpressiveTheme,参数完全可控。在 alpha25 上写 SplitButtonLayout 而不是 SplitButton(后者在 beta02 被删除),这样以后升级不用改代码。
封面取色
class CoverSeedRepository(private val context: Context, private val imageLoader: ImageLoader) {
private val cache = LruCache<String, Int>(64)
suspend fun seedFor(url: String): Color? {
cache[url]?.let { return Color(it) }
val request = ImageRequest.Builder(context).data(url).size(128).allowHardware(false).build()
val bitmap = (imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap() ?: return null
return withContext(Dispatchers.Default) {
bitmap.asImageBitmap().themeColorOrNull()
// 6.x 中已标记弃用、7.0 移除;届时换成 material-kolor-palette 的 Palette.themeColorOrNull()
}?.also { cache.put(url, it.toArgb()) }
}
}
模糊层
val hazeState = rememberHazeState()
val blur = LocalBlurPolicy.current // 自动 / 开 / 关,电视和低内存设备为关
Box {
LazyColumn(Modifier.hazeSource(hazeState), contentPadding = scaffoldPadding) { … }
MiniPlayer(
Modifier.hazeBlur(
HazeInput.Backdrop(hazeState),
style = HazeMaterials.regular(MaterialTheme.colorScheme.secondaryContainer),
).takeIf { blur.enabled } ?: Modifier.background(MaterialTheme.colorScheme.secondaryContainer),
)
}
代码结构
ui/theme/ | MyMusicTheme、Appearance、CoverSeedRepository、BlurPolicy、AppTypography(Roboto Flex 可下载字体) |
ui/components/ | MiniPlayer、PlayPauseButton、WavySeekBar、SourceChip、TrackRow、PlatformBadges、CoverArt、SettingsGroup、Loading。Expressive 的实验性 API 只在这里出现,升级时改一处 |
ui/adaptive/ | AppScaffold(断点 → 导航形态)、NowPlayingPane、BottomPlayerBar |
ui/tv/ | TvNavigationDrawer、Modifier.tvFocusable()、TvHero、AmbientPlayer、遥控器按键映射 |
data/AppStore | 新增设置键:配色来源、自选色、浏览页 / 播放页风格、对比度、纯黑背景、模糊策略 |
分期
主题地基
升级 material3 / 加入 MaterialKolor 与 Haze;重写
AppTheme.kt;封面取色仓库;模糊策略;外观设置新选项。完成后现有页面即可随封面变色。播放体验
悬浮迷你播放器;沉浸式播放页(铺满封面、流动背景、自动隐藏、封面手势、形变播放键、波浪进度、音源 chip);歌词状态与跳转;换源面板;共享元素转场和预测性返回。
浏览页
榜单、歌单、搜索、我的、歌单详情、设置分组列表;LoadingIndicator 与骨架屏;Snackbar 撤销;设置入口迁移。
大屏与电视
自适应 Scaffold 与断点;平板右侧窗格和底部播放条;电视抽屉导航、焦点样式、Hero 跟随、环境模式、遥控器映射。
打磨与验证
无障碍走查(TalkBack、200% 字号、高对比度);低端机与电视盒子帧时间;Baseline Profile;更新 README 的界面说明。
风险
Expressive API 还在 alpha
固定在 1.5.0-alpha25,稳定版日期未公布,MaterialShapes 和 LoadingIndicator 仍需 @ExperimentalMaterial3ExpressiveApi。统一封装在 ui/components,并固定版本号。
每首歌换色会不会晃眼
浏览页用低彩度的 TonalSpot,只有播放页用 Vibrant;过渡 450ms;设置里可以改成品牌色或自选色,彻底固定。
电视盒子性能
默认关闭实时模糊和封面旋转;焦点放大用 graphicsLayer,不触发重新布局。
改动面大
按上面五期推进,每期结束都是可发布状态;P1 只换主题不动布局,风险最低,可以先合入。