当前位置: 首页 > news >正文

在鸿蒙HarmonyOS 5中HarmonyOS应用开发实现QQ音乐风格的播放功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似QQ音乐的音乐播放功能,包括播放控制、进度条、歌词显示等核心功能。

1. 项目基础配置

首先在config.json中添加必要的权限和能力声明:

"abilities": [{"name": "MainAbility","type": "page","launchType": "standard","backgroundModes": ["audio"]}
],
"reqPermissions": [{"name": "ohos.permission.READ_MEDIA"},{"name": "ohos.permission.MEDIA_LOCATION"},{"name": "ohos.permission.INTERNET"}
]

2. 音频服务实现

创建音频播放服务,使用@ohos.multimedia.audio模块:

// AudioPlayerService.ets
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';export class AudioPlayerService {private audioPlayer: audio.AudioPlayer | null = null;private currentState: PlayerState = PlayerState.IDLE;private currentPosition: number = 0;private duration: number = 0;private currentSong: Song | null = null;// 初始化音频播放器async initAudioPlayer() {const audioManager = audio.getAudioManager();const audioStreamInfo: audio.AudioStreamInfo = {samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,channels: audio.AudioChannel.CHANNEL_2,sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW};const audioPlayerOptions: audio.AudioPlayerOptions = {streamInfo: audioStreamInfo,playerType: audio.PlayerType.MEDIA_PLAYER};this.audioPlayer = await audioManager.createAudioPlayer(audioPlayerOptions);// 注册事件监听this.audioPlayer.on('play', () => {this.currentState = PlayerState.PLAYING;});this.audioPlayer.on('pause', () => {this.currentState = PlayerState.PAUSED;});this.audioPlayer.on('stop', () => {this.currentState = PlayerState.STOPPED;});this.audioPlayer.on('timeUpdate', (time: number) => {this.currentPosition = time;});this.audioPlayer.on('durationUpdate', (duration: number) => {this.duration = duration;});this.audioPlayer.on('error', (error: BusinessError) => {console.error(`AudioPlayer error: ${JSON.stringify(error)}`);this.currentState = PlayerState.ERROR;});}// 播放音乐async play(song: Song) {if (!this.audioPlayer) {await this.initAudioPlayer();}this.currentSong = song;// 设置音频源if (song.isLocal) {const file = await fs.open(song.url, fs.OpenMode.READ_ONLY);await this.audioPlayer.setSource(file.fd);} else {await this.audioPlayer.setSource(song.url);}await this.audioPlayer.play();}// 暂停播放async pause() {if (this.audioPlayer && this.currentState === PlayerState.PLAYING) {await this.audioPlayer.pause();}}// 继续播放async resume() {if (this.audioPlayer && this.currentState === PlayerState.PAUSED) {await this.audioPlayer.play();}}// 停止播放async stop() {if (this.audioPlayer) {await this.audioPlayer.stop();this.currentPosition = 0;}}// 跳转到指定位置async seekTo(position: number) {if (this.audioPlayer) {await this.audioPlayer.seek(position);}}// 获取当前播放状态getCurrentState(): PlayerState {return this.currentState;}// 获取当前播放位置getCurrentPosition(): number {return this.currentPosition;}// 获取歌曲总时长getDuration(): number {return this.duration;}// 获取当前播放歌曲getCurrentSong(): Song | null {return this.currentSong;}
}export enum PlayerState {IDLE = 'idle',PLAYING = 'playing',PAUSED = 'paused',STOPPED = 'stopped',ERROR = 'error'
}export interface Song {id: string;title: string;artist: string;album: string;url: string;coverUrl: string;duration: number;isLocal: boolean;lyrics?: LyricLine[];
}export interface LyricLine {time: number; // 毫秒text: string;
}

3. 播放器UI实现

创建主播放器界面:

