目录

引言        

一、项目目录

二、项目展示

三、核心能力:应用的入口

1.EntryAbility:应用的生命周期管理

2.EntryBackupAbility:数据备份能力

四、播放器核心:音乐的心脏

1.AVPlayerManager:播放器的灵魂

2.播放模式切换

五、数据模型:信息的载体

1.音乐数据模型

2.其他数据模型

六、页面组件:用户的面孔

1.主页面组件

2.播放页面

3.迷你播放器

七、状态管理:数据的流动

1.事件总线的应用

2.数据持久化

八、其他页面展示

1.首页搜索功能

2.其他应用(跳转web网页)

3.每日推荐和推荐歌单

4.登录页

5.设置个人位置


引言        

        随着HarmonyOS Next的正式发布,越来越多的开发者开始关注鸿蒙。作为开发者,如何快速上手HarmonyOS Next并构建一个功能完整的应用?音乐播放器是一个非常好的切入点,它涵盖了音频播放、UI交互、状态管理等多个核心技术点。

        今天,我将基于我的实际项目经验,分享如何在HarmonyOS Next平台上构建一个功能完整的音乐播放应用,让你少走弯路,快速掌握HarmonyOS Next开发精髓。

 本项目为学习项目,学习B站某马教程后进行修改,本项目为入门练习,没有后端接口(省流),项目中的的数据均为静态测试数据,仅供学习参考。

本项目源码链接:https://github.com/silver-kite-wu/HarmonyOS-MyAppmusic.git

一、项目目录

MyAppmusic采用了经典的三层架构,表现层,业务逻辑层,数据层。

二、项目展示

三、核心能力:应用的入口

1.EntryAbility:应用的生命周期管理

EntryAbility.ets 是应用的主入口,负责管理应用的生命周期:

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 传递上下文并初始化播放器
    const rm = this.context.resourceManager;
    avplayerClass.setContext(this.context.cacheDir, 
      this.context.filesDir, 
      (path: string) => rm.getRawFileDescriptor(path));

    // 异步初始化播放器
    avplayerClass.init().then(async ()=>{
      console.log('播放器初始化成功');
    })

    // 加载页面
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content.');
        return;
      }
    });
  }

  onNewWant(want: Want): void {
    // 点击通知后进入播放页
    router.pushUrl({ url: 'pages/Playnow' })
  }
}

关键实现点 :

1. 上下文传递 :将应用的上下文传递给播放器管理类
2. 异步初始化 :播放器初始化不阻塞页面加载
3. 通知处理 : onNewWant 方法处理通知点击事件

2.EntryBackupAbility:数据备份能力

EntryBackupAbility.ets 提供了应用数据备份功能,这是HarmonyOS的一个重要特性。通过备份能力,用户可以在更换设备或重装应用时恢复数据。

四、播放器核心:音乐的心脏

1.AVPlayerManager:播放器的灵魂

avplayermanager.ets 是整个应用的核心,管理着音频播放的全生命周期:

export default class avplayerClass {
  static player: media.AVPlayer | null = null;
  static isplay: boolean = false;
  static playmodel: 'auto' | 'repeat' | 'random' = 'auto';
  static playlist: songtype[] = songlist;
  static playindex: number = -1;

  static async init(): Promise<void> {
    if (avplayerClass.isInitialized && avplayerClass.player) {
      return;
    }
    try {
      avplayerClass.player = await media.createAVPlayer();
      avplayerClass.isInitialized = true;

      // 监听播放器状态变化
      avplayerClass.player.on('stateChange', (state: string) => {
        switch (state) {
          case 'playing':
            avplayerClass.isplay = true;
            break;
          case 'paused':
            avplayerClass.isplay = false;
            break;
          case 'completed':
            avplayerClass.next();
            break;
        }
        avplayerClass.updatestate();
      });

      // 监听播放进度更新
      avplayerClass.player.on('timeUpdate', (time: number) => {
        avplayerClass.time = Math.max(0, Number(time) || 0);
        avplayerClass.updatestate();
      });
    } catch (error) {
      console.error('初始化播放器失败:', error);
    }
  }
}

技术亮点 :

1. 单例模式 :确保全局只有一个播放器实例
2. 事件驱动 :通过监听播放器事件来更新状态
3. 状态同步 :使用事件总线广播播放状态

2.播放模式切换

