• Home
Name Date Size #Lines LOC

..--

AppScope/06-May-2025-3432

entry/06-May-2025-559520

hvigor/06-May-2025-3636

musicplayer/06-May-2025-2,7102,464

.gitignoreD06-May-2025133 1212

README.mdD06-May-202510.8 KiB291245

build-profile.json5D06-May-20251.4 KiB5959

code-linter.json5D06-May-2025957 3434

hvigorfile.tsD06-May-2025842 215

oh-package.json5D06-May-2025808 2524

ohosTest.mdD06-May-20254.2 KiB2018

README.md

1# 折叠屏音乐播放器案例
2
3### 介绍
4
5本示例介绍使用ArkUI中的容器组件FolderStack在折叠屏设备中实现音乐播放器场景,展示当前播放歌曲信息,支持播控中心控制播放和后台播放能力。
6
7### 效果图预览
8
9<img src="musicplayer/music_player.gif" width="400">
10
11**使用说明**
12
131. 播放器预加载了歌曲列表,即开即用;
142. 支持播放模式切换、播放、暂停、重新播放、上一首、下一首操作;
153. 支持歌词展示、随歌曲滚动;
164. 支持歌曲进度拖拽;
175. 在折叠屏上,支持横屏半展开态下的组件自适应动态布局;
186. 支持退到后台播放音乐,由播控中心控制操作和拉起应用;
19
20### 实现思路
21
221. 采用MVVM模式进行架构设计,目录结构中区分展示层、模型层、控制层,展示层通过控制层与模型层沟通,展示层的状态数据与控制层进行双向绑定,模型层的变更通过回调形式通知给控制层,并最终作用于展示层。
23
242. 在可折叠设备上使用FolderStack组件作为容器组件,承载播放器的所有功能组件,在半折叠态上,使需要移动到上屏的子组件产生相应的动态效果。
25```typescript
26// TODO:知识点:FolderStack继承于Stack控件,通过upperItems字段识别指定id的组件,自动避让折叠屏折痕区后移到上半屏
27FolderStack({ upperItems: [CommonConstants.FOLDER_STACK_UP_COMP_ID] }) {
28    MusicPlayerInfoComp({ musicModel: this.musicModel, curFoldStatus: this.curFoldStatus })
29        .id(CommonConstants.FOLDER_STACK_UP_COMP_ID)
30    MusicPlayerCtrlComp({ musicModel: this.musicModel })
31}
32```
33
34源码请参考[MusicPlayerPage.ets](./musicplayer/src/main/ets/pages/MusicPlayerPage.ets)
35
363. 在需要移动到上屏的子组件上添加属性动效,当组件属性发生变更时,达成动态展示效果。
37```typescript
38Image(this.musicModel.coverRes)
39  .width(this.curImgSize)
40  .height(this.curImgSize)
41  .margin(20)
42  .animation(this.attrAniCfg)
43  .interpolation(ImageInterpolation.High)
44  .draggable(false)
45```
46
47源码请参考[MusicPlayerInfoComp.ets](./musicplayer/src/main/ets/components/MusicPlayerInfoComp.ets)
48
494. 折叠屏设备上,依赖display的屏幕状态事件,监听屏幕折叠状态变更,通过对折叠状态的分析,更新UI属性。
50```typescript
51display.on('foldStatusChange', (curFoldStatus: display.FoldStatus) => {
52    this.curFoldStatus = curFoldStatus;
53    this.windowModel.updateMainWinPreferredOrientation(curFoldStatus);
54})
55```
56
57源码请参考[MusicPlayerPage.ets](./musicplayer/src/main/ets/pages/MusicPlayerPage.ets)
58
595. 创建AVSession实例,注册AVSession实例事件,激活AVSession实例,接入系统播控中心
60```typescript
61// TODO:知识点:创建AVSession实例
62this.session = await AVSessionManager.createAVSession(this.bindContext!, this.avSessionTag, this.avSessionType);
63// TODO:知识点:注册AVSession事件
64await this.registerSessionListener(eventListener);
65// TODO:知识点:激活AVSession实例
66await this.session.activate();
67```
68
69源码请参考[AVSessionModel.ets](./musicplayer/src/main/ets/model/AVSessionModel.ets)
70
716. 设置AVSession实例元信息和播放状态
72```typescript
73// 设置必要的媒体信息
74let metadata: AVSessionManager.AVMetadata = {
75  assetId: '0', // 由应用指定,用于标识应用媒体库里的媒体
76  title: musicModel?.title,
77  mediaImage: imagePixel,
78  artist: musicModel?.singer,
79  duration: musicModel?.totalTime,
80  lyric: lrcStr
81}
82// TODO:知识点:设置AVSession元信息
83this.session?.setAVMetadata(metadata).then(() => {
84  logger.info(`SetAVMetadata successfully`);
85}).catch((err: BusinessError) => {
86  logger.error(`Failed to set AVMetadata. Code: ${err.code}, message: ${err.message}`);
87});
88
89
90// TODO:知识点:设置AVSession当前状态
91this.session?.setAVPlaybackState(this.curState, (err) => {
92  if (err) {
93    console.error(`Failed to set AVPlaybackState. Code: ${err.code}, message: ${err.message}`);
94  } else {
95    console.info(`SetAVPlaybackState successfully`);
96  }
97});
98```
99
100源码请参考[AVSessionModel.ets](./musicplayer/src/main/ets/model/AVSessionModel.ets)
101
1027. 创建WantAgent实例,确认后台任务类型,启动后台任务
103```typescript
104let wantAgentInfo: wantAgent.WantAgentInfo = {
105  // 点击通知后,将要执行的动作列表
106  // 添加需要被拉起应用的bundleName和abilityName
107  wants: [
108    {
109      bundleName: "com.north.cases",
110      abilityName: "com.north.cases.EntryAbility"
111    }
112  ],
113  // 指定点击通知栏消息后的动作是拉起ability
114  actionType: wantAgent.OperationType.START_ABILITY,
115  // 使用者自定义的一个私有值
116  requestCode: 0,
117  // 点击通知后,动作执行属性
118  actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
119};
120// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
121wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
122  // TODO:知识点:设置后台任务类型,启动后台任务
123  backgroundTaskManager.startBackgroundRunning(this.bindContext!,
124    backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
125    // 此处执行具体的长时任务逻辑,如放音等。
126    console.info(`Succeeded in operationing startBackgroundRunning.`);
127    this.isBackgroundTaskRunning = true;
128  }).catch((err: BusinessError) => {
129    console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
130  });
131});
132```
133
134源码请参考[AVSessionModel.ets](./musicplayer/src/main/ets/model/AVSessionModel.ets)
135
1368. 播控中心状态回传给AVPlayer,采用监听AVSession实例的事件进行
137```typescript
138// 播放
139this.session?.on('play', () => {
140  logger.info('avsession on play');
141  eventListener.onPlay();
142});
143// 暂停
144this.session?.on('pause', () => {
145  logger.info('avsession on pause');
146  eventListener.onPause();
147});
148// 停止
149this.session?.on('stop', () => {
150  logger.info('avsession on stop');
151  eventListener.onStop();
152});
153// 下一首
154this.session?.on('playNext', async () => {
155  logger.info('avsession on playNext');
156  eventListener.onPlayNext();
157});
158// 上一首
159this.session?.on('playPrevious', async () => {
160  logger.info('avsession on playPrevious');
161  eventListener.onPlayPrevious();
162});
163// 拖进度
164this.session?.on('seek', (position) => {
165  logger.info('avsession on seek', position.toString());
166  eventListener.onSeek(position);
167});
168// 标记喜好
169this.session?.on('toggleFavorite', (assetId) => {
170  logger.info('avsession on toggleFavorite', assetId);
171});
172// 播放循环模式切换
173this.session?.on('setLoopMode', (mode) => {
174  logger.info('avsession on setLoopMode', mode.toString());
175  eventListener.onSetLoopMode();
176});
177```
178
179源码请参考[AVSessionModel.ets](./musicplayer/src/main/ets/model/AVSessionModel.ets)
180
1819. 歌词滚动使用scrollToIndex接口实现,通过监听播放进度进行歌词滚动
182```typescript
183onViewModelChanged() {
184  const curMusiclyricsLine = this.viewModel.curMusiclyricsLine;
185  this.lyricsScrollerCtrl.scrollToIndex(curMusiclyricsLine, true, ScrollAlign.CENTER);
186}
187```
188
189源码请参考[MusicPlayerInfoComp.ets](./musicplayer/src/main/ets/components/MusicPlayerInfoComp.ets)
190
19110. 歌曲切换根据播放模式进行相应业务逻辑,单曲循环采用seek到歌曲开始处,列表循环索引加1,随机播放索引加随机数
192```typescript
193switch (this.curLoopMode) {
194  case AVSessionManager.LoopMode.LOOP_MODE_SINGLE: {
195    this.seek(0);
196    break;
197  }
198
199  case AVSessionManager.LoopMode.LOOP_MODE_LIST: {
200    this.curMusicModelIndex = (this.curMusicModelIndex + 1) % this.musicModelArr.length;
201    this.curMusicModelRaw = this.musicModelArr[this.curMusicModelIndex];
202    await this.reset();
203    await this.prepare(() => {
204      this.play();
205    });
206    this.avsessionModel?.setSessionInfo(this.curMusicModelRaw);
207    break;
208  }
209
210  case AVSessionManager.LoopMode.LOOP_MODE_SHUFFLE: {
211    const randomVal: number = Decimal.random(1).e;
212    let dieta: number = 1;
213    while (dieta < this.musicModelArr.length - 1) {
214      if (randomVal >= dieta - 1 / this.musicModelArr.length - 1 && randomVal < dieta / this.musicModelArr.length - 1) {
215        break;
216      }
217      dieta++;
218    }
219    this.curMusicModelIndex = (this.curMusicModelIndex + dieta) % this.musicModelArr.length;
220    this.curMusicModelRaw = this.musicModelArr[this.curMusicModelIndex];
221    await this.reset();
222    await this.prepare(() => {
223      this.play();
224    });
225    this.avsessionModel?.setSessionInfo(this.curMusicModelRaw);
226    break;
227  }
228}
229```
230
231源码请参考[MusicPlayViewModel.ets](./musicplayer/src/main/ets/viewmodel/MusicPlayViewModel.ets)
232
233### 高性能知识点
234
235暂无
236
237### 工程结构&模块类型
238
239   ```
240   foldablescreencases                  // har类型
241   |---common
242   |   |---constants
243   |   |    |---CommonConstants.ets     // 通用常量
244   |---components
245   |   |---MusicPlayerCtrlComp.ets      // 自定义组件-音乐播放器控制栏
246   |   |---MusicPlayerInfoComp.ets      // 自定义组件-音乐播放器歌曲详情展示
247   |---model
248   |   |---AVPlayerModel.ets            // 模型层-音频播放管理器
249   |   |---AVSessionModel.ets           // 模型层-音频会话管理器
250   |   |---MusicModel.ets               // 模型层-音乐歌曲数据模型
251   |   |---WindowModel.ets              // 模型层-窗口管理器
252   |---pages
253   |   |---MusicPlayerPage.ets          // 展示层-音乐播放器
254   |---viewmodel
255   |   |---MusicPlayerViewModel.ets     // 控制层-音乐播放器控制器
256   ```
257
258### 模块依赖
259
260依赖本地的utils模块
261
262### 参考资料
263
264- [FolderStack](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-folderstack.md)
265- [属性动画](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-animatorproperty.md)
266- [AVPlayer](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/media/media/using-avplayer-for-playback.md)
267- [状态管理概述](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/quick-start/arkts-state-management-overview.md)
268- [AVSession](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/media/avsession/avsession-access-scene.md)
269
270### 相关权限
271
272[ohos.permission.KEEP_BACKGROUND_RUNNING](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/security/AccessToken/permissions-for-all.md#ohospermissionkeepbackgroundrunning)
273
274### 约束与限制
275
2761.本示例仅支持在标准系统上运行,支持设备:折叠屏。
277
2782.本示例为Stage模型,支持API12版本SDK,SDK版本号(API Version 12 Release)。
279
2803.本示例需要使用DevEco Studio 5.0.0 Release 才可编译运行。
281
282### 下载
283
284如需单独下载本工程,执行如下命令:
285```javascript
286git init
287git config core.sparsecheckout true
288echo /code/SystemFeature/Media/MusicPlayer/ > .git/info/sparse-checkout
289git remote add origin https://gitee.com/openharmony/applications_app_samples.git
290git pull origin master
291```