// PlayerPage.ets
import { AudioPlayerService, PlayerState, Song } from './AudioPlayerService';@Entry
@Component
struct PlayerPage {private audioService: AudioPlayerService = new AudioPlayerService();@State currentSong: Song | null = null;@State playerState: PlayerState = PlayerState.IDLE;@State currentPosition: number = 0;@State duration: number = 0;@State showLyrics: boolean = true;@State currentLyricIndex: number = -1;// 模拟歌曲数据private songs: Song[] = [{id: "1",title: "示例歌曲1",artist: "歌手1",album: "专辑1",url: "https://example.com/song1.mp3",coverUrl: "resources/cover1.jpg",duration: 240000,isLocal: false,lyrics: [{ time: 0, text: "[00:00.00] 这是第一句歌词" },{ time: 5000, text: "[00:05.00] 这是第二句歌词" },// 更多歌词...]},// 更多歌曲...];onPageShow() {this.audioService.initAudioPlayer();this.loadSong(this.songs[0]);// 定时更新UI状态setInterval(() => {this.currentPosition = this.audioService.getCurrentPosition();this.duration = this.audioService.getDuration();this.playerState = this.audioService.getCurrentState();this.currentSong = this.audioService.getCurrentSong();this.updateCurrentLyric();}, 500);}loadSong(song: Song) {this.audioService.play(song);}// 更新当前歌词updateCurrentLyric() {if (!this.currentSong || !this.currentSong.lyrics) {this.currentLyricIndex = -1;return;}const lyrics = this.currentSong.lyrics;for (let i = 0; i < lyrics.length; i++) {if (this.currentPosition < lyrics[i].time) {this.currentLyricIndex = i - 1;return;}}this.currentLyricIndex = lyrics.length - 1;}// 格式化时间显示formatTime(ms: number): string {const totalSeconds = Math.floor(ms / 1000);const minutes = Math.floor(totalSeconds / 60);const seconds = totalSeconds % 60;return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;}build() {Column() {// 顶部导航栏Row() {Image($r('app.media.ic_back')).width(24).height(24).margin({ left: 12 }).onClick(() => {// 返回上一页})Text(this.currentSong?.title || '未播放').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Image($r('app.media.ic_more')).width(24).height(24).margin({ right: 12 })}.width('100%').height(56).alignItems(VerticalAlign.Center)// 专辑封面Column() {if (this.currentSong) {Image(this.currentSong.coverUrl || $r('app.media.default_cover')).width(300).height(300).borderRadius(150).margin({ top: 30 }).animation({ duration: 1000, curve: Curve.EaseInOut })}}.width('100%').height('40%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)// 歌曲信息Column() {Text(this.currentSong?.title || '--').fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 8 })Text(this.currentSong?.artist || '--').fontSize(16).fontColor('#999999')}.width('100%').margin({ top: 20, bottom: 20 }).alignItems(HorizontalAlign.Center)// 进度条Column() {Row() {Text(this.formatTime(this.currentPosition)).fontSize(12).fontColor('#666666').width('15%')Slider({value: this.currentPosition,min: 0,max: this.duration || 1,step: 1000,style: SliderStyle.OutSet}).blockColor('#07C160').trackColor('#E5E5E5').selectedColor('#07C160').layoutWeight(1).onChange((value: number) => {this.audioService.seekTo(value);})Text(this.formatTime(this.duration)).fontSize(12).fontColor('#666666').width('15%')}.width('90%').margin({ top: 10 })}.width('100%')// 歌词显示区域if (this.showLyrics && this.currentSong?.lyrics) {Scroll() {Column() {ForEach(this.currentSong.lyrics, (lyric: LyricLine, index: number) => {Text(lyric.text.split('] ')[1] || lyric.text).fontSize(this.currentLyricIndex === index ? 18 : 16).fontColor(this.currentLyricIndex === index ? '#07C160' : '#666666').textAlign(TextAlign.Center).margin({ top: 10, bottom: 10 }).width('100%')})}.width('100%')}.height('20%').scrollable(ScrollDirection.Vertical).scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring)}// 控制按钮Row() {Image($r('app.media.ic_prev')).width(36).height(36).margin({ right: 30 }).onClick(() => {// 上一首})if (this.playerState === PlayerState.PLAYING) {Image($r('app.media.ic_pause')).width(48).height(48).onClick(() => {this.audioService.pause();})} else {Image($r('app.media.ic_play')).width(48).height(48).onClick(() => {if (this.playerState === PlayerState.PAUSED) {this.audioService.resume();} else if (this.currentSong) {this.audioService.play(this.currentSong);}})}Image($r('app.media.ic_next')).width(36).height(36).margin({ left: 30 }).onClick(() => {// 下一首})}.width('100%').margin({ top: 20, bottom: 30 }).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)// 底部控制栏Row() {Image($r('app.media.ic_favorite')).width(24).height(24).margin({ right: 30 })Image($r('app.media.ic_download')).width(24).height(24).margin({ right: 30 })Image($r('app.media.ic_lyrics')).width(24).height(24).onClick(() => {this.showLyrics = !this.showLyrics;})Image($r('app.media.ic_playlist')).width(24).height(24).margin({ left: 30 })}.width('100%').height(60).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)}.width('100%').height('100%')}
}