static async next(): Promise<void> {
  switch (avplayerClass.playmodel) {
    case 'repeat':
      // 单曲循环
      await avplayerClass.playSongByIndex(avplayerClass.playindex);
      break;
    case 'random':
      // 随机播放
      const randomIndex = Math.floor(Math.random() * avplayerClass.playlist.length);
      avplayerClass.playindex = randomIndex;
      await avplayerClass.playSongByIndex(randomIndex);
      break;
    case 'auto':
    default:
      // 自动播放
      avplayerClass.playindex++;
      if (avplayerClass.playindex >= avplayerClass.playlist.length) {
        avplayerClass.playindex = 0;
      }
      await avplayerClass.playSongByIndex(avplayerClass.playindex);
  }
}

这部分没有按钮进行实现,感兴趣的可以自行修改,进行实现。

五、数据模型:信息的载体

1.音乐数据模型

music.ets 定义了音乐的数据结构:

export interface songtype {
  img: string;      // 封面图片URL
  name: string;     // 歌曲名称
  author: string;   // 歌手名称
  url: string;      // 音乐URL
  id: number;      // 唯一标识
}

export const songlist: songtype[] = [
  {
    img: "https://y.qq.com/music/photo_new/...",
    name: "起风了",
    author: "买辣椒也用券",
    url: "rawfile:1.mp3",
    id: 0
  },
  // 更多歌曲...
];

本来原先URL是http链接,测试的时候一会能播放一会播放不了,可能是音乐源服务器有防外联的机制,不允许同一IP高频访问,也有可能是其他问题,可以在评论区说说其他可能。现在使用的是本地文件来播放。

2.其他数据模型

项目还包含其他数据模型:

- maintabdata.ets :标签栏数据
- swiperdata.ets :轮播图数据
- maindailytype.ets :每日推荐数据
- recommenddata.ets :推荐歌单数据
- pinglundata.ets :评论数据
数据组织原则 :

- 每个数据模型文件负责一类数据
- 使用接口定义数据结构
- 导出默认数据数组

本项目没有后端接口,数据均为静态测试数据,感兴趣的可以自行优化

六、页面组件:用户的面孔

1.主页面组件

mainpage.ets 是应用的主页面,使用Tabs组件实现标签切换:

@Entry
@Component
struct main {
  @State curr_index: number = 0
  tabdata: Tabclass[] = [
    {txt:'主页', image:$r('app.media.recommend')},
    {txt:'发现', image:$r('app.media.find')},
    {txt:'评论', image:$r('app.media.pinglu')},
    {txt:'我的', image:$r('app.media.my')}
  ]

  build() {
    NavDestination() {
      Tabs({barPosition:BarPosition.End}){
        ForEach(this.tabdata,(itme:Tabclass,index:number)=>{
          TabContent(){
            if (index===0) {
              main_page()
            } else if (index===1) {
              find_page()
            } else if(index===2) {
              pinglun_page()
            } else {
              my_page()
            }
          }
          .tabBar(this.tabbuilder(itme,index))
        })
      }
    }
  }
}

这里可以进一步优化,不用官方的Tabs,改成自定义组件,原生的不知道为什么,它在左右滑动的时候有延迟,不如自定义组件,在开发者社区,Tabs效果也都是用的自定义组件来实现,或者禁止左右滑动。

2.播放页面

Playnow.ets 是音乐播放的详细页面,包含丰富的交互功能:

@Entry
@Component
export struct Playnow {
  @State playstate: playstate = {
    duration: 0,
    time: 0,
    isplay: false,
    playmodel: 'auto',
    playindex: this.playindex,
    img: songlist[this.playindex].img,
    name: songlist[this.playindex].name,
    author: songlist[this.playindex].author
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Image($r('app.media.back'))
          .onClick(() => router.back())
        Text('正在播放')
          .fontSize(22)
          .fontColor('#fff')
      }

      // 专辑封面(带旋转动画)
      Stack() {
        Image(this.playstate.img)
          .width(300)
          .height(300)
          .borderRadius(150)
          .rotate({
            x: 0, y: 0, z: 1,
            angle: this.start
          })
      }

