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

简介: 手把手带你实现 鸿蒙应用 键盘音乐

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

先看结果

关键技术

  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. 此时效果是这样的 , 文字也会直接在状态栏上显示
  1. 此时,考虑到不同设备的状态栏高度可能不同,所以我们需要
  1. 动态获取状态栏高度,存到全局状态中 AppStorageV2
  2. 页面读取全局状态中的状态栏高度,单独给页面进行设置
//   1 获取应用窗口对象
  const windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
  //  2  设置全屏
  windowClass.setWindowLayoutFullScreen(true)
  // 3 获取布局避让遮挡的区域
  const type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR;
  const avoidArea = windowClass.getWindowAvoidArea(type);
  const bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度
  const vpHeight = px2vp(bottomRectHeight) //  转换成 vp单位的数值
  //   4 把导航栏高度数据 存在全局
  const appStatu = AppStorageV2.connect(AppStatu, "AppStatu", () => new AppStatu())
  appStatu!.vpHeight = vpHeight
a. AppStatu 是自定义类,用来存储数据 状态栏高度数据的

src/main/ets/types/index.ets

@ObservedV2
export class AppStatu {
  @Trace vpHeight: number =0
}

搭建背景

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

@ObservedV2
export class Lyric {
  @Trace title: string = ""
  @Trace content: string[] = []
}

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

  1. 如图所示,绿色为正确
  2. 黄色为未输入或者不正确

@ObservedV2
export class LyricStatu {
  @Trace title: string = ""
  @Trace isCorrect: boolean = false
}

处理要渲染的数据

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

  1. 手上的数据

src/main/ets/mock/index.ets

import { Lyric } from '../types'
export const tonghua: Lyric = {
  title: "童话",
  content: ["LONOL", "LONOL", "OOMML", "LONOL", "LQPPO", "LONOM", "MMOTS", "PPRRQQ", "QQNPOONO", "ONOR", "LSRQPPPRRQQ",
    "QQVUTUV", "VPOT", "TTSSSLSRQQRQ", "QRQ", "RQPOOQST", "TTSPPRQ", "OQST", "TTSPPRQRQPO", "PQMMOONO",]
}

  1. 处理后的数据结构

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

  3. 如何处理呢 在页面打开的时候在aboutToAppear中处理即可 lyricList
import { tonghua } from '../mock'
import {   LyricStatu } from '../types'
@Entry
@ComponentV2
struct Index {
  // 琴谱列表
 @Local lyricList: LyricStatu[][] = []
  aboutToAppear() {
    this.lyricList = tonghua.content.map(row => {
      const list = row.split('').map(v => {
        const o = new LyricStatu()
        o.title = v
        return o
      })
      return list
    })
  }
}

### 渲染琴谱
   
   ```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
const avPlayer = await media.createAVPlayer()

  1. 监听状态的改变 我们对播放器的每一个操作,都会影响到它状态发生改变
avPlayer.on("stateChange", (state) => {
      switch (state) {
        // 如果播放器初始化完毕,那么就让它开始状态
        case "initialized":
          avPlayer.prepare()
          break;
        case "prepared":
         // 如果播放器准备完毕,就让它变成开始播放     
          avPlayer.play()
          break;
        default:
          break;
      }
    })

  1. 设置播放音乐的URL


const res = await getContext().resourceManager.getRawFd(this.url)
    avPlayer.fdSrc = res  // 设置完播放器后,播放器会进入 initialized 状态
  1. 开始播放
    我们已经在 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月前
|
存储 移动开发 Android开发
HarmonyOS应用开发者高级认证(88分答案)
HarmonyOS应用开发者高级认证(88分答案)
2552 0
|
5天前
|
安全 物联网 大数据
基于开元鸿蒙(OpenHarmony)的【智能药房与药品管理综合应用系统
基于开元鸿蒙(OpenHarmony)的【智能药房与药品管理综合应用系统
38 8
|
5天前
|
传感器 监控 算法
基于开源鸿蒙(OpenHarmony)的【智能家居综合应用】系统
基于开源鸿蒙(OpenHarmony)的【智能家居综合应用】系统
27 6
|
5天前
|
传感器 人工智能 监控
【基于开源鸿蒙(OpenHarmony)的智慧农业综合应用系统】
【基于开源鸿蒙(OpenHarmony)的智慧农业综合应用系统】
31 6
|
5天前
|
搜索推荐 物联网 开发工具
基于OpenHarmony(开源鸿蒙)的智慧医疗综合应用系统
基于OpenHarmony(开源鸿蒙)的智慧医疗综合应用系统
22 5
|
1天前
|
数据安全/隐私保护
高质量 HarmonyOS 应用权限管控流程
高质量 HarmonyOS 应用权限管控流程
7 0
|
2月前
|
存储 JavaScript 前端开发
HarmonyOS 3.1/4.0应用升级到HarmonyOS NEXT改动点
在 “2024鸿蒙零基础快速实战-仿抖音App开发(ArkTS版)”(<https://coding.imooc.com/class/843.html>)视频课程中,因为讲师在该课程授课时是使用的HarmonyOS 3.1/4.0应用(API 9),如果部分学员采用了最新的HarmonyOS NEXT API,此时就会遇到API兼容性的问题。
137 0
HarmonyOS 3.1/4.0应用升级到HarmonyOS NEXT改动点
|
3月前
|
人工智能 搜索推荐 数据可视化
鸿蒙应用实践:利用扣子API开发起床文案生成器
本文将使用扣子(coze)智能体API开发一个起床文案生成器,用于自己的鸿蒙应用中生成”千人千面“的起床文案。
87 0
|
3月前
鸿蒙应用更新跳转到应用市场
鸿蒙应用更新跳转到应用市场
80 0
|
3月前
|
缓存
鸿蒙实现获取应用缓存和清理缓存
鸿蒙实现获取应用缓存和清理缓存
264 0

热门文章

最新文章

相关实验场景

更多