写点什么

手把手带你实现 鸿蒙应用 - 键盘音乐

作者:万少
  • 2024-12-15
    江苏
  • 本文字数:5566 字

    阅读完需:约 18 分钟

手把手带你实现 鸿蒙应用-键盘音乐

手把手带你实现 鸿蒙应用-键盘音乐

先看结果

关键技术

  1. 基本布局技巧

  2. AVPlayer

  3. 面向对象

  4. 全部采用 V2 版本 状态管理技术

新建一个项目

  1. 创建项目


  2. 新建项目


  3. 目录结构 - 可以后期用到再去新建


设置全局沉浸式

设置和不设置全局沉浸式的区别是这样的



  1. src/main/ets/entryability/EntryAbility.ets 文件内进行编辑

  2. loadContent 中进行设置



//   1 设置应用全屏let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
// 2 设置沉浸式windowClass.setWindowLayoutFullScreen(true);
复制代码


  1. 此时效果是这样的 , 文字也会直接在状态栏上显示


  2. 此时,考虑到不同设备的状态栏高度可能不同,所以我们需要

  3. 动态获取状态栏高度,存到全局状态中 AppStorageV2

  4. 页面读取全局状态中的状态栏高度,单独给页面进行设置

  5. AppStatu 是自定义类,用来存储数据 状态栏高度数据的

  6. src/main/ets/types/index.ets

搭建背景


  build() {
Column({ space: 30 }) {
} .width("100%") .height("100%") .backgroundImage($r("app.media.startIcon")) .backgroundImageSize(ImageSize.FILL) .backdropBlur(1000) // 对背景进行模糊
}
复制代码

搭建琴谱

琴谱背景区域

