# 音乐卡片设计 ## 背景 当前项目已经具备基础播放控制抽象: - `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 种卡片 这样既能尽快落地普通卡、中等卡、歌词卡,又能与当前播放器解耦方向保持一致,避免后续返工。