【鸿蒙实战开发】手写鸿蒙应用-键盘音乐

201 篇文章 4 订阅
201 篇文章 0 订阅

先看结果

image.png

关键技术

  1. 基本布局技巧
  2. AVPlayer
  3. 面向对象
  4. 全部采用 V2版本 状态管理技术

新建一个项目

  1. 创建项目

image.png

  1. 新建项目

image.png

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

image.png

设置全局沉浸式

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

image.png

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

  2. loadContent 中进行设置

image.png

 `//   1 设置应用全屏
 let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口

 // 2   设置沉浸式
 windowClass.setWindowLayoutFullScreen(true)` </pre>
  1. 此时效果是这样的 , 文字也会直接在状态栏上显示

image.png

  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` </pre>
    
  2. AppStatu 是自定义类,用来存储数据 状态栏高度数据的

    src/main/ets/types/index.ets


       `@ObservedV2
       export class AppStatu {
        @Trace vpHeight: number =0
       }` </pre>

搭建背景

image.png

 `build() {

 Column({ space: 30 }) {

 }
 .width("100%")
 .height("100%")
 .backgroundImage($r("app.media.startIcon"))
 .backgroundImageSize(ImageSize.FILL)
 .backdropBlur(1000) // 对背景进行模糊

 }` </pre>

搭建琴谱

image.png

琴谱背景区域

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

image.png


 `@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) // 对背景进行模糊

 }` </pre>

定义琴谱数据类型

琴谱只需要两个字段

  1. 琴谱对应歌曲的标题 title
  2. 琴谱 对应的英文字母 content

src/main/ets/types/index.ets


`@ObservedV2
export class Lyric {
 @Trace title: string = ""
 @Trace content: string[] = []
}` </pre>

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

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

image.png

`@ObservedV2
export class LyricStatu {
 @Trace title: string = ""
 @Trace isCorrect: boolean = false
}` </pre>

处理要渲染的数据

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

  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",]
    }` </pre>
  1. 处理后的数据结构

image.png

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

1722255962488.jpg

  1. 如何处理呢 在页面打开的时候在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
     })
     }
    }` </pre>

 `### 渲染琴谱

 ```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) // 对背景进行模糊

 }` </pre>

得到结果
image.png

搭建键盘

image.png

准备音频资源

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

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


 `const res = await getContext().resourceManager.getRawFd("paino1.mp3")
AVPlayer实例.fdSrc = res` </pre>

image.png

定义字母和音频映射数据

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" },
 ]
]` </pre>

页面关联数据


`import { letters, tonghua } from '../mock'

...
// 键盘 和 对应的音乐按键
@Local letters: LettemMusic[][] = letters` </pre>

构建键盘布局结构


 `// 键盘
 @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)
 }` </pre>

使用键盘布局结构

 `build() {

 Column({ space: 30 }) {
 //   1 琴谱
 this.MusicScore()

 //   2 键盘
 this.KeyBoard()
 }
 .width("100%")
 .height("100%")
 .backgroundImage($r("app.media.startIcon"))
 .backgroundImageSize(ImageSize.FILL)
 .backdropBlur(1000) // 对背景进行模糊

 }` </pre>

按下键盘,播放音乐功能

关键流程

  1. 封装AVPlayer管理类,每一个按键对应一个单独声音,因为上一个声音没有播放完毕,我们是可以同时播放第二个、第三个声音的,所以可以通过实例化多个 AVPlayer来使其一一对应
  2. 点击键盘 获取键盘对应的音乐路径
  3. 将音乐路径传递给AVPlayer,使其播放声音

了解AVPlayer

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

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

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

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

image.png

AVPlayer基本使用流程

  1. 创建 AVPlayer 实例 此时,avPlayer进入空闲状态 idle
    `const avPlayer = await media.createAVPlayer()` </pre>
  1. 监听状态的改变 我们对播放器的每一个操作,都会影响到它状态发生改变
 `avPlayer.on("stateChange", (state) => {
  switch (state) {
  // 如果播放器初始化完毕,那么就让它开始状态
  case "initialized":
  avPlayer.prepare()
  break;
  case "prepared":
  // 如果播放器准备完毕,就让它变成开始播放 
  avPlayer.play()
  break;
  default:
  break;
  }
  })` </pre>
  1. 设置播放音乐的URL
     `const res = await getContext().resourceManager.getRawFd(this.url)
     avPlayer.fdSrc = res  // 设置完播放器后,播放器会进入 initialized 状态` </pre>
  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` </pre>

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

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

image.png

`// 用来判断按下的按键和琴谱是否对应的
letterFlat: LettemMusic[] = []

aboutToAppear() {
 this.letterFlat = this.letters.flat()
 // ...
 }` </pre>

给键盘添加点击事件

 `.onClick(() => this.playLetter(item))` </pre>

image.png

实现点击播放音乐


 `// 用来管理正在播放的声音对应的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)
 }
 }` </pre>

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

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

image.png

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

image.png

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

 `// 用户弹的到琴谱坐标
 nextRowColumn: number[] = [0, 0]` </pre>

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

 `// 点击键盘播放音乐
 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("最后一个了")
 }
 }
 }
 }` </pre>

小结

  1. 本篇教程可能用词不够简洁,如按键、键盘、音乐、乐谱、琴谱有些名词其实是代表同一个意思。
  2. 页面结构功能没有拆分成组件独立管理
  3. 功能稍弱,如切换琴谱,按键反馈、登录、分享、排行功能都缺失,只实现了核心的功能

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

这份鸿蒙(HarmonyOS NEXT)文档包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)5.0最新学习路线

在这里插入图片描述

有了路线图,怎么能没有学习文档呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

《鸿蒙 (OpenHarmony)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值