HarmonyOS Next之仿网易云APP实战开发第二期(1) 原创

wuyanghcoa
发布于 2024-11-30 13:13
浏览
0收藏

前言

在数字化时代,音乐已经成为我们生活中不可或缺的一部分。本文将带领大家探讨如何基于HarmonyOS Next开发一个仿网易云音乐APP,从音频播放的核心技术到自定义播放组件的开发,我们将重点讨论音频播放的基本界面和功能、播放原理,以及如何进行鸿蒙网络访问和歌曲列表的构建。

基础

1. 媒体播放

在HarmonyOS中,媒体播放主要通过AVPlayer模块实现。AVPlayer提供了一系列的API来控制媒体的播放,包括创建播放器、设置播放源、准备播放、播放控制等。以下是AVPlayer的一些关键操作:

  • 创建播放器:使用OH_AVPlayer * OH_AVPlayer_Create(void)创建一个播放器实例。
  • 设置播放源:通过OH_AVPlayer_SetURLSourceOH_AVPlayer_SetFDSource设置播放源,可以是网络URL或者文件描述符。
  • 准备播放:调用OH_AVPlayer_Prepare准备播放环境,异步缓存媒体数据。
  • 播放控制:使用OH_AVPlayer_PlayOH_AVPlayer_PauseOH_AVPlayer_Stop等API进行播放控制。
  • 资源释放:播放结束后,使用OH_AVPlayer_ReleaseOH_AVPlayer_ReleaseSync释放播放器资源。

2. 鸿蒙网络访问

在网络开发中,HTTP请求是必不可少的一部分。在鸿蒙(HarmonyOS)开发中,同样需要处理网络请求,无论是与后端服务器交互还是获取外部API的数据。下面是对鸿蒙开发中涉及到的HTTP模块——http模块,以及一个常用的第三方库——axios模块的总结。

基本概念:

  • 请求(Request):客户端向服务器发送的消息。
  • 响应(Response):服务器接收到请求后返回给客户端的消息。
  • 消息格式:HTTP消息由报文头(Header)、状态行(Status Line)和可选的实体主体(Entity Body)组成。

基本用法:

  1. 导入
import { http } from '@kit.NetworkKit'
  1. 创建请求对象
const req = http.createHttp()
  1. 发送请求并获取响应
req.request('请求地址url')
.then((res: http.HttpResponse) => {
AlertDialog.show({ message: JSON.stringify(res) })
})

注意事项:

  • 预览器:无需配置网络权限即可成功发送请求。

  • 模拟器:需要配置网络权限才能成功发送请求。

  • 真机:需要配置网络权限才能成功发送请求。

  • 在HarmonyOS中,你需要在module.json5文件中配置网络权限。

3. 歌曲列表

歌曲列表的实现通常涉及到UI组件和数据管理。在HarmonyOS中,可以使用List组件来展示歌曲列表,并通过数据绑定动态更新列表内容。

4. 自定义播放组件开发

自定义播放组件的开发涉及到播放器的封装和UI的定制。以下是开发步骤和示例代码:

class AudioPlayer {
  constructor() {
    this.player = OH_AVPlayer_Create();
  }
  play(url) {
    OH_AVPlayer_SetURLSource(this.player, url);
    OH_AVPlayer_Prepare(this.player);
    OH_AVPlayer_Play(this.player);
  }
  pause() {
    OH_AVPlayer_Pause(this.player);
  }
  // 其他播放器控制方法...
}

具体实现

这里主要展示歌曲播放组件页面的开发

AVPlayer.ets

import media from '@ohos.multimedia.media'
import { BusinessError } from '@kit.BasicServicesKit'
import { audio } from '@kit.AudioKit'
import {Size_Data as sd} from '../Common/Constant/Size_data'
import {Configuration} from '../Common/Constant/Configuration'

/**
 * 音频播放器操作类
 * 提供音频播放器的初始化、播放控制和错误处理等功能
 */
export class player_op{
  is_paused:boolean = false // 播放器暂停状态标志
  player:media.AVPlayer|undefined // 音频播放器实例

