欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 资讯 > HarmonyOS NEXT 应用开发实战:音乐播放器的完整实现

HarmonyOS NEXT 应用开发实战:音乐播放器的完整实现

2024/12/21 23:41:07 来源:https://blog.csdn.net/qq8864/article/details/144576118  浏览:    关键词:HarmonyOS NEXT 应用开发实战:音乐播放器的完整实现

在 HarmonyOS NEXT 的应用开发过程中,我们可以利用其提供的丰富的组件和 API 来实现一个功能强大的音乐播放器。本文将通过一个实践案例,详细介绍如何使用 HarmonyOS NEXT 开发一个音乐播放器,包括播放模式切换、歌词显示、播放进度控制等功能。

项目结构

首先,我们来看一下项目的结构。为了代码的整洁和模块化,我们将音乐播放器的相关逻辑和数据封装在不同的文件中:

project-root/
├── common/
│   ├── api/
│   │   └── musicApi.ets  // 音乐API接口定义
│   ├── bean/
│   │   └── apiTypes.ets  // 数据类型定义
│   └── constant/
│       └── Constant.ets  // 常量定义
├── utils/
│   └── EfAVPlayer.ets  // 播放器封装
└── app/└── pages/└── MusicPlayer/└── MusicPlayerPageBuilder.ets  // 音乐播放器页面构建
音乐播放器封装

EfAVPlayer.ets 文件中,我们封装了一个名为 EfAVPlayer 的类,用于管理多媒体播放器的各种操作。该类内部使用了 HarmonyOS 的多媒体 API,并对其进行了封装,以便在应用中更方便地调用。

import media from '@ohos.multimedia.media';
import { BusinessError } from '@kit.BasicServicesKit';export type EfAVPlayerState = 'idle' | 'initialized' | 'prepared' | 'playing' | 'paused' | 'completed' | 'stopped'| 'released' | 'error';export interface EFPlayOptions {immediately?: booleanloop?: booleanvolume?: number
}export class EfAVPlayer {private avPlayer: media.AVPlayer | null = null;private stateChangeCallback?: Function;private errorCallback?: Function;private timeUpdateCallback?: Function;volume: number = 1loop: boolean = falseduration: number = 0;currentTime: number = 0;state: EfAVPlayerState = "idle"private efPlayOptions: EFPlayOptions = {immediately: true,loop: this.loop,volume: this.volume};getAVPlayer() {return this.avPlayer}setPlayOptions(options: EFPlayOptions = {}) {if (options.immediately !== undefined) {this.efPlayOptions.immediately = options.immediately}if (options.loop !== undefined) {this.efPlayOptions.loop = options.loopthis.loop = options.loop}if (options.volume !== undefined) {this.efPlayOptions.volume = options.volumethis.volume = options.volume}if (this.avPlayer && ['prepared', 'playing', 'paused', 'completed'].includes(this.avPlayer.state)) {if (this.avPlayer.loop !== this.loop) {this.avPlayer.loop = this.loop}this.avPlayer.setVolume(this.volume)}}async init(options: EFPlayOptions = this.efPlayOptions) {if (!this.avPlayer) {this.avPlayer = await media.createAVPlayer();this.setPlayOptions(options);this._onError();this._onStateChange();this._onTimeUpdate();}return this.avPlayer;}private async _onStateChange() {const avPlayer = await this.init();avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {this.state = state as EfAVPlayerStateswitch (state) {case 'idle':break;case 'initialized':avPlayer.prepare();break;case 'prepared':this.duration = avPlayer.duration;if (this.efPlayOptions.immediately) {avPlayer.play();}break;case 'playing':this.avPlayer!.loop = !!this.efPlayOptions.loop;this.loop = !!this.efPlayOptions.loop;break;case 'paused':break;case 'completed':break;case 'stopped':break;case 'released':break;default:break;}this.stateChangeCallback && this.stateChangeCallback(state);});}async onStateChange(callback: (state: EfAVPlayerState) => void) {this.stateChangeCallback = callback;}async onError(callback: (stateErr: Error) => void) {this.errorCallback = callback;}private async _onError() {const avPlayer = await this.init();avPlayer.on("error", (err: BusinessError) => {console.error("EfAVPlayer", err.message, err.code)this.errorCallback && this.errorCallback(err);});}private async _onTimeUpdate() {const avPlayer = await this.init();avPlayer.on("timeUpdate", (time: number) => {this.currentTime = time;this.timeUpdateCallback && this.timeUpdateCallback(time);});}async seek(time: number) {const avPlayer = await this.init();avPlayer.seek(time);}async onTimeUpdate(callback: (time: number) => void) {this.timeUpdateCallback = callback;}async stop() {const avPlayer = await this.init();await avPlayer.stop();}async setUrl(url: string) {const avPlayer = await this.init();avPlayer.url = url;}async setFdSrc(url: media.AVFileDescriptor) {const avPlayer = await this.init();avPlayer.fdSrc = url;}async setDataSrc(url: media.AVDataSrcDescriptor) {const avPlayer = await this.init();avPlayer.dataSrc = url;}async play() {const avPlayer = await this.init();avPlayer.play();}async pause() {const avPlayer = await this.init();avPlayer.pause();}async reset() {await this.avPlayer?.reset()}async release() {await this.avPlayer?.release();this.avPlayer?.off("stateChange");this.avPlayer?.off("error");this.avPlayer?.off("timeUpdate");this.currentTime = 0;this.duration = 0;this.avPlayer = null;this.errorCallback = undefined;this.stateChangeCallback = undefined;this.timeUpdateCallback = undefined;}async quickPlay(url: string | media.AVFileDescriptor | media.AVDataSrcDescriptor) {await this.init({ immediately: true, loop: true });if (typeof url === "string") {await this.setUrl(url)} else {if (typeof (url as media.AVFileDescriptor).fd === "number") {await this.setFdSrc(url as media.AVFileDescriptor)} else {await this.setDataSrc(url as media.AVDataSrcDescriptor)}}await this.play()}
}
音乐播放器页面构建

