ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙NEXT API 12+从零开发音乐播放器保姆级教程

鸿蒙NEXT API 12+从零开发音乐播放器保姆级教程 如果你最近在折腾鸿蒙开发大概率和我一样被“HarmonyOS6”这个标题搞得很困惑——系统到底出到几了SDK用哪个版本API 12到底是什么意思我这次用 HarmonyOS NEXT SDK 5.0.0(12) 从零写了一个音乐播放器从工程创建到真机运行花了不到两周中间踩了不少坑。这篇文章把完整过程拆开讲从环境配置、入口加载、Tabs底部导航到AVPlayer音频播放、权限申请、后台播放和上架前要处理的签名全部按保姆级标准来。无论你是刚过鸿蒙应用开发基础认证的新手还是从 Android/iOS 转过来的老手照着这份教程都能把一个能用的播放器跑起来。1. 项目整体设计与开发环境准备1.1 先弄清楚你手上是哪个鸿蒙SDKHarmonyOS 的版本命名确实容易让人绕晕。这次写播放器我用的是 HarmonyOS NEXT SDK版本号 5.0.0(12)后面的(12)表示 API 12。网上搜“harmonyos next sdk”时会看到“api 12”这个说法意思是 API Level 12 及以上当前 DevEco Studio 中稳定可用的就是这套。标题写“HarmonyOS6”实际开发时你只需要认准 API 12 这条技术线不要纠结数字因为应用开发的API差异远比系统版本数字重要。选 API 12 有一个核心原因它把旧版 FA 模型彻底淘汰了强制走 Stage 模型应用入口、组件生命周期、权限管理逻辑都更统一。而且从 API 12 开始媒体播放相关的 AVPlayer 能力补齐了状态机、音效切换、DRM 保护等做音乐播放器刚刚好。如果你用旧 API 9 的示例代码去跑大概率会碰到WindowStage.loadContent不存在、Entry组件结构对不上这类问题。所以第一步老老实实装最新 SDK别折腾旧版本。1.2 DevEco Studio 环境搭建开发工具我用的 DevEco Studio 当前稳定版安装包在官网下载安装后打开配置 SDK。这里有个小坑SDK 管理器有时候不会自动勾选全部组件你至少要把HarmonyOS SDK、Toolchains和emulator装上。我自己第一次只装了 SDK结果创建工程后模拟器列表是空的重新去 SDK Manager 里补装System-image才解决。模拟器方面纯 UI 调试用本地模拟器没问题但做音频播放建议从一开始就准备真机。模拟器的音频链路和真机差别很大某些模拟器版本拿到AVPlayer后虽然状态正常但声音会延迟或完全没有输出。我在教程后面的排坑部分会细说。连接真机时用 USB 线连接后在 DevEco Studio 的Device File Manager里能看到设备如果看不到先在命令行执行hdc list targets确认手机开启了“开发者模式”和“USB调试”。1.3 决定用Stage模型和元服务工程创建时会让你选“Application”还是“Atomic Service”。前者就是普通应用后者是鸿蒙的元服务。音乐播放器这种工具类产品完全可以做成元服务用户即点即用不用完整安装。不过元服务的包名规则、体积限制和上架入口略有不同新手先创建 standard application 更稳妥。我这次先用普通应用把功能跑通后续如果要做“鸿蒙元服务”版本再复制一份工程改配置也不难。Stage 模型下一个应用至少包含entry模块模块内部有EntryAbility这就是你要加载的第一个页面入口。旧 FA 模型里MainAbility和pages/index的关联方式已经废弃了API 12 统一走windowStage.loadContent来加载。这一点必须理解清楚因为后面所有页面跳转、路由配置都建立在 Stage 模型的AbilityStage和UIAbility生命周期之上。2. 项目入口与主界面骨架2.1 从EntryAbility到WindowStage.loadContent打开工程后先看entry/src/main/ets/entryability/EntryAbility.ets。核心逻辑集中在onWindowStageCreate方法里完整代码大致长这样import { AbilityConstant, UIAbility, Want } from kit.AbilityKit; import { window } from kit.ArkUI; import { hilog } from kit.PerformanceAnalysisKit; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 应用级初始化可以放这里 } onWindowStageCreate(windowStage: window.WindowStage): void { // 加载主页面 windowStage.loadContent(pages/Index, (err) { if (err.code) { hilog.error(0x0000, MusicPlayer, 加载页面失败: %{public}s, JSON.stringify(err)); return; } hilog.info(0x0000, MusicPlayer, 主页面加载成功); }); } }这里最关键的是pages/Index这个字符串。它指向entry/src/main/ets/pages/Index.ets但路径里不需要写.ets后缀也不用写ets/pages前缀。如果你把页面文件放到二级目录如pages/player/Player.ets这里就要写pages/player/Player。我一开始没注意大小写写成pages/index结果加载时报错找不到页面因为文件名大小写必须完全一致。此外如果你想让播放页全屏沉浸可以在loadContent之前调用windowStage.getMainWindow()拿到窗口设置全屏。我在播放器里加了一个自定义背景色所以没有做系统状态栏适配直接让页面内容延伸到状态栏之外。具体代码是windowStage.getMainWindow((err, mainWindow) { if (!err) { mainWindow.setWindowLayoutFullScreen(true); } });注意setWindowLayoutFullScreen会隐藏系统状态栏但页面顶部你的控件需要自己避开状态栏高度否则时间、电量会压在标题上。我踩过这个坑后来干脆不做全屏只在Index页面正常显示播放页用 SafeArea 处理。2.2 用Tabs定制底部导航栏音乐播放器主界面一般有三个页签推荐页、歌单页、我的页。最直接的做法是用Tabs组件。HarmonyOS 的Tabs用法和 Android 的BottomNavigationView类似但更灵活barBuilder可以完全自定义底部栏。下面是我在Index.ets里使用的结构Entry Component struct Index { State currentTab: number 0; Builder tabBuilder(index: number, title: string, icon: ResourceStr) { Column() { Image(icon) .width(24) .height(24) .objectFit(ImageFit.Contain) Text(title) .fontSize(12) .fontColor(this.currentTab index ? #FF3B30 : #999999) } .justifyContentContentCenter() .width(100%) .height(100%) } build() { Tabs({ barPosition: BarPosition.End, index: this.currentTab }) { TabContent() { HomePage() } .tabBar(this.tabBuilder(0, 推荐, $r(app.media.ic_home))) TabContent() { PlaylistPage() } .tabBar(this.tabBuilder(1, 歌单, $r(app.media.ic_list))) TabContent() { ProfilePage() } .tabBar(this.tabBuilder(2, 我的, $r(app.media.ic_user))) } .scrollable(false) .barHeight(56) .onChange((index: number) { this.currentTab index; }) } }barPosition: BarPosition.End表示 TabBar 在底部这是音乐类 App 的标配。scrollable(false)禁止左右滑动切换防止用户误滑到其他页签。底部栏高度我设为56图标用$r(app.media.xxx)引用resources/base/media下的图片资源。这里有个细节Tabs的TabContent内不能直接放Builder只能放组件或自定义组件。我把首页、歌单页、我的页分别拆成了HomePage、PlaylistPage、ProfilePage三个子组件这样每个页面各自管理自己的状态主入口不膨胀。2.3 RelativeContainer和Flex布局怎么选做播放器界面时最常见的问题是卡片和列表项到底用哪种布局我的经验是需要“相对于父容器某条边或某个元素对齐”时用RelativeContainer需要“一行内水平排列多个元素”时用Flex两个都能用时就选简单直观的那个。推荐页的“正在播放”卡片我用RelativeContainer做了一个右上角播放按钮封面图、歌曲名、歌手、播放按钮散落在卡片里播放按钮要无论封面图尺寸怎样都保持在右上角。代码简化为RelativeContainer() { Image(this.currentSong.cover) .width(72) .height(72) .borderRadius(12) .alignRules({ center: { anchor: __container__, align: HorizontalAlign.Center }, middle: { anchor: __container__, align: VerticalAlign.Center } }) Text(this.currentSong.name) .fontSize(18) .fontWeight(FontWeight.Bold) .alignRules({ left: { anchor: img_cover, align: HorizontalAlign.End } }) Button(播放) .alignRules({ right: { anchor: __container__, align: HorizontalAlign.End }, bottom: { anchor: __container__, align: VerticalAlign.Bottom } }) } .width(100%) .height(120) .id(card_container)alignRules里的__container__是一个特殊锚点代表父容器。你给子组件设置id后其他组件也能以它为锚点。比如歌曲名如果要以封面图右侧对齐就可以写left: { anchor: img_cover, align: HorizontalAlign.End }。需要注意的是RelativeContainer要求被锚引的组件必须设置id而且必须先于引用它的组件在代码中出现否则编译期不会报错运行时会闪退。列表项就简单得多用Flex水平布局Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { Image(song.cover) .width(48) .height(48) .borderRadius(8) Column() { Text(song.name) .fontSize(16) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(song.artist) .fontSize(13) .fontColor(#999999) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Text(song.duration) .fontSize(12) .fontColor(#CCCCCC) } .width(100%) .padding({ left: 16, right: 16, top: 10, bottom: 10 })layoutWeight(1)就是我说的“权重”它让中间歌曲信息区域占满剩余空间右侧时长文本自动右对齐。Flex 在处理列表项时比 RelativeContainer 快得多因为不需要解析复杂的锚点关系。3. 音乐播放器核心功能实现3.1 用AVPlayer拉起音频播放鸿蒙 API 12 的媒体播放核心类是AVPlayer它在kit.MediaKit里。整个使用流程可以归纳为创建实例 → 设置资源 → prepare → play → 监听状态。我先封装了一个简单的PlayerManager用单例模式管理播放器避免页面销毁时播放中断。核心代码import { media } from kit.MediaKit; export class PlayerManager { private static instance: PlayerManager | null null; private avPlayer: media.AVPlayer | null null; private state: media.AVPlayerState media.AVPlayerState.IDLE; static getInstance(): PlayerManager { if (!PlayerManager.instance) { PlayerManager.instance new PlayerManager(); } return PlayerManager.instance; } async play(url: string) { if (!this.avPlayer) { this.avPlayer await media.createAVPlayer(); this.avPlayer.on(stateChange, (state: media.AVPlayerState) { this.state state; }); this.avPlayer.on(error, (err) { console.error(AVPlayer error: ${JSON.stringify(err)}); }); } this.avPlayer.url url; await this.avPlayer.prepare(); await this.avPlayer.play(); } pause() { this.avPlayer?.pause(); } playNext(url: string) { this.avPlayer?.reset(); this.avPlayer!.url url; this.avPlayer?.prepare(); this.avPlayer?.play(); } }这里有一个很重要的点AVPlayer不是一创建就能play的它有一个状态机状态包括idle、initialized、prepared、playing、paused、completed、stopped、released。设置url后进入initialized调用prepare()后进入prepared之后才能play()。强烈建议在 UI 层监听stateChange因为像网络资源加载慢、文件格式不支持等问题都会反映为error事件不监听的话播放失败你根本不知道原因。另外url可以是网络链接也可以是本地文件路径。本地音频我一般用file://开头比如file:///data/storage/el2/base/files/test.mp3。网络路径直接传https://地址即可。3.2 从本地媒体库读取音乐列表播放器不能光播放单曲还得能从手机媒体库读出音频列表。HarmonyOS 提供PhotoAccessHelper来访问媒体库但需要注意权限。第一步在entry/src/main/module.json5里声明权限{ module: { requestPermissions: [ { name: ohos.permission.READ_MEDIA } ] } }第二步在页面中动态请求权限不能只声明不请求。API 12 的权限分系统授权和用户授权READ_MEDIA属于用户授权类必须弹窗询问。我用abilityAccessCtrl发起请求import { abilityAccessCtrl, Permissions } from kit.AbilityKit; import { common } from kit.AbilityKit; async function requestPermission(context: common.UIAbilityContext): Promiseboolean { const permissions: ArrayPermissions [ohos.permission.READ_MEDIA]; const atManager abilityAccessCtrl.createAtManager(); try { const result await atManager.requestPermissionsFromUser(context, permissions); const grantStatus result.authResults[0]; return grantStatus abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } catch (err) { console.error(权限请求失败: JSON.stringify(err)); return false; } }第三步用photoAccessHelper获取音频资源。注意媒体库的音频在PhotoAsset的mediaType为mediaType.VIDEO和mediaType.IMAGE之外还有一个mediaType.AUDIO。获取列表的代码import { photoAccessHelper } from kit.MediaLibraryKit; async function loadAudioList(context: common.UIAbilityContext) { const phAccessHelper photoAccessHelper.getPhotoAccessHelper(context); const fetchOp: photoAccessHelper.FetchOptions { selections: , selectionArgs: [], sort: { key: date_added, isAsc: false } }; const fetchResult await phAccessHelper.getAssets(fetchOp); const assets await fetchResult.getAllObjects(); return assets.filter(item item.mediaType photoAccessHelper.PhotoViewModes.ALL); }实际项目中还需要按mediaType过滤出纯音频这里我偷懒写了示例。如果你只做一个“播放器壳子”也可以把固定几首网络音频放进rawfile目录就不用走媒体库权限开发阶段更快。3.3 播放进度、歌词和通知栏控制播放进度我用Slider组件显示配合一个每秒触发一次的定时器更新。State currentTime: number 0; State duration: number 0; private timerId: number -1; startProgressTimer() { this.timerId setInterval(() { if (this.avPlayer) { this.currentTime this.avPlayer.currentTime; this.duration this.avPlayer.duration; } }, 1000); }setInterval在 ArkTS 里没问题但页面退出时记得clearInterval否则会内存泄漏。进度条拖动时应该先暂停自动更新等用户松手后再根据 slider 的onChange事件调用seek否则一边拖动一边被定时器拉回去体验很糟糕。通知栏控制是让播放器具备基本后台能力的关键。HarmonyOS 提供AVSession来统一管理媒体会话做到锁屏显示封面、通知栏播放/暂停按钮、耳机线控等。引入kit.AVSessionKit后创建一个AVSession并设置元数据import { avSession } from kit.AVSessionKit; async function createSession() { const session await avSession.createAVSession(context, MusicPlayer, avSession.AVSessionType.AUDIO); await session.setAVMetadata({ title: this.currentSong.name, artist: this.currentSong.artist, album: this.currentSong.album }); await session.setAVPlaybackState({ state: avSession.PlaybackState.PLAYING, position: this.currentTime, duration: this.duration }); }创建AVSession之后系统通知栏会和播放器联动用户在通知栏点暂停session会发出命令你还需要监听play、pause事件来相应控制播放器。这一部分是后台播放的基本盘如果跳过了应用切到后台后系统可能会暂停播放音轨。如果你希望应用退到后台仍能持续播放还需要申请“长任务”权限使用kit.BackgroundTasksKit的continuousTaskManager声明audio类型任务。这部分涉及系统资源管理不展开写但你需要在module.json5里声明ohos.permission.KEEP_BACKGROUND_RUNNING并且用户要授权后台任务。我的经验是很多真机测试时权限会弹窗选择“允许”才能后台继续。4. 页面之间的数据流转与状态同步4.1 全局播放器单例还是页面内播放最初我图省事把AVPlayer写在HomePage里结果切到歌单页再返回播放器就停了因为页面被Tabs缓存后组件状态不稳定。后来我改成全局单例PlayerManager所有页面都通过它操作播放器UI 自己的状态只负责展示。单例模式在 ArkTS 里写起来很直接见上面的PlayerManager。但要解决一个问题播放进度需要多个页面同时更新。比如播放页显示进度条、列表页显示正在播放的歌曲名如果都去轮询PlayerManager会很脆。我用了 AppStorage 来做全局状态共享AppStorage.setOrCreate(currentSong, { name: 晴天, artist: 周杰伦, cover: $r(app.media.cover_qingtian), url: https://example.com/qingtian.mp3 }); AppStorage.setOrCreate(isPlaying, false);页面里用StorageProp(isPlaying)或StorageLink(isPlaying)来读取和同步状态。StorageProp是单向同步页面改变不写回全局StorageLink是双向同步。播放器状态这种“多处展示、只在一处改”的场景用StorageProp就够了。4.2 用Prop和Link给子组件传参歌单列表页里每一个歌曲项可以封装成一个子组件SongItem。父组件通过数组渲染时传原始值但子组件内部最好不要直接修改父组件数据。如果只是展示用Prop接收普通对象如果子组件要修改父组件的某个状态比如点击列表项要把当前歌曲传给播放页那就用回调函数或者Link。我的做法是列表项只负责回调onSongClickComponent struct SongItem { Prop song: Song; Prop isPlaying: boolean false; onSongClick: () void () {}; build() { Row() { // ... } .onClick(() { this.onSongClick(); }) } }在父组件中ForEach(this.songList, (song: Song) { SongItem({ song: song, isPlaying: PlayerManager.getInstance().isCurrentSong(song.id) }) .onSongClick(() { PlayerManager.getInstance().play(song.url); AppStorage.setOrCreate(currentSong, song); }) }, (song: Song) song.id)注意ForEach的第三个参数是一个键值生成器我用song.id保证列表复用正确。如果歌曲没有唯一id删除或换序时会出各种怪异问题。4.3 播放模式切换和歌词滚动播放模式单曲循环、列表循环、随机播放我放在PlayerManager里用一个枚举控制enum PlayMode { ORDER 1, SINGLE 2, RANDOM 3 }切换模式后在AVPlayer的stateChange事件监听里判断completed状态再取下一首。这里最容易被忽略的是 “completed 后不能直接再调play()”必须先reset()或重新prepare()。我一开始在完成事件里直接play()状态卡在completed不动后来看了日志才找到正确顺序监听completed→ 选择下一首 URL → 调用reset()→ 设置新 URL →prepare()→play()。歌词滚动属于复杂的自定义组件这里给一个基础思路用Scroll组件包含歌词行数组根据当前播放时间不断计算应该滚动的偏移量调用ScrollController.scrollTo将当前句歌词移到中间。想做得精细可以等播放器基本稳定后再加。新手不要一上来就写歌词同步先把播放、暂停、切歌、进度条跑通。5. 排坑实录与发布前检查5.1 模拟器没有声音赶紧换真机我开发前期一直在本地模拟器上跑界面正常但AVPlayer状态走到playing后没有任何声音。查了各种文档最后在社区里看到有人提模拟器的音频输出有设备兼容限制。我自己的经验是模拟器只适合调 UI涉及音频播放、媒体会话、后台长任务这些能力一律用真机。真机调试时需要开启开发者模式并在 DevEco Studio 的“设备”面板连接华为手机。如果hdc list targets看不到设备先换一根数据传输线很多 USB 线只能充电不能传数据。5.2 hilog 日志定位崩溃和状态异常写鸿蒙应用崩溃排查最好的工具是hilog。它可以按标签过滤日志也可以从应用内把日志打出来。我在PlayerManager里每个关键方法都加了日志import { hilog } from kit.PerformanceAnalysisKit; hilog.info(0x0000, MusicPlayer, 准备播放: %{public}s, url);调试时终端执行hdc shell hilog | grep MusicPlayer这样能实时看到AVPlayer的状态变化和错误信息。如果遇到页面闪退先看onWindowStageCreate里loadContent的err参数大多数找不到页面、路径错误、组件语法错误都会走到这里。有一次我的页面加载失败是因为在Builder里写了逻辑判断语句ArkTS 要求Builder方法体中只允许布局代码不允许复杂的业务逻辑这个错误提示不明显浪费了不少时间。5.3 签名配置与上架要用官方渠道开发阶段用自动签名就行但发布到真机需要手动签名。DevEco Studio 的 “File → Project Structure → Signing Configs” 里可以勾选 “Automatically generate signature”前提是你登录了华为账号。生成的文件包括.p12、.cer、.p7b这些是打包和上架必须的不要泄露给任何人。配置好后打开 “Build → Build Hap(s)/APP(s)”产物在entry/build/default/outputs/default/下.hap是应用包上架时要把.app包上传到 AppGallery Connect。关于上架渠道我见过很多人到处找第三方 HAP 资源站或者解包工具比如网上流传的hap-store、微信解包之类我的建议是不要碰。这些来源的包权属不明还可能被注入恶意代码。音乐播放器涉及用户媒体库和通知栏权限一旦被恶意利用结果非常麻烦。老老实实走官方 AGC 上架既可以发应用也可以发元服务审核流程成熟。5.4 提醒别被“鸿蒙 PC 版”这类热搜带偏热词里出现不少“开源鸿蒙 pc 版官网下载”“鸿蒙系统 pc 版安装”之类的内容和开发音乐播放器没什么关系。开源鸿蒙OpenHarmony的 PC 版是另一个技术分支普通应用开发不需要去下载 ISO 刷机。同样网上关于“鸿蒙系统回退”“旧机型升级”的话题也不是应用开发者该关心的。你只需要关注 DevEco Studio、SDK 版本、API 文档和真机调试足够把播放器做好。如果看到“鸿蒙开发基础认证”“底部导航栏”“RelativeContainer Flex Tabs”这些词说明你已经在正确的学习路线上继续刷官方文档就行。发布前最后检查一遍权限是否最少化只申请READ_MEDIA和后台长任务音频资源是否有版权AVPlayer在页面销毁时是否正确release通知栏的封面图是否压缩过。这些细节决定你上架后用户会不会给差评。我个人在实际操作中的体会是鸿蒙开发最大的门槛不是 ArkTS 语法而是很多能力散落在不同 Kit 里你需要花时间把MediaKit、AVSessionKit、AbilityKit、ArkUI的关系理清楚。写播放器是一个特别好的练习项目因为它强迫你同时接触状态管理、媒体能力、权限系统和后台任务。如果你能把这个项目完整跑通再去啃其他鸿蒙应用就轻松多了。最后再分享一个小技巧调试后台播放时一定要在真机上按 Home 键退到桌面观察通知栏和锁屏界面是否正常很多模拟器不会暴露这类问题。祝你把播放器尽快跑起来。
返回列表