4. 歌词解析功能

添加歌词解析工具类:

// LyricParser.ets
export class LyricParser {static parseLyric(lyricText: string): LyricLine[] {const lines = lyricText.split('\n');const result: LyricLine[] = [];const timeRegex = /$$(\d{2}):(\d{2})\.(\d{2,3})$$/;for (const line of lines) {const matches = timeRegex.exec(line);if (matches) {const minutes = parseInt(matches[1]);const seconds = parseInt(matches[2]);const milliseconds = parseInt(matches[3].length === 2 ? matches[3] + '0' : matches[3]);const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;const text = line.replace(timeRegex, '').trim();if (text) {result.push({time,text: `[${matches[0].substring(1)}] ${text}`});}}}// 按时间排序result.sort((a, b) => a.time - b.time);return result;}
}

5. 播放列表实现

创建播放列表组件:

// PlaylistDialog.ets
@Component
export struct PlaylistDialog {private songs: Song[];@Link currentSong: Song | null;private audioService: AudioPlayerService;@State searchText: string = '';build() {Column() {// 搜索框Row() {TextInput({ placeholder: '搜索播放列表' }).height(40).layoutWeight(1).onChange((value: string) => {this.searchText = value;})}.padding(10).borderRadius(20).backgroundColor('#F5F5F5').margin({ top: 10, bottom: 10 })// 歌曲列表List() {ForEach(this.getFilteredSongs(), (song: Song) => {ListItem() {Row() {Image(song.coverUrl || $r('app.media.default_cover')).width(50).height(50).borderRadius(5).margin({ right: 10 })Column() {Text(song.title).fontSize(16).fontColor(this.currentSong?.id === song.id ? '#07C160' : '#000000')Text(song.artist).fontSize(12).fontColor('#999999')}.layoutWeight(1)if (this.currentSong?.id === song.id) {Image($r('app.media.ic_playing')).width(20).height(20)}}.padding(10).width('100%')}.onClick(() => {this.audioService.play(song);})})}.layoutWeight(1)}.width('100%').height('70%').backgroundColor(Color.White).borderRadius(10)}private getFilteredSongs(): Song[] {if (!this.searchText) {return this.songs;}return this.songs.filter(song => song.title.includes(this.searchText) || song.artist.includes(this.searchText));}
}

6. 后台播放与通知控制

实现后台播放和通知控制:

// 在AudioPlayerService中添加
import notification from '@ohos.notification';
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';export class AudioPlayerService {// ...之前代码private registerBackgroundTask() {let bgTask = {mode: backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,isPersisted: true,bundleName: "com.example.musicapp", // 替换为你的应用包名abilityName: "MainAbility"};backgroundTaskManager.startBackgroundRunning(bgTask).then(() => console.info("Background task registered")).catch(err => console.error("Background task error: " + JSON.stringify(err)));}private showNotification(song: Song) {let notificationRequest = {content: {contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,normal: {title: "正在播放",text: song.title,additionalText: song.artist}},actionButtons: [{title: "上一首",actionType: notification.ActionType.BUTTON_ACTION},{title: this.currentState === PlayerState.PLAYING ? "暂停" : "播放",actionType: notification.ActionType.BUTTON_ACTION},{title: "下一首",actionType: notification.ActionType.BUTTON_ACTION}],id: 1,slotType: notification.SlotType.MEDIA_PLAYBACK};notification.publish(notificationRequest).then(() => {console.info("Notification published");});}// 修改play方法async play(song: Song) {// ...之前代码this.registerBackgroundTask();this.showNotification(song);}
}

7. 实际项目注意事项

