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```