  /**
   * 初始化播放器回调函数
   * 设置播放器的事件监听,包括错误处理和状态变化
   */
  Init_player_callingback(){
    if(this.player==undefined) return
    // 错误事件处理
    this.player.on('error',(err:BusinessError)=>{
      this.player?.reset()
      AlertDialog.show({
        message:'当前曲目无法播放',
        alignment:DialogAlignment.Center,
        width:sd.play_failure_width,
        height:sd.play_failure_height,
        shadow:{radius:50,color:Configuration.dialog_background_color,offsetX:30,offsetY:-30}
      })
    })
    // 状态变化事件处理
    this.player.on('stateChange',async (state:string,reason:media.StateChangeReason)=>{
      switch (state) {
        case 'idle':
          console.info('AvPlayer_State_Change: Player reset, now state: Idle')
          this.player?.release()
          break
        case 'initialized':
          console.info('AvPlayer_State_Change: Player initialized, now state: Initialized')
          if(this.player!=undefined){
            //设置播放器渲染参数,必须在Prepare调用前设置
            this.player.audioRendererInfo = {
              usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
              rendererFlags:0
            }
          }
          this.player?.prepare()
          break
        case 'prepared':
          console.info('AvPlayer_State_Change: Player prepared, now state: prepared')
          this.player?.play()
          break
        case 'playing':
          //播放到末尾后自动跳转到Completed状态
          console.info('AvPlayer_State_Change: Player play, now state: playing')
          break
        case 'paused':
          console.info('AvPlayer_State_Change: Player play, now state: paused')
          break
        case 'completed':
          console.info('AvPlayer_State_Change: now state: Complete')
          this.player?.stop()
          break
        case 'stopped':
          this.player?.reset()
          break
        case 'released':
          this.player = undefined
          break
        default :
          break
      }
    })
  }

  /**
   * 开始播放音频
   * @param url 音频文件的URL
   */
  Start_play(url:string){
    media.createAVPlayer().then((_player:media.AVPlayer)=>{
      this.player = _player
      this.Init_player_callingback()
      this.player.url = url
    })
  }

  /**
   * 暂停播放
   */
  player_pause(){
    if(this.is_paused) return
    this.is_paused = true
    this.player?.pause()
  }

  /**
   * 继续播放
   */
  player_continue(){
    if(!this.is_paused) return
    this.is_paused = false
    this.player?.play()
  }

  /**
   * 重置播放器并播放新的音频
   * @param _url 新音频文件的URL
   */
  player_reset(_url:string){
    this.is_paused = false
    if(this.player==undefined){
      this.Start_play(_url)
    }else{
      this.player.release().then(()=>{
        this.player = undefined
        this.Start_play(_url)
      })
    }
  }

}

// 创建并导出音频播放器操作实例
const Player =  new player_op()
export default Player

SongPage.ets

import {Song,Page_transmission as pt,Static_Config} from '../Class_def/song_def'
import http from "@ohos.net.http"
import {Size_Data as sd} from '../Common/Constant/Size_data'
import { CircleShape } from '@kit.ArkUI'
import {Order} from '../Common/Constant/Order'
import player from '../Api/AVPlayer'
import {web_dialog} from '../Build/Dialog'
import {Configuration} from '../Common/Constant/Configuration'
import text from '@ohos.graphics.text'


@Builder
export function song_builder(name:string,param:object){
  Song_page({current_song:Static_Config.current_song,page_info:(param as pt).page_info})
}

@Entry
@Component
struct test{
  build() {
  }
}

@Component
struct Song_page {
  @State current_song:Song = new Song()
  @State lyrics:Array<string> = []
  page_info:NavPathStack|undefined

  aboutToAppear(): void {
    //Get lyrics
    if(this.current_song.id<0) return
    this.get_http()
  }

