2026-04-02-music-card-design.md 13 KB

音乐卡片设计

背景

当前项目已经具备基础播放控制抽象:

  • MusicPlaybackController 负责统一播放动作入口。
  • PlaybackCoordinator 负责将控制动作转发到实际播放运行时。
  • PlaybackStateBridge 已经维护当前歌曲、队列、索引、播放状态、进度与时长。

但项目中还没有音乐卡片能力,entry/src/main/module.json5 也尚未声明 form 类型扩展能力。

参考实现里有两条路线:

  • KDVideo:单一音乐播放卡片体系,使用 FormExtensionAbility + preferences 持久化快照 + updateForm 主动更新
  • musicCard 官方样例:多类型卡片平台,使用数据库维护卡片实例,并承载播放、歌词、推荐等多类卡片。

结合当前项目结构与近期播放器控制收敛方向,首版音乐卡片应优先复用 KDVideo 的轻量链路,而不是引入官方样例那种重型卡片平台。

本阶段目标

新增 3 种音乐卡片:

  1. 普通播放卡片
  2. 中等播放卡片
  3. 歌词卡片

完成标准:

  • 用户可从桌面添加上述 3 种卡片。
  • 三种卡片都能显示当前播放歌曲的封面、标题、歌手和播放状态。
  • 普通卡片和中等卡片支持上一首、播放暂停、下一首。
  • 歌词卡片支持封面、两行歌词、上一首、播放暂停、下一首。
  • 点击卡片主体可回到应用并打开当前播放器视图。
  • 卡片在歌曲切换、播放暂停、进度推进时能同步刷新。
  • 无歌词时,歌词卡片自动回退显示歌名和歌手,不出现空白。

非目标:

  • 首版不做拖动进度条。
  • 首版不做收藏、喜欢、推荐歌单等扩展能力。
  • 首版不做整页滚动歌词,仅展示两行歌词。
  • 首版不引入卡片数据库,不建设统一多业务卡片平台。
  • 首版不处理锁屏卡片。

方案对比

方案一:轻量单链路卡片方案

做法:

  • 新增一个 FormExtensionAbility
  • 三种卡片共用一套播放卡片快照和动作协议。
  • 通过 preferences 保存快照和 formId 列表。
  • 播放状态变化时主动 updateForm

优点:

  • 与当前项目现状最匹配。
  • 对现有播放架构侵入较小。
  • 后续继续推进播放器控制层解耦时不会推翻重来。

缺点:

  • 需要补齐卡片基础设施与一层卡片数据适配。

方案二:直接依赖 LocalMusic 页面状态

做法:

  • 让卡片更新逻辑尽量绑定 LocalMusic 页面内部字段和生命周期。

优点:

  • 初看接入路径最短。

缺点:

  • 页面耦合过重。
  • 一旦 LocalMusic 生命周期变化或后续继续拆播放器,卡片很容易失效。
  • 不利于卡片成为稳定的系统级入口。

方案三:官方样例式多卡片平台

做法:

  • 参考 musicCard 样例建设数据库、统一工具类、多类卡片管理框架。

优点:

  • 扩展性最好。

缺点:

  • 对当前项目明显过重。
  • 第一版投入和改动面过大。
  • 会把“先把播放器卡片做出来”这个目标拖慢。

推荐方案

采用方案一,即“轻量单链路卡片方案”。

核心原则:

  • 控制动作统一走 MusicPlaybackController
  • 卡片展示数据统一由一套独立的播放卡片快照提供。
  • 卡片实例管理通过 FormExtensionAbility 完成。
  • 不把卡片直接绑定到 LocalMusic 页面生命周期。

目标架构

1. FormExtensionAbility

新增 MusicCardFormAbility,负责:

  • onAddForm 时返回初始卡片数据。
  • 记录 formId。
  • onUpdateForm 时按最新快照刷新卡片。
  • onFormEvent 时处理卡片发回的动作。
  • onRemoveForm 时清理失效 formId。

2. MusicCardManager

新增统一卡片管理器,负责:

  • 从当前播放状态构建卡片快照。
  • 将卡片快照转换为三种卡片共用的 binding data。
  • 主动更新所有已注册 form。
  • 节流歌词卡片刷新频率。
  • 处理 form 更新失败后的 formId 清理。

它是卡片层的唯一更新入口。

3. MusicCardSnapshotStore

使用 preferences 持久化卡片快照,保证:

  • 卡片首次添加时能拿到最近一次播放状态。
  • 应用未完全拉起时仍能显示上次有效内容。

4. MusicCardFormStore

使用 preferences 保存 formId 列表,负责:

  • 新增 formId
  • 删除 formId
  • 读取当前所有 formId

5. 现有播放层的职责

PlaybackStateBridge 继续承担基础播放状态桥接职责:

  • currentSong
  • currentQueue
  • currentQueueIndex
  • isPlaying
  • positionMs
  • durationMs