MusicPlayerPageBuilder.ets 文件中,我们定义了音乐播放器的页面结构。主要使用了 ColumnRowListTextSlider 等组件来构建界面,并通过 EfAVPlayer 类来管理音频播放。

import { getLyric, getTexts } from "../../common/api/musicApi"
import { LyricItem, SongItem } from "../../common/bean/apiTypes"
import { Constant } from "../../common/constant/Constant"
import { EfAVPlayer } from "../../utils/EfAVPlayer"
import { Log } from "../../utils/logutil"
import { BusinessError } from "@kit.BasicServicesKit"enum PlayMode {order,single,repeat,random
}interface PlayModeIcon {url: ResourceStrmode: PlayModename: string
}@Builder
export function MusicPlayerPageBuilder() {MusicPlayer()
}@Component
struct MusicPlayer {pageStack: NavPathStack = new NavPathStack()private scroller: Scroller = new Scroller()private types?: number;@StateavPlayer: EfAVPlayer = new EfAVPlayer()@StateplayModeIndex: number = 1@StateactiveIndex: number = 0@StatesongItem: SongItem = {} as SongItem@StateplayList: SongItem[] = []@StatelrcList: LyricItem[] = []@StateplayModeIcons: PlayModeIcon[] = [{mode: PlayMode.order,url: "resource/order",name: "顺序播放"},{mode: PlayMode.single,url: "resource/single",name: "单曲循环"},{mode: PlayMode.repeat,url: "resource/repeat",name: "列表循环"},{mode: PlayMode.random,url: "resource/random",name: "随机播放"},]aboutToAppear() {this.avPlayer.onStateChange(async (state) => {if (state === "completed") {await this.avPlayer.reset()switch (this.playModeIcons[this.playModeIndex].mode) {case PlayMode.order:if (this.activeIndex + 1 < this.playList.length - 1) {this.activeIndex++this.setPlay()}breakcase PlayMode.single:this.setPlay()breakcase PlayMode.repeat:if (this.activeIndex + 1 >= this.playList.length) {this.activeIndex = 0} else {this.activeIndex++}this.setPlay()breakcase PlayMode.random:this.activeIndex = Math.floor(Math.random() * (this.playList.length))this.setPlay()break}}})this.avPlayer.onTimeUpdate((time) => {})}getPlayItem() {return this.playList[this.activeIndex]}setPlay() {this.songItem = this.getPlayItem()if (this.types == undefined) {this.avPlayer.quickPlay(Constant.Song_Url + this.songItem.Sound)} else {this.avPlayer.quickPlay(Constant.Song_Url + this.songItem.Songmp3)}}setSeek = (time: number) => {this.avPlayer.seek(time)}timeFormat(time: number) {const minute = Math.floor(time / 1000 / 60).toString().padStart(2, '0')const second = Math.floor(time / 1000 % 60).toString().padStart(2, '0')return `${minute}:${second}`}playToggle = () => {if (this.avPlayer.state === "playing") {this.avPlayer.pause()} else {if (this.avPlayer.state === "idle") {this.setPlay()} else {this.avPlayer.play()}}}playModeToggle = () => {if (this.playModeIndex + 1 >= this.playModeIcons.length) {this.playModeIndex = 0} else {this.playModeIndex++}}previous = async () => {await this.avPlayer.reset()if (this.activeIndex - 1 < 0) {this.activeIndex = this.playList.length - 1} else {this.activeIndex--}this.setPlay()}next = async () => {await this.avPlayer.reset()const currentMode = this.playModeIcons[this.playModeIndex].modeif (currentMode === PlayMode.random) {this.activeIndex = Math.floor(Math.random() * (this.playList.length))} else {if (this.activeIndex + 1 >= this.playList.length) {this.activeIndex = 0} else {this.activeIndex++}}this.setPlay()}isCurrentLyric(item: LyricItem): boolean {const currentTimeInSeconds = Math.floor(this.avPlayer.currentTime / 1000);if (parseInt(item.Timing) <= currentTimeInSeconds && parseInt(item.EndTiming) >= currentTimeInSeconds) {return true}return false}build() {NavDestination() {Column() {List({ space: 0, scroller: this.scroller }) {ForEach(this.lrcList, (item: LyricItem, idx) => {ListItem() {Text(item.Sentence).fontColor(this.isCurrentLyric(item) ? Color.Blue : Color.Black).padding(10)}}, (itm: LyricItem) => itm.SongId)}.height('85%').divider({ strokeWidth: 1, color: '#F1F3F5' }).listDirection(Axis.Vertical).edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })Column() {Row({ space: 2 }) {Text(this.timeFormat(this.avPlayer.currentTime)).fontColor(Color.Blue)Slider({ min: 0, max: this.avPlayer.duration, value: this.avPlayer.currentTime }).layoutWeight(1).onChange(this.setSeek)Text(this.timeFormat(this.avPlayer.duration)).fontColor(Color.Blue)}Row() {Image($r('app.media.gobackward_15')).toolIcon().onClick(() => {this.setSeek(this.avPlayer.currentTime - 15000)})Image(this.avPlayer.state === "playing" ? $r('app.media.pause') : $r('app.media.play_fill')).fillColor(this.avPlayer.state === "playing" ? Color.Red : Color.Black).toolIcon().onClick(this.playToggle)Image($r('app.media.goforward_15')).toolIcon().onClick(() => {this.setSeek(this.avPlayer.currentTime + 15000)})}.width("80%").justifyContent(FlexAlign.SpaceAround)}.justifyContent(FlexAlign.Center).padding(10)}.height("100%").opacity(1).backgroundColor('#80EEEEEE').justifyContent(FlexAlign.SpaceBetween)}.width("100%").height("100%").onReady(ctx => {this.pageStack = ctx.pathStacklet par = ctx.pathInfo.param as { item: SongItem, types: number }this.songItem = par.itemthis.types = par.typesthis.playList.push(par.item)}).onShown(() => {if (Object.keys(this.songItem).length !== 0) {setTimeout(() => {this.playToggle()}, 100)}if (this.types == undefined) {getTexts(this.songItem.SongId).then((res) => {this.lrcList = res.data.data}).catch((err: BusinessError) => {Log.debug("request", "err.code:%d", err.code)Log.debug("request", err.message)});} else {getLyric(this.songItem.SongId).then((res) => {this.lrcList = res.data.data}).catch((err: BusinessError) => {Log.debug("request", "err.code:%d", err.code)Log.debug("request", err.message)});}}).onBackPressed(() => {this.avPlayer.reset()this.avPlayer.release()return false})
}@Extend(Image)
function toolIcon() {.width(40).stateStyles({normal: {.scale({ x: 1, y: 1 }).opacity(1)},pressed: {.scale({ x: 1.2, y: 1.2 }).opacity(0.4)}}).animation({ duration: 300, curve: Curve.Linear })
}
主要功能实现
  1. 播放模式切换:通过定义 PlayMode 枚举和 PlayModeIcon 接口,实现了顺序播放、单曲循环、列表循环和随机播放四种模式的切换。
  2. 歌词显示:通过 ListForEach 组件,实现了歌词的逐行显示,并在当前播放的歌词前设置蓝色高亮。
  3. 播放进度控制:使用 Slider 组件来控制播放进度,并通过 setSeek 方法实现跳转到指定时间点的功能。
  4. 播放控制:通过 playToggle 方法实现了播放和暂停的切换。
总结

通过本文的介绍,我们了解了如何在 HarmonyOS NEXT 中实现一个音乐播放器。这不仅涉及到界面的构建,还涉及到对音频播放器的封装和管理。希望本文能对大家有所帮助,如果在开发过程中遇到问题,也可以参考 HarmonyOS 官方文档或社区论坛寻求答案。

开发过程中,我们始终遵循 HarmonyOS 的设计理念,注重用户体验和代码的可维护性。希望未来的 HarmonyOS 应用开发能更加高效和易于实现。

作者介绍

作者:csdn猫哥

原文链接:https://blog.csdn.net/yyz_1987/article/details/144553700

团队介绍

坚果派团队由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉等相关内容,团队成员聚集在北京、上海、南京、深圳、广州、宁夏等地,目前已开发鸿蒙原生应用和三方库60+,欢迎交流。

版权声明

本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com