2026-03-31-localmusic-player-refactor-design.md 12 KB

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、播放页动画状态、播放列表弹层状态、编辑标签状态等重型对象和状态。
  • 播放相关事件目前主要通过 eventHubLocalMusic.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 的顶层拥有者。
  • 将播放页的 bindContentCoverLocalMusic.ets 移除,迁移到 NewIndex.ets
  • NewIndex.ets 负责:
    • 迷你播放条点击后打开播放页。
    • 返回键优先关闭播放页。
    • 承载独立播放页组件。

这样可以保证播放页是全局层能力,而不是媒体库子页面内部能力。

2. 播放控制层边界

新增独立播放控制层,统一承接以下职责:

  • 持有或封装 IjkMediaPlayer 实例。
  • 处理播放、暂停、上一首、下一首、切歌、seek、倍速、循环模式、随机模式。
  • 维护播放进度、总时长、当前时间、缓冲态、播放状态。
  • 管理歌词控制器、歌词同步、单行歌词与播放页歌词展示所需状态。
  • 处理播放相关 eventHub 事件注册与派发。
  • 同步 AppStorage 中仍需对外共享的关键状态,如 currentSongsongListcurrIndexisPlaying

该层不负责媒体库列表分页,不直接承担大段页面 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 环境限制无法完成可靠编译,需要在实现阶段明确记录未完成项。