  async get_http() : Promise<void>{
    let http_request = http.createHttp()
    let response = http_request.request(
      Order.url + Order.get_lyrics + this.current_song.id.toString()
    );

    await response.then((data)=>{
      if(data.responseCode==200){
        //Get new search data
        this.lyrics.splice(0,this.lyrics.length)      //Clear content
        let res = data.result as string
        let json_res = JSON.parse(res) as object
        let lrc = (json_res?.["lrc"]) as object
        let lrc_content = (lrc as object)?.["lyric"] as string
        let base:number = 0
        for(let i=0;i!=lrc_content.length;i++){
          if(lrc_content[i]==']') base = i+1
          if(lrc_content[i]=='\n') this.lyrics.push(lrc_content.substring(base,i))
        }
      }else{
        console.info("Search request fail")
      }
    })
  }

  build() {
    NavDestination(){
      Column(){
        Stack(){
          //当前歌曲名称
          Text(this.current_song.song_name)
            .width('100%')
            .fontSize(30)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .padding(5)
            .position({left:0,top:10})

          //返回按钮
          Button(){
            Text("<")
              .fontSize(20)
              .fontColor(Color.White)
          }
          .borderWidth(1.5)
          .borderColor(Color.White)
          .backgroundColor(Color.Transparent)
          .width('10%')
          .height('90%')
          .margin(10)
          .position({left:5,top:2})
          .type(ButtonType.Circle)
          .onClick((event: ClickEvent) => {
            this.page_info?.pop()
          })

        }
        .width('100%')
        .height('7%')
        .margin({top:10})

        //歌手名称,点击可打开显示歌手主页的网页
        Text(this.current_song.singer_name)
          .width('100%')
          .fontSize(20)
          .fontColor(Configuration.singer_name_color)
          .textAlign(TextAlign.Center)
          .onClick(()=>{
            let dialog_controller:CustomDialogController|null = new CustomDialogController({
              builder: web_dialog({
                singer_id:this.current_song.singer_id.toString()
              }),
              alignment:DialogAlignment.Bottom,
              offset:{dx:0,dy:-100},
              shadow:{radius:50,color:Color.Red,offsetX:30,offsetY:-30}
            })
            dialog_controller.open()
            dialog_controller = null
          })

        Blank().height(30)

        //Main Content:用于显示歌词
        List(){
          ForEach(this.lyrics,(line:string)=>{
            ListItem(){
              Text(line)
                .fontSize(20)
                .width('100%')
                .textAlign(TextAlign.Center)
                .fontColor(Configuration.lyric_color)
            }
            .width('100%')
            .height(sd.lyrics_line)
            .margin(10)
          },(index:number)=>index.toString())
        }
        .height('65%')
        .width('100%')
        .scrollBar(BarState.Off)

        /*按键区,包含三个按键,分别为
         * 1. 切换上一首,待后续添加逻辑
         * 2. 暂停或继续播放
         * 3. 切换下一首,待后续添加逻辑
         */
        Row(){

          Image($r('app.media.last'))
            .margin({left:'20%'})
            .size({width:sd.next_button,height:sd.next_button})
            .clipShape(new CircleShape({width:sd.next_button,height:sd.next_button}))

          Image($r('app.media.play'))
            .size({width:sd.play_button,height:sd.play_button})
            .clipShape(new CircleShape({width:sd.play_button,height:sd.play_button}))
            .margin({left:'7%'})
            .onClick(()=>{
              if(player.player!=undefined){
                if(player.is_paused) player.player_continue()
                else player.player_pause()
              }
            })

          Image($r('app.media.next'))
            .size({width:sd.next_button,height:sd.next_button})
            .clipShape(new CircleShape({width:sd.next_button,height:sd.next_button}))
            .margin({left:'7%'})

        }
        .width('100%')
        .height('20%')

        //content({current_song:this.current_song}).width('100%').height('100%')
      }
      .width('100%')
      .height('100%')
      .linearGradient({
        direction:GradientDirection.Bottom,colors:
        [['#FF4D4D',0.0],
          ['#FF7A7A',0.5],
          ['#FFA5A5',1.0]]})
    }.hideTitleBar(true)
  }
}

实现效果

HarmonyOS Next之仿网易云APP实战开发第二期(1)-鸿蒙开发者社区
HarmonyOS Next之仿网易云APP实战开发第二期(1)-鸿蒙开发者社区

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
已于2024-11-30 13:18:13修改
收藏
回复
举报
回复
    相关推荐