在鸿蒙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. 实际项目注意事项
- 性能优化:对于大量歌曲,使用LazyForEach优化列表性能
- 网络请求:实现网络歌曲的缓存机制
- 错误处理:完善网络错误、文件错误的处理
- 权限管理:动态申请运行时权限
- 国际化:支持多语言
- 主题切换:实现日间/夜间模式
- 播放模式:实现单曲循环、列表循环、随机播放等模式
- 音效设置:添加均衡器、音效设置
- 本地音乐扫描:实现设备本地音乐文件扫描功能
- 播放历史:记录播放历史功能
通过以上实现,你可以在HarmonyOS 5应用中创建类似QQ音乐的全功能音乐播放器,包含播放控制、歌词显示、播放列表管理等核心功能,并支持后台播放和通知控制。