  1. ​性能优化​​:对于大量歌曲,使用LazyForEach优化列表性能
  2. ​网络请求​​:实现网络歌曲的缓存机制
  3. ​错误处理​​:完善网络错误、文件错误的处理
  4. ​权限管理​​:动态申请运行时权限
  5. ​国际化​​:支持多语言
  6. ​主题切换​​:实现日间/夜间模式
  7. ​播放模式​​:实现单曲循环、列表循环、随机播放等模式
  8. ​音效设置​​:添加均衡器、音效设置
  9. ​本地音乐扫描​​:实现设备本地音乐文件扫描功能
  10. ​播放历史​​:记录播放历史功能

通过以上实现,你可以在HarmonyOS 5应用中创建类似QQ音乐的全功能音乐播放器,包含播放控制、歌词显示、播放列表管理等核心功能,并支持后台播放和通知控制。

http://www.xdnf.cn/news/964423.html

相关文章:

  • CppCon 2015 学习:Improving the future<T> with monads
  • MinHook 对.NET底层的 SendMessage 拦截真实案例反思
  • PHP和Node.js哪个更爽?
  • 【论文阅读】多任务学习起源类论文《Multi-Task Feature Learning》
  • MyBatis注解开发的劣势与不足
  • LeetCode--27.移除元素
  • Leetcode 3578. Count Partitions With Max-Min Difference at Most K
  • HTML 列表、表格、表单
  • Docker-containerd-CRI-CRI-O-OCI-runc
  • 【kafka】Golang实现分布式Masscan任务调度系统
  • Python 自动化临时邮箱工具,轻松接收验证码,支持调用和交互模式(支持谷歌gmail/googlemail)
  • 【C++】26. 哈希扩展1—— 位图
  • 【PhysUnits】17.5 实现常量除法(div.rs)
  • Linux上并行打包压缩工具
  • Cryosparc: Local Motion Correction注意输出颗粒尺寸
  • 基于大模型的输尿管下段结石诊疗全流程预测与方案研究
  • 多场景 OkHttpClient 管理器 - Android 网络通信解决方案
  • 【AI study】ESMFold安装
  • Ribbon负载均衡实战指南:7种策略选择与生产避坑
  • 深度学习核心概念:优化器、模型可解释性与欠拟合
  • 【无标题新手学习期权从买入看涨期权开始】
  • OpenCV 图像像素值统计
  • Python入门手册:常用的Python标准库
  • C++初阶-list的模拟实现(难度较高)
  • C++学习-入门到精通【17】自定义的模板化数据结构
  • ParcelJS:零配置极速前端构建工具全解析
  • React 中的TypeScript开发范式
  • 存储设备应用指导
  • C++ 手写实现 unordered_map 和 unordered_set:深入解析与源码实战
  • 光伏功率预测 | BP神经网络多变量单步光伏功率预测(Matlab完整源码和数据)