      // 播放控制
      Row() {
        Image($r('app.media.previous'))
          .onClick(() => avplayerClass.previous())
        
        Image(this.playstate.isplay ? $r('app.media.pause') : $r('app.media.play'))
          .onClick(async () => {
            if (this.playstate.isplay) {
              await avplayerClass.pause();
            } else {
              await avplayerClass.play();
            }
          })
        
        Image($r('app.media.next'))
          .onClick(() => avplayerClass.next())
      }
    }
  }
}

此处可以添加新的组件,实现之前说的三种播放状态,随机,自动,单曲。

交互:

1. 专辑封面旋转 :播放时封面旋转,暂停时停止
2. 进度条拖拽 :用户可以拖动进度条调整播放进度
3. 播放控制 :完整的播放、暂停、上一首、下一首功能

3.迷你播放器

zhu_page.ets 中的迷你播放器是应用的一大特色:

Row() {
  // 封面
  Image(this.miniImg)
    .width(40)
    .height(40)
    .borderRadius(6)

  // 歌曲信息
  Column({ space: 4 }) {
    Text(this.miniName || '未播放')
      .fontSize(14)
      .fontWeight(600)
    Text(this.miniAuthor || '')
      .fontSize(12)
      .fontColor('#666')
    Progress({value:this.val,total:this.total,type:ProgressType.Linear})
  }

  // 播放/暂停
  Image(this.miniIsPlay ? $r('app.media.play') : $r('app.media.pause'))
    .onClick(() => this.togglePlay())
}

设计理念 :

- 始终可见:悬浮在页面底部,不随页面滚动
- 状态同步:实时显示播放状态和进度
- 快速跳转:点击可进入完整播放页面

七、状态管理:数据的流动

1.事件总线的应用

使用HarmonyOS的EventEmitter实现跨页面状态同步:

static updatestate(): void {
  const stateData: PlayStateData = {
    duration: avplayerClass.duration,
    time: avplayerClass.time,
    isplay: avplayerClass.isplay,
    playmodel: avplayerClass.playmodel,
    playlist: avplayerClass.playlist,
    playindex: avplayerClass.playindex,
    img: currentSong.img || '',
    name: currentSong.name || '',
    author: currentSong.author || ''
  };

  // 发送状态更新事件
  emitter.emit('play_state_update', {
    data: {
      img: stateData.img,
      name: stateData.name,
      author: stateData.author,
      isplay: stateData.isplay,
      playindex: stateData.playindex,
      time: stateData.time,
      duration: stateData.duration
    }
  });
}

订阅事件 :

emitter.on('play_state_update', (eventData) => {
  const d = eventData.data;
  if (!d) return;
  
  this.miniImg = d.img || this.miniImg;
  this.miniName = d.name || this.miniName;
  this.miniAuthor = d.author || this.miniAuthor;
  this.miniIsPlay = d.isplay !== undefined ? !!d.isplay : this.miniIsPlay;
});

2.数据持久化

使用Preferences API实现数据持久化(用户首选项):

export async function saveLastMini(context: Context, state: MiniPlayState): Promise<void> {
  try {
    const dataPreferences = await preferences.getPreferences(context, 'music_prefs');
    await dataPreferences.put('lastMini', JSON.stringify(state));
    await dataPreferences.flush();
  } catch (err) {
    console.error('保存播放状态失败:', err);
  }
}

export async function loadLastMini(context: Context): Promise<MiniPlayState | null> {
  try {
    const dataPreferences = await preferences.getPreferences(context, 'music_prefs');
    const value = await dataPreferences.get('lastMini', '');
    return value ? JSON.parse(value as string) : null;
  } catch (err) {
    console.error('加载播放状态失败:', err);
    return null;
  }
}

八、其他页面展示

1.首页搜索功能

2.其他应用(跳转web网页)

这是内嵌的Web页面,点击右上角可以跳转到浏览器查看。

3.每日推荐和推荐歌单

其可以跳转到对应的歌单

4.登录页

蓝色协议点击可以弹出协议内容。

5.设置个人位置

其他的功能还有外联转置,头像更换,通知栏等等。。。

开发心得

1. 架构设计的重要性 :好的架构能让开发事半功倍
2. 代码规范的价值 :统一的规范能提高团队协作效率
3. 性能优化的必要性 :优化能显著提升用户体验
4. 持续学习的态度 :HarmonyOS生态在快速发展,需要不断学习


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、分享给更多的开发者!

让我们一起,在HarmonyOS的世界里,创造更多精彩的应用! 🎵🚀

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