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

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

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

先看结果

关键技术

  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. 功能稍弱,如切换琴谱,按键反馈、登录、分享、排行功能都缺失,只实现了核心的功能
目录
打赏
0
2
3
1
133
分享
相关文章
二、探索HarmonyOS Next应用的入口:深度解析Index页面
Index.ets是HarmonyOS Next开发者手册应用的入口页面,它展示了应用的主要导航结构和用户界面设计。本文将深入分析这个文件的代码结构、UI组件使用以及路由实现,帮助开发者理解HarmonyOS应用的基本构建模式。
75 0
鸿蒙5开发宝藏案例分享---应用接续提升内容发布体验
本文分享了鸿蒙应用接续功能的实战经验,帮助开发者实现跨设备流转。文章介绍了该功能的核心要点、开发条件及多个实战案例,如图文草稿跨设备接续、协同文档实时接续和社交通讯录接续,并提供了避坑指南与调试秘籍。通过动态压缩策略优化传输速度,结合AI能力提升体验。适合想了解鸿蒙跨设备开发的开发者参考学习。
鸿蒙5开发宝藏案例分享---一多开发实例(音乐)
鸿蒙开发干货分享:涵盖动态布局、交互动效、服务卡片设计、内存优化、分布式开发及性能加速六大核心主题。从折叠屏适配到手势动画,从服务卡片最佳实践到内存泄漏检测,结合官方100+实战案例与高频痛点解决方案,助你解锁鸿蒙开发隐藏技巧,提升效率与用户体验。快来一起探讨开发中的那些“坑”吧!
|
2月前
|
60.[HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的天气应用网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的天气应用界面。
73 0
|
2月前
|
59. [HarmonyOS NEXT 实战案例四] 天气应用网格布局(上)
天气应用是移动设备上常见的应用类型,它需要在有限的屏幕空间内展示多种天气信息,如当前温度、天气状况、未来天气预报、空气质量等。HarmonyOS NEXT提供的GridRow和GridCol组件非常适合实现这种复杂的信息展示布局。本教程将详细讲解如何使用这些组件构建一个美观、实用的天气应用界面。
71 0
鸿蒙开发:应用内如何做更新
使用系统的,直接调用检查和显示更新弹窗即可,可以说就两个方法,我们就实现了应用更新的功能,可以说是非常的简单,如果系统的弹窗无法满足您的需求,您可以自定义弹窗,然后实现跳转应用详情页面即可。
101 0
鸿蒙开发:应用内如何做更新
零基础用 DevEco Studio 打造你的首个 HarmonyOS 应用,开启鸿蒙生态的创新之旅,实现你的技术探索梦想
在鸿蒙生态蓬勃发展的当下,越来越多的开发者怀揣着热情与创意,渴望投身于HarmonyOS应用开发的浪潮之中。
零基础用 DevEco Studio 打造你的首个 HarmonyOS 应用,开启鸿蒙生态的创新之旅,实现你的技术探索梦想
HarmonyOS NEXT~HarmonyOS 语言仓颉:下一代分布式开发语言的技术解析与应用实践
HarmonyOS语言仓颉是华为专为HarmonyOS生态系统设计的新型编程语言,旨在解决分布式环境下的开发挑战。它以“编码创造”为理念,具备分布式原生、高性能与高效率、安全可靠三大核心特性。仓颉语言通过内置分布式能力简化跨设备开发,提供统一的编程模型和开发体验。文章从语言基础、关键特性、开发实践及未来展望四个方面剖析其技术优势,助力开发者掌握这一新兴工具,构建全场景分布式应用。
358 35
【HarmonyOS 5】鸿蒙组件&模板服务详解 - 助力高效开发的利器
在移动应用开发领域,效率与质量始终是开发者追求的核心目标。鸿蒙系统作为新兴的操作系统,为开发者提供了丰富且强大的开发资源,其中鸿蒙组件&模板服务更是成为开发者快速构建高质量应用的得力助手。
108 0
HarmonyOS NEXT仓颉开发语言实战案例:外卖App
仓颉语言实战分享,教你如何用仓颉开发外卖App界面。内容包括页面布局、导航栏自定义、搜索框实现、列表模块构建等,附完整代码示例。轻松掌握Scroll、List等组件使用技巧,提升HarmonyOS应用开发能力。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等