瀏覽代碼

docs(player): 补充播放器拆分设计

Codex 4 月之前
父節點
當前提交
c04ef9bfe3
共有 1 個文件被更改,包括 294 次插入0 次删除
  1. 294 0
      docs/superpowers/specs/2026-03-31-localmusic-player-refactor-design.md

+ 294 - 0
docs/superpowers/specs/2026-03-31-localmusic-player-refactor-design.md

@@ -0,0 +1,294 @@
+# LocalMusic 播放控制与播放页拆分设计
+
+## 背景
+
+当前播放器相关能力主要堆叠在 `entry/src/main/ets/view/LocalMusic.ets` 中,形成了几个明显问题:
+
+1. `LocalMusic.ets` 同时承担媒体库列表、分页加载、播放控制、播放页 UI、歌词、播放列表弹层、详情、编辑标签、均衡器、更多菜单等职责,文件过大,修改风险高。
+2. 迷你播放条的显示状态已经在 `entry/src/main/ets/pages/NewIndex.ets` 上层维护,但播放页实际仍由 `LocalMusic.ets` 内部 `bindContentCover` 托管,导致播放页生命周期和媒体库页面深度耦合。
+3. 歌曲详情、编辑标签这类能力既被媒体库列表使用,也被播放页使用,但当前实现散落在 `LocalMusic.ets` 私有状态中,复用边界不清晰。
+4. 重型播放状态与超大列表页面常驻耦合,容易造成状态膨胀、页面驻留成本过高,不利于后续排查内存占用和发热问题。
+
+用户希望本轮先完成播放器重构,再在第二阶段实现桌面音乐卡片;并明确要求:
+
+- 播放页不要再走 `router`,改用 `bindContentCover`。
+- 点击迷你播放条后弹出独立播放页。
+- 现有播放页能力尽量整体搬迁,不做缩水版。
+- 歌曲详情、编辑标签等需要复用的能力要抽成组件,供 `LocalMusic` 和播放页共用。
+- 本轮尽量不改现有交互和视觉效果,做无感拆分。
+
+## 目标
+
+- 将播放页容器从 `LocalMusic.ets` 迁移到 `NewIndex.ets` 上层,通过 `bindContentCover` 统一托管。
+- 将播放控制逻辑从 `LocalMusic.ets` 抽离为独立播放控制层,后续可被迷你播放条、播放页、桌面卡片复用。
+- 将歌曲详情、编辑标签等复用能力抽成独立组件,不再依赖 `LocalMusic.ets` 私有页面状态。
+- `LocalMusic.ets` 只保留媒体库、分页、列表、详情导航、列表入口等媒体库域职责。
+- 在不改变主要交互与视觉效果的前提下,降低 `LocalMusic.ets` 常驻状态复杂度,为内存占用和发热优化打基础。
+
+## 非目标
+
+- 本轮不实现桌面音乐卡片。
+- 本轮不把播放页改造成独立 Navigation 路由页面。
+- 本轮不重做播放器视觉设计,也不主动改变现有手势和动画语义。
+- 本轮不顺带大规模重构媒体库分页、远程歌曲、发现页播放逻辑,除非为接入新播放控制层所必需。
+
+## 现状分析
+
+### 1. 播放页宿主位置不对
+
+- `NewIndex.ets` 已通过 `@Provide isShowPlay` 持有播放页显示状态,并负责迷你播放条交互。
+- 但 `LocalMusic.ets` 内部仍使用 `bindContentCover($$this.isShowPlay, this.MusicPlayBuilder(), ...)` 托管播放页。
+- 结果是“全局播放页状态”由上层拥有,“播放页实例和生命周期”却绑在子页面里,边界分裂。
+
+### 2. 播放控制与媒体库页面耦合过深
+
+- `LocalMusic.ets` 内同时持有 `IjkMediaPlayer`、进度更新、歌词控制器、AVSession、播放页动画状态、播放列表弹层状态、编辑标签状态等重型对象和状态。
+- 播放相关事件目前主要通过 `eventHub` 与 `LocalMusic.ets` 通信,说明其他页面把它当作事实上的播放控制中心。
+- 这会导致后续任何播放能力扩展都必须回到 `LocalMusic.ets` 修改。
+
+### 3. 复用能力没有明确边界
+
+- 歌曲详情 `detailSheet(item)`、编辑标签 `editSheet(item)` 既服务于列表项长按,也服务于播放页更多功能。
+- 但它们依赖 `LocalMusic.ets` 的编辑字段、歌词临时状态、封面状态、保存回写流程。
+- 结果是播放页即使拆出单独组件,也无法真正摆脱 `LocalMusic.ets`。
+
+## 方案对比
+
+### 方案 A:只抽播放页 UI 文件
+
+做法:
+
+- 将 `MusicPlayBuilder()` 的 UI 拆到独立组件文件。
+- `IjkMediaPlayer`、歌词、播放控制、弹层状态继续保留在 `LocalMusic.ets`。
+
+优点:
+
+- 改动最小,上手最快。
+
+缺点:
+
+- 本质仍是“一个大页面 + 一个外置 UI 文件”,没有解决播放控制归属问题。
+- `LocalMusic.ets` 体量和状态复杂度下降有限。
+- 对内存、发热、后续音乐卡片复用帮助很小。
+
+### 方案 B:拆分“播放控制层 + 独立播放页 + 复用弹层组件”
+
+做法:
+
+- `NewIndex.ets` 成为播放页唯一宿主,通过 `bindContentCover` 托管独立播放页组件。
+- 抽出独立播放控制层,统一管理播放器实例、进度、播放行为、播放事件、歌词同步等播放域能力。
+- 将歌曲详情、编辑标签等复用能力抽成组件,列表页和播放页共享。
+
+优点:
+
+- 边界清晰,能真正降低 `LocalMusic.ets` 复杂度。
+- 能保持当前交互外观基本不变,同时为音乐卡片和其他播放入口留出复用接口。
+- 能把常驻重型播放状态从媒体库大页面中拆出,优化方向正确。
+
+缺点:
+
+- 改动面较大,需要梳理当前播放页依赖。
+- 需要在短期内同时处理状态迁移和组件复用。
+
+### 方案 C:直接改成独立 Navigation 页面
+
+做法:
+
+- 新建独立播放页路由或 Navigation 目的页。
+- 所有播放页打开动作统一改为页面跳转。
+
+优点:
+
+- 从页面结构上最彻底。
+
+缺点:
+
+- 与当前用户要求不符。
+- 会改变返回链路和页面层级,回归风险高。
+- 不适合这次“无感拆分”的目标。
+
+## 推荐方案
+
+本轮采用方案 B。
+
+原因:
+
+1. 它是本轮唯一同时满足“无感拆分”“不走 router”“整体搬迁现有播放页能力”“后续支持音乐卡片”的方案。
+2. 播放页 UI 是否拆文件不是核心,核心是播放域状态从 `LocalMusic.ets` 脱钩;方案 B 能真正做到这一点。
+3. 通过先抽公共组件,再迁播放页容器,可以在尽量保持现有交互的前提下逐步落地,风险可控。
+
+## 设计
+
+### 1. 顶层承载结构
+
+- `NewIndex.ets` 持续作为 `isShowPlay` 的顶层拥有者。
+- 将播放页的 `bindContentCover` 从 `LocalMusic.ets` 移除,迁移到 `NewIndex.ets`。
+- `NewIndex.ets` 负责:
+  - 迷你播放条点击后打开播放页。
+  - 返回键优先关闭播放页。
+  - 承载独立播放页组件。
+
+这样可以保证播放页是全局层能力,而不是媒体库子页面内部能力。
+
+### 2. 播放控制层边界
+
+新增独立播放控制层,统一承接以下职责:
+
+- 持有或封装 `IjkMediaPlayer` 实例。
+- 处理播放、暂停、上一首、下一首、切歌、seek、倍速、循环模式、随机模式。
+- 维护播放进度、总时长、当前时间、缓冲态、播放状态。
+- 管理歌词控制器、歌词同步、单行歌词与播放页歌词展示所需状态。
+- 处理播放相关 `eventHub` 事件注册与派发。
+- 同步 `AppStorage` 中仍需对外共享的关键状态,如 `currentSong`、`songList`、`currIndex`、`isPlaying`。
+
+该层不负责媒体库列表分页,不直接承担大段页面 UI 构建。
+
+### 3. 播放页组件边界
+
+新增独立播放页组件 `PlayerPage.ets`。
+
+该组件负责:
+
+- 承载现有播放页完整视觉结构与交互。
+- 处理下拉关闭、上滑切歌、封面/歌词切换、底部控制区、更多菜单、播放列表入口等 UI 行为。
+- 通过播放控制层读取和修改播放状态。
+
+该组件不再自己初始化播放器核心能力,也不直接承担媒体库页面职责。
+
+### 4. 公共复用组件边界
+
+本轮至少抽出以下复用组件:
+
+- 歌曲详情组件:供列表页与播放页共用。
+- 编辑标签组件:供列表页与播放页共用。
+
+必要时可继续抽出:
+
+- 当前播放列表组件。
+- 播放页更多菜单中的独立弹层项。
+
+这些组件的原则是:
+
+- 组件只负责 UI 与交互组织。
+- 具体保存、刷新、同步逻辑通过参数和回调传入。
+- 不再直接耦合 `LocalMusic.ets` 私有字段命名和私有状态流。
+
+### 5. LocalMusic 保留职责
+
+重构后 `LocalMusic.ets` 只保留媒体库域能力:
+
+- 本地歌曲、艺术家、专辑、文件夹等列表加载与分页。
+- 列表页排序、筛选、详情页切换。
+- 列表项点击后发起播放请求。
+- 列表项长按时调起公共详情组件、公共编辑标签组件。
+- 媒体库页本身的显示与交互。
+
+`LocalMusic.ets` 不再负责:
+
+- 托管播放页 `bindContentCover`。
+- 独占播放控制中心角色。
+- 长期持有只为播放页服务的大量临时 UI 状态。
+
+## 数据与状态流
+
+### 1. 顶层状态
+
+- `isShowPlay` 继续由 `NewIndex.ets` 维护。
+- `NewIndex.ets` 通过 `bindContentCover` 决定独立播放页显示与关闭。
+
+### 2. 共享播放状态
+
+为降低一次性改动风险,本轮继续复用已有 `AppStorage` 关键键值:
+
+- `currentSong`
+- `songList`
+- `currIndex`
+- `isPlaying`
+
+这样可以先稳定迁移播放能力,不强行在本轮引入新的全局状态协议。
+
+### 3. 事件流
+
+现有迷你播放条、发现页等外部入口仍可暂时复用 `eventHub`,但事件的接收与处理从 `LocalMusic.ets` 转移到新的播放控制宿主。
+
+核心事件包括:
+
+- 打开播放页
+- 关闭播放页
+- 播放 / 暂停
+- 上一首 / 下一首
+- 打开当前播放列表
+
+后续如果需要,再逐步减少对 `eventHub` 的依赖。
+
+### 4. 编辑与详情回写
+
+歌曲详情、编辑标签组件关闭后,需要通过回调完成:
+
+- 当前歌曲信息同步。
+- 媒体库列表项刷新。
+- 播放页展示信息刷新。
+- 必要的歌词、封面、标签文本同步。
+
+回写逻辑放在控制层或调用方,不写死在公共组件内部。
+
+## 性能与内存优化方向
+
+本轮不承诺一次性彻底解决所有发热问题,但要明确消减以下高风险点:
+
+1. 将播放页容器从 `LocalMusic.ets` 移出,减少媒体库超大页面与完整播放页 UI 的常驻绑定。
+2. 将播放控制、歌词控制、播放页临时动画状态从媒体库列表域剥离,降低页面级状态数量。
+3. 公共弹层组件按需创建,避免详情、编辑标签逻辑继续深埋在超大页面内部。
+4. 为第二阶段音乐卡片复用播放器控制层,避免未来再次从 `LocalMusic.ets` 复制逻辑。
+
+## 风险与处理
+
+### 风险 1:播放页拆出后交互回归
+
+处理:
+
+- 播放页 UI 以“整体搬迁”为原则,优先保持现有结构与行为。
+- 顶层只改变承载位置,不主动改视觉和交互语义。
+
+### 风险 2:播放控制迁移时共享状态丢失
+
+处理:
+
+- 第一版继续复用现有 `AppStorage` 键,降低联动改造范围。
+- 事件入口保持兼容,优先确保迷你播放条、列表页、发现页不被破坏。
+
+### 风险 3:详情/编辑标签拆分后刷新链路断裂
+
+处理:
+
+- 在组件接口中显式定义保存成功回调、关闭回调、当前歌曲回写回调。
+- 先围绕当前使用点完成最小闭环,不做过度抽象。
+
+### 风险 4:发热问题不能立刻完全改善
+
+处理:
+
+- 本轮先消除最明显的结构性耦合点。
+- 实现后重点回归连续播放、多次打开关闭播放页、媒体库长驻场景,观察驻留与交互流畅度。
+
+## 验证
+
+### 手工验证
+
+1. 从迷你播放条点击进入播放页,确认通过 `bindContentCover` 打开独立播放页。
+2. 播放页内播放、暂停、上一首、下一首、拖动进度、歌词切换、播放列表打开行为与重构前保持一致。
+3. 从播放页打开“详情”“编辑标签”,确认组件正常展示,并能正确保存和回写当前歌曲信息。
+4. 从媒体库列表长按歌曲打开“详情”“编辑标签”,确认与播放页复用同一套组件,行为正常。
+5. 播放页关闭后,迷你播放条状态保持正常,返回键可优先关闭播放页。
+6. 连续播放多首歌曲,反复打开关闭播放页,确认没有明显卡顿、异常状态残留或播放器失联。
+
+### 回归验证
+
+- 发现页、远程页、迷你播放条仍可正常控制播放。
+- `LocalMusic.ets` 分页列表、排序、详情页切换行为不受影响。
+- 当前播放列表总数、歌曲切换、封面和歌词展示保持正确。
+
+### 自动化验证
+
+- 如本轮新增了可纯逻辑测试的播放控制辅助类,为其补充对应单测。
+- 当前仓库若受本地 DevEco / SDK 环境限制无法完成可靠编译,需要在实现阶段明确记录未完成项。