使用背景图片+模糊搭建琴谱区域,高度由内容撑开



  @Builder  MusicScore() {    // 琴谱    Column({ space: 3 }) {      Text("琴谱")    }    .width("100%")    .backgroundImage($r("app.media.startIcon"))    .backgroundImageSize(ImageSize.FILL)    .backdropBlur(500) // 对背景进行模糊    .padding({      top: this.appStatu!.vpHeight + 20    })  }
build() {
Column({ space: 30 }) { // 1 琴谱 this.MusicScore() } .width("100%") .height("100%")
.backgroundImage($r("app.media.startIcon")) .backgroundImageSize(ImageSize.FILL) .backdropBlur(1000) // 对背景进行模糊
}
复制代码

定义琴谱数据类型

琴谱只需要两个字段


  1. 琴谱对应歌曲的标题 title

  2. 琴谱 对应的英文字母 content


src/main/ets/types/index.ets


@ObservedV2export class Lyric {  @Trace title: string = "";  @Trace content: string[] = [];}
复制代码

定义字母的正确和不正确的状态类型

  1. 如图所示,绿色为正确

  2. 黄色为未输入或者不正确



@ObservedV2export class LyricStatu {  @Trace title: string = "";  @Trace isCorrect: boolean = false;}
复制代码

处理要渲染的数据

为了方便页面的效果处理,我们需要将手上的数据,简单处理下,方便页面渲染


  1. 手上的数据

  2. src/main/ets/mock/index.ets

  3. 处理后的数据结构


  4. 为什么要这样处理,因为让它方便渲染


  5. 如何处理呢 在页面打开的时候在aboutToAppear中处理即可 lyricList



### 渲染琴谱
```typescript // 状态栏的高度 @Local appStatu: AppStatu | undefined = AppStorageV2.connect(AppStatu, "AppStatu", () => new AppStatu())
@Builder MusicScore() { // 琴谱 Column({ space: 3 }) { // 标题 Text(tonghua.title) .fontSize(30) .fontColor("#fff") ForEach(this.lyricList, (item1: LyricStatu[]) => { Row({ space: 5 }) { ForEach(item1, (item2: LyricStatu) => { Text(item2.title) .fontColor(item2.isCorrect ? "#23d96e" : "#ffcf49") .fontSize(20) }) } }) } .width("100%") .backgroundImage($r("app.media.startIcon")) .backgroundImageSize(ImageSize.FILL) .backdropBlur(500) // 对背景进行模糊 .padding({ // 设置文字下移,否则被屏幕摄像头给挡住 top: this.appStatu!.vpHeight + 20 }) }


build() {
Column({ space: 30 }) { // 1 琴谱 this.MusicScore()
// this.KeyBoard() } .width("100%") .height("100%")
.backgroundImage($r("app.media.startIcon")) .backgroundImageSize(ImageSize.FILL) .backdropBlur(1000) // 对背景进行模糊
}

复制代码


得到结果


搭建键盘

准备音频资源

键盘一个 26 个字母,对应边有 26 个声音。一一相对应


其中,我们的静态资源存放在 rawFile 中,鸿蒙应用在打包时不会对里面的文件做任何的编译处理,然后在使用的时候需要搭配 AVPlayer 使用。如


const res = await getContext().resourceManager.getRawFd("paino1.mp3");AVPlayer实例.fdSrc = res;
复制代码


定义字母和音频映射数据

src/main/ets/mock/index.ets


export const letters: LettemMusic[][] = [  [    { name: "Q", src: "paino17.mp3" },    { name: "W", src: "paino23.mp3" },    { name: "E", src: "paino5.mp3" },    { name: "R", src: "paino18.mp3" },    { name: "T", src: "paino20.mp3" },    { name: "Y", src: "paino25.mp3" },    { name: "U", src: "paino21.mp3" },    { name: "I", src: "paino9.mp3" },    { name: "O", src: "paino15.mp3" },    { name: "P", src: "paino16.mp3" },  ],  [    { name: "A", src: "paino1.mp3" },    { name: "S", src: "paino19.mp3" },    { name: "D", src: "paino4.mp3" },    { name: "F", src: "paino6.mp3" },    { name: "G", src: "paino7.mp3" },    { name: "H", src: "paino8.mp3" },    { name: "J", src: "paino10.mp3" },    { name: "K", src: "paino11.mp3" },    { name: "L", src: "paino12.mp3" },  ],  [    { name: "Z", src: "paino26.mp3" },    { name: "X", src: "paino24.mp3" },    { name: "C", src: "paino3.mp3" },    { name: "V", src: "paino22.mp3" },    { name: "B", src: "paino2.mp3" },    { name: "N", src: "paino14.mp3" },    { name: "M", src: "paino13.mp3" },  ],];
复制代码

页面关联数据

import { letters, tonghua } from '../mock'
...// 键盘 和 对应的音乐按键@Local letters: LettemMusic[][] = letters
复制代码

构建键盘布局结构

  // 键盘  @Builder  KeyBoard() {    Column({ space: 10 }) {      ForEach(this.letters, (items: LettemMusic[]) => {        Row({ space: 8 }) {          ForEach(items, (item: LettemMusic) => {            Text(item.name)              .backgroundColor("rgba(255,255,255,0.9)")              .padding(10)              .borderRadius(10)              .fontWeight(400)              .stateStyles({                clicked: {                  .backgroundColor("#fff")                }              })          })        }        .width("100%")        .padding(2)        .justifyContent(FlexAlign.Center)      })
} .layoutWeight(1) }
复制代码

使用键盘布局结构

  build() {
Column({ space: 30 }) { // 1 琴谱 this.MusicScore()
// 2 键盘 this.KeyBoard() } .width("100%") .height("100%") .backgroundImage($r("app.media.startIcon")) .backgroundImageSize(ImageSize.FILL) .backdropBlur(1000) // 对背景进行模糊
}
复制代码

按下键盘,播放音乐功能

关键流程

  1. 封装 AVPlayer 管理类,每一个按键对应一个单独声音,因为上一个声音没有播放完毕,我们是可以同时播放第二个、第三个声音的,所以可以通过实例化多个 AVPlayer 来使其一一对应

  2. 点击键盘 获取键盘对应的音乐路径

  3. 将音乐路径传递给 AVPlayer,使其播放声音

了解 AVPlayer

使用 AVPlayer 可以实现端到端播放原始媒体资源,本开发指导将以完整地播放一首音乐作为示例,向开发者讲解 AVPlayer 音频播放相关功能。

播放的全流程包含:创建 AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。

在进行应用开发的过程中,开发者可以通过 AVPlayer 的 state 属性主动获取当前状态或使用 on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。


使用流程基本围绕这一张图即可


AVPlayer 基本使用流程

  1. 创建 AVPlayer 实例 此时,avPlayer 进入空闲状态 idle

  2. 监听状态的改变 我们对播放器的每一个操作,都会影响到它状态发生改变

  3. 设置播放音乐的 URL

  4. 开始播放

  5. 我们已经在 prepared 状态中,设置了自动播放了 avPlayer.play()

核心思路讲解

  1. 我们思考一下弹钢琴的逻辑,我们是不是可以同时按下多个按键,同时播放声音的? 所以我们需要 new 多个 AVPlayer 播放器实例

  2. 如果你重复按下两个相同的琴键,终止上一个琴键的播放,马上开启新的一个琴键的播放

  3. 最后,当这个琴键播放完毕时,我们要销毁掉这个实例,释放内存

AVPlayerManager

src/main/ets/utils/AvPlayerManager.ets

实现了对 AVPlayer 功能的基本封装


import { media } from "@kit.MediaKit";
class AVPlayerManager { // 播放器实例 avPlayer: media.AVPlayer | null = null; url: string = ""; // 播放完毕的回调事件 playComplete: () => void = () => {};
// 构造函数 constructor(url: string) { this.init(); this.url = url; }
// 初始化 async init() { this.avPlayer = await media.createAVPlayer(); this.avPlayer.on("stateChange", (state) => { switch (state) { case "initialized": this.avPlayer?.prepare(); break; case "prepared": this.avPlayer?.play(); break; case "completed": // 播放完毕,销毁实例 this.avPlayer?.release(); this.playComplete(); break; default: break; } }); this.avPlayer.on("error", (err) => { console.log("err", err); }); // 设置URL const res = await getContext().resourceManager.getRawFd(this.url); this.avPlayer!.fdSrc = res; }}
export default AVPlayerManager;
复制代码

对琴谱数据进行扁平化处理

方便判断按下的键盘是否正确和播放正确的按键音乐



// 用来判断按下的按键和琴谱是否对应的letterFlat: LettemMusic[] = []
aboutToAppear() { this.letterFlat = this.letters.flat() // ... }
复制代码

给键盘添加点击事件

 .onClick(() => this.playLetter(item))
复制代码


实现点击播放音乐

  // 用来管理正在播放的声音对应的AVPlayer实例 如按下了 Q W ,那么就会出生两个 AVPlayer实例  avPlayManagerList: AVPlayerManager[] = []
// 点击键盘播放音乐 playLetter(letter: LettemMusic) { // 根据点击的键盘 找到琴谱音乐对象 如 { name :"A" ,src :"paino1.mp1"} const item = this.letterFlat.find(v => v.name === letter.name)
// 根据播放的歌曲路径 判断当前音乐是否正在播放 const avIndex = this.avPlayManagerList.findIndex(v => v.url === item!.src)
if (avIndex !== -1) { // 如果正在播放 马上销毁 this.avPlayManagerList[avIndex].avPlayer?.release() // 并且从数组中移除 this.avPlayManagerList.splice(avIndex, 1) } // 根据当前点击的键盘创建对应的AVPlayer实例 const avplayManager = new AVPlayerManager(letter.src) // 追加到数组中 this.avPlayManagerList.push(avplayManager)
// 添加一个播放完毕的回调,用来删除avPlayManagerList数组中的AvPlay avplayManager.playComplete = () => { const index = this.avPlayManagerList.findIndex(v => v.url === item!.src) this.avPlayManagerList.splice(index, 1) } }
复制代码

按下键盘,判断按键是否正确

类似练习打字效果,按对按键了,就设置绿色,如:



因为我们的琴谱是个二维数组



因此,我们也是定义一个数组 [行的坐标,列的坐标],分别是二维数组相对应


  // 用户弹的到琴谱坐标  nextRowColumn: number[] = [0, 0]
复制代码


接着,也是在点击事件中,根据按下的按键和对应的琴谱是否相等,如果是,设置绿色


  // 点击键盘播放音乐  playLetter(letter: LettemMusic) {
// .... // 获取行坐标 const row = this.nextRowColumn[0] // 获取列坐标 const column = this.nextRowColumn[1] // 判断当前的坐标是否超出范围 if (this.lyricList[row] && this.lyricList[row][column]) { // 获取坐标对应的琴谱 const item = this.lyricList[row][column] // 判断按下的按键和对应的琴谱是否相等 如 L == L if (item.title === letter.name) { // 设置选中 item.isCorrect = true // 以下代码是设置坐标递进 if (this.lyricList[row][column+1]) { this.nextRowColumn[1] = column + 1 } else if (this.lyricList[row+1]) { this.nextRowColumn[0] = row + 1 this.nextRowColumn[1] = 0 } else { console.log("最后一个了") } } } }
复制代码

小结

  1. 本篇教程可能用词不够简洁,如按键、键盘、音乐、乐谱、琴谱有些名词其实是代表同一个意思。

  2. 页面结构功能没有拆分成组件独立管理

  3. 功能稍弱,如切换琴谱,按键反馈、登录、分享、排行功能都缺失,只实现了核心的功能


发布于: 刚刚阅读数: 5
用户头像

万少

关注

还未添加个人签名 2021-12-02 加入

还未添加个人简介

评论

发布
暂无评论
手把手带你实现 鸿蒙应用-键盘音乐_鸿蒙_万少_InfoQ写作社区