LocalMusic 播放控制与播放页拆分设计
背景
当前播放器相关能力主要堆叠在 entry/src/main/ets/view/LocalMusic.ets 中,形成了几个明显问题:
LocalMusic.ets 同时承担媒体库列表、分页加载、播放控制、播放页 UI、歌词、播放列表弹层、详情、编辑标签、均衡器、更多菜单等职责,文件过大,修改风险高。
- 迷你播放条的显示状态已经在
entry/src/main/ets/pages/NewIndex.ets 上层维护,但播放页实际仍由 LocalMusic.ets 内部 bindContentCover 托管,导致播放页生命周期和媒体库页面深度耦合。
- 歌曲详情、编辑标签这类能力既被媒体库列表使用,也被播放页使用,但当前实现散落在
LocalMusic.ets 私有状态中,复用边界不清晰。
- 重型播放状态与超大列表页面常驻耦合,容易造成状态膨胀、页面驻留成本过高,不利于后续排查内存占用和发热问题。
用户希望本轮先完成播放器重构,再在第二阶段实现桌面音乐卡片;并明确要求:
- 播放页不要再走
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。
原因:
- 它是本轮唯一同时满足“无感拆分”“不走 router”“整体搬迁现有播放页能力”“后续支持音乐卡片”的方案。
- 播放页 UI 是否拆文件不是核心,核心是播放域状态从
LocalMusic.ets 脱钩;方案 B 能真正做到这一点。
- 通过先抽公共组件,再迁播放页容器,可以在尽量保持现有交互的前提下逐步落地,风险可控。
设计
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. 编辑与详情回写
歌曲详情、编辑标签组件关闭后,需要通过回调完成:
- 当前歌曲信息同步。
- 媒体库列表项刷新。
- 播放页展示信息刷新。
- 必要的歌词、封面、标签文本同步。
回写逻辑放在控制层或调用方,不写死在公共组件内部。
性能与内存优化方向
本轮不承诺一次性彻底解决所有发热问题,但要明确消减以下高风险点:
- 将播放页容器从
LocalMusic.ets 移出,减少媒体库超大页面与完整播放页 UI 的常驻绑定。
- 将播放控制、歌词控制、播放页临时动画状态从媒体库列表域剥离,降低页面级状态数量。
- 公共弹层组件按需创建,避免详情、编辑标签逻辑继续深埋在超大页面内部。
- 为第二阶段音乐卡片复用播放器控制层,避免未来再次从
LocalMusic.ets 复制逻辑。
风险与处理
风险 1:播放页拆出后交互回归
处理:
- 播放页 UI 以“整体搬迁”为原则,优先保持现有结构与行为。
- 顶层只改变承载位置,不主动改视觉和交互语义。
风险 2:播放控制迁移时共享状态丢失
处理:
- 第一版继续复用现有
AppStorage 键,降低联动改造范围。
- 事件入口保持兼容,优先确保迷你播放条、列表页、发现页不被破坏。
风险 3:详情/编辑标签拆分后刷新链路断裂
处理:
- 在组件接口中显式定义保存成功回调、关闭回调、当前歌曲回写回调。
- 先围绕当前使用点完成最小闭环,不做过度抽象。
风险 4:发热问题不能立刻完全改善
处理:
- 本轮先消除最明显的结构性耦合点。
- 实现后重点回归连续播放、多次打开关闭播放页、媒体库长驻场景,观察驻留与交互流畅度。
验证
手工验证
- 从迷你播放条点击进入播放页,确认通过
bindContentCover 打开独立播放页。
- 播放页内播放、暂停、上一首、下一首、拖动进度、歌词切换、播放列表打开行为与重构前保持一致。
- 从播放页打开“详情”“编辑标签”,确认组件正常展示,并能正确保存和回写当前歌曲信息。
- 从媒体库列表长按歌曲打开“详情”“编辑标签”,确认与播放页复用同一套组件,行为正常。
- 播放页关闭后,迷你播放条状态保持正常,返回键可优先关闭播放页。
- 连续播放多首歌曲,反复打开关闭播放页,确认没有明显卡顿、异常状态残留或播放器失联。
回归验证
- 发现页、远程页、迷你播放条仍可正常控制播放。
LocalMusic.ets 分页列表、排序、详情页切换行为不受影响。
- 当前播放列表总数、歌曲切换、封面和歌词展示保持正确。
自动化验证
- 如本轮新增了可纯逻辑测试的播放控制辅助类,为其补充对应单测。
- 当前仓库若受本地 DevEco / SDK 环境限制无法完成可靠编译,需要在实现阶段明确记录未完成项。