LocalMusic 仍然负责真实播放过程中的状态推进和歌词解析,但它不直接成为卡片宿主。

卡片侧通过 MusicCardManager 从现有播放状态中派生专用显示数据。

建议文件布局

建议新增以下文件:

  • entry/src/main/ets/entryformability/MusicCardFormAbility.ets
  • entry/src/main/ets/common/player/MusicCardConstants.ets
  • entry/src/main/ets/common/player/MusicCardSnapshotStore.ets
  • entry/src/main/ets/common/player/MusicCardFormStore.ets
  • entry/src/main/ets/common/player/MusicCardManager.ets
  • entry/src/main/ets/common/player/MusicCardSnapshot.ets
  • entry/src/main/ets/widget/pages/MusicPlayerWidgetCard.ets
  • entry/src/main/ets/widget/pages/MusicPlayerWidgetWideCard.ets
  • entry/src/main/ets/widget/pages/MusicPlayerWidgetLyricCard.ets
  • entry/src/main/resources/base/profile/form_config.json

需要修改的现有文件:

  • entry/src/main/module.json5
  • entry/src/main/ets/entryability/EntryAbility.ets
  • entry/src/main/ets/view/LocalMusic.ets
  • 可能还包括现有歌词工具类或封面工具类的少量扩展

卡片数据模型

建议新增一份专门的卡片快照,不直接暴露播放器内部所有字段。

建议字段:

  • title
  • artist
  • coverPath
  • hasCoverImage
  • isPlaying
  • hasSong
  • currentPositionMs
  • durationMs
  • currentTimeText
  • durationTimeText
  • lyricLine1
  • lyricLine2
  • hasLyric
  • playType
  • filePath
  • updatedAtMs

字段原则:

  • 卡片只拿自己需要的显示字段。
  • 不把完整队列、复杂页面状态、动画状态塞进卡片快照。
  • 歌词卡片展示只保留两行,避免把整份歌词文本塞进卡片层。

三种卡片设计

1. 普通播放卡片

定位:

  • 紧凑型播放控制卡片。

展示内容:

  • 小封面
  • 歌名
  • 歌手
  • 上一首
  • 播放暂停
  • 下一首

交互:

  • 点卡片主体:打开应用并进入播放器
  • 点按钮:直接控制播放

说明:

  • 第一版不展示进度条。
  • 第一版不展示歌词。

2. 中等播放卡片

定位:

  • 信息更完整的播放控制卡片。

展示内容:

  • 更大的封面
  • 歌名
  • 歌手
  • 可选时间信息或播放状态文案
  • 上一首
  • 播放暂停
  • 下一首

交互:

  • 与普通卡片一致

说明:

  • 第一版与普通卡片共用同一套动作与数据协议。
  • 差异主要体现在布局和视觉密度。

3. 歌词卡片

定位:

  • 增强版播放控制卡片。

展示内容:

  • 封面
  • 当前歌词
  • 下一句歌词
  • 上一首
  • 播放暂停
  • 下一首

歌词规则:

  • 有逐行歌词时:显示当前行和下一行。
  • 只有当前行没有下一行时:第二行留空或回退为歌手名。
  • 无歌词时:回退显示歌名和歌手。

说明:

  • 首版不做多行滚动歌词。
  • 首版不做歌词区域滚动动画。
  • 重点保证“准、稳、不卡”。

歌词更新策略

歌词卡片是本方案最容易引发频繁刷新的部分,必须节流。

推荐策略:

  • 播放进度仍由 LocalMusic 在现有链路里持续推进。
  • LocalMusic 在推进进度后,调用 MusicCardManager 的节流更新入口。
  • MusicCardManager 基于当前歌曲和当前进度,解析出“当前行 + 下一行”。
  • 只有歌词行内容发生变化、或播放态发生变化、或歌曲发生变化时,才实际调用 updateForm

建议节流频率:

  • 普通状态:800ms 到 1000ms 一次
  • 若歌词行未变化:不更新卡片

这样可以兼顾显示及时性和系统开销。

封面策略

封面优先级:

  1. 当前歌曲可用的本地封面路径
  2. 已缓存的缩略图路径
  3. 默认占位图

建议复用现有封面能力:

  • CoverThumbCache
  • 当前歌曲已有的 pixelMapPath 或相关封面字段

首版原则:

  • 本地和已缓存封面优先
  • 不在卡片层新建复杂下载流程
  • 远端封面若无法稳定直出,则先回退占位图

卡片动作协议

三种卡片统一使用一套动作常量。

建议动作:

  • play_pause
  • prev_song
  • next_song
  • open_player
  • sync_register

建议参数:

  • formId
  • action
  • source

协议规则:

  • 控制按钮通过 postCardAction(call) 回到应用。
  • 主体点击通过 postCardAction(router) 或统一路由动作打开播放器。
  • sync_register 用于卡片出现时补登记 formId,避免首次添加或恢复后 formId 丢失。

动作回流链路

推荐链路:

  1. 卡片按钮触发 postCardAction
  2. EntryAbility 接收 call 事件
  3. EntryAbility 解析动作
  4. EntryAbility 调用 MusicPlaybackController
  5. 控制器驱动当前播放运行时执行动作
  6. 播放状态变化后回写 PlaybackStateBridge
  7. MusicCardManager 刷新所有卡片

主体点击链路:

  1. 卡片发送 open_player
  2. EntryAbility 设置打开播放器所需状态或触发既有事件
  3. 应用拉起后展示当前播放器视图

与当前项目的集成点

1. module.json5

需要新增一个 extensionAbilities 节点:

  • 名称建议为 MusicCardFormAbility
  • 类型为 form
  • 元数据指向 $profile:form_config

2. form_config.json

需要声明 3 个 form:

  • 普通播放卡片
  • 中等播放卡片
  • 歌词卡片

建议都设置为动态卡片,关闭定时更新,走主动刷新:

  • isDynamic: true
  • updateEnabled: false

原因:

  • 音乐卡片依赖实时播放状态
  • 主动刷新比定时刷新更准确

3. EntryAbility

需要增加:

  • 卡片动作 call handler 注册
  • open_player 路由处理
  • 必要时对 formId 的兜底登记

4. LocalMusic

需要增加:

  • 在歌曲切换、播放暂停、进度推进时调用 MusicCardManager
  • 在歌词行变化时推动歌词卡片刷新

不建议增加:

  • 不把卡片逻辑直接写成大段页面内分支
  • 不让 LocalMusic 自己维护 formId 列表

分阶段实施建议

阶段一:打通基础卡片链路

目标:

  • 新增 FormExtensionAbility
  • 新增 form_config.json
  • 跑通普通播放卡片
  • 跑通按钮控制和打开播放器

验收:

  • 可添加普通卡片
  • 点击上一首、播放暂停、下一首生效
  • 切歌后卡片自动刷新

阶段二:补齐中等播放卡片

目标:

  • 复用同一套数据模型和动作协议
  • 完成中等卡布局

验收:

  • 中等卡片可正常显示和控制
  • 与普通卡共用同一套更新链路

阶段三:补齐歌词卡片

目标:

  • 打通歌词两行提取
  • 实现节流更新
  • 实现歌词缺失回退

验收:

  • 歌词卡片随播放进度切换两行歌词
  • 无歌词时稳定显示歌名和歌手
  • 更新频率稳定,不造成明显卡顿

阶段四:稳定性清理

目标:

  • 清理无效 formId
  • 处理卡片恢复、应用重启、播放中断等场景
  • 统一日志与错误兜底

验收:

  • 移除卡片后不会持续报错
  • 应用重启后卡片能显示最近状态

风险与应对

1. 歌词刷新过频

风险:

  • updateForm 调用过于频繁,影响性能和稳定性。

应对:

  • 对歌词卡片做节流。
  • 仅在歌词行变化时刷新。

2. 封面来源不稳定

风险:

  • 远端或临时路径封面在卡片中不可直接显示。

应对:

  • 首版优先本地缓存图或缩略图。
  • 无法稳定获取时回退占位图。

3. 播放器状态与卡片状态分裂

风险:

  • 页面内局部状态和卡片快照来源不一致。

应对:

  • 卡片只认 MusicCardManager 构建的统一快照。
  • 不允许三种卡片各自维护不同的数据来源。

4. LocalMusic 耦合仍偏重

风险:

  • 卡片更新时机如果分散写在 LocalMusic 多处,后续维护成本会很高。

应对:

  • 抽一个集中入口,例如 notifyPlaybackCardStateChanged()
  • 歌曲切换、播放态切换、进度推进都收敛到统一通知方法。

测试与验证

首版应重点做真机冒烟验证。

必须验证:

  • 添加三种卡片是否成功
  • 普通卡片播放控制是否正常
  • 中等卡片播放控制是否正常
  • 歌词卡片是否显示两行歌词
  • 无歌词歌曲是否正确回退
  • 切歌后封面、标题、歌手是否同步
  • 暂停恢复后卡片状态是否同步
  • 点击卡片主体是否能进入播放器
  • 删除卡片后是否仍有 form 更新报错
  • 应用重启后卡片是否能显示最近一次播放状态

结论

本项目的音乐卡片应采用“KDVideo 轻量链路 + 当前项目播放控制抽象”的组合方案:

  • 不直接照搬官方样例的重型多卡片平台
  • 不把卡片强绑到 LocalMusic 页面生命周期
  • 先用一套统一快照和统一动作协议支撑 3 种卡片

这样既能尽快落地普通卡、中等卡、歌词卡,又能与当前播放器解耦方向保持一致,避免后续返工。