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