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

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

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

先看结果

关键技术

  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. 功能稍弱,如切换琴谱,按键反馈、登录、分享、排行功能都缺失,只实现了核心的功能
目录
相关文章
|
10天前
「Mac畅玩鸿蒙与硬件41」UI互动应用篇18 - 多滑块联动控制器
本篇将带你实现一个多滑块联动的控制器应用。用户可以通过拖动多个滑块,动态控制不同参数(如红绿蓝三色值),并实时显示最终结果。我们将以动态颜色调节为例,展示如何结合状态管理和交互逻辑,打造一个高级的滑块控制器应用。
145 78
「Mac畅玩鸿蒙与硬件41」UI互动应用篇18 - 多滑块联动控制器
|
11天前
|
UED
「Mac畅玩鸿蒙与硬件40」UI互动应用篇17 - 照片墙布局
本篇将带你实现一个简单的照片墙布局应用,通过展示多张图片组成照片墙效果,用户可以点击图片查看其状态变化。
118 67
「Mac畅玩鸿蒙与硬件40」UI互动应用篇17 - 照片墙布局
|
1天前
|
存储 索引
「Mac畅玩鸿蒙与硬件44」UI互动应用篇21 - 随机励志语录生成器
本篇将带你实现一个随机励志语录生成器应用。用户点击按钮后,界面会随机显示一条预设的励志语录。该应用展示了如何结合数组操作、状态管理和动态更新界面内容的功能,是一个轻量级的互动应用示例。
53 21
「Mac畅玩鸿蒙与硬件44」UI互动应用篇21 - 随机励志语录生成器
|
17天前
|
存储 UED
「Mac畅玩鸿蒙与硬件37」UI互动应用篇14 - 随机颜色变化器
本篇将带你实现一个随机颜色变化器应用。用户点击“随机颜色”按钮后,界面背景会随机变化为淡色系颜色,同时显示当前的颜色代码,页面还会展示一只猫咪图片作为装饰,提升趣味性。
74 36
「Mac畅玩鸿蒙与硬件37」UI互动应用篇14 - 随机颜色变化器
|
14天前
「Mac畅玩鸿蒙与硬件38」UI互动应用篇15 - 猜数字增强版
本篇将带你实现一个升级版的数字猜谜游戏。相比基础版,新增了计分和历史记录功能,用户可以在每次猜测后查看自己的得分和猜测历史。此功能展示了状态管理的进阶用法以及如何保存和显示历史数据。
70 31
「Mac畅玩鸿蒙与硬件38」UI互动应用篇15 - 猜数字增强版
|
8天前
「Mac畅玩鸿蒙与硬件43」UI互动应用篇20 - 闪烁按钮效果
本篇将带你实现一个带有闪烁动画的按钮交互效果。通过动态改变按钮颜色,用户可以在视觉上感受到按钮的闪烁效果,提升界面互动体验。
60 19
「Mac畅玩鸿蒙与硬件43」UI互动应用篇20 - 闪烁按钮效果
|
9天前
「Mac畅玩鸿蒙与硬件42」UI互动应用篇19 - 数字键盘应用
本篇将带你实现一个数字键盘应用,支持用户通过点击数字键输入数字并实时更新显示内容。我们将展示如何使用按钮组件和状态管理来实现一个简洁且实用的数字键盘。
52 17
「Mac畅玩鸿蒙与硬件42」UI互动应用篇19 - 数字键盘应用
|
1天前
|
数据处理
「Mac畅玩鸿蒙与硬件45」UI互动应用篇22 - 评分统计工具
本篇将带你实现一个评分统计工具,用户可以对多个选项进行评分。应用会实时更新每个选项的评分结果,并统计平均分。这一功能适合用于问卷调查或评分统计的场景。
100 65
「Mac畅玩鸿蒙与硬件45」UI互动应用篇22 - 评分统计工具
|
13天前
|
前端开发 UED
「Mac畅玩鸿蒙与硬件39」UI互动应用篇16 - 倒计时环形进度条
本篇将带你实现一个倒计时环形进度条应用。用户可以设置倒计时的时间,启动倒计时后,应用会动态显示一个随着时间递减的环形进度条,同时伴有数字倒计时显示。这是结合动画效果和时间管理的实用示例。
97 10
「Mac畅玩鸿蒙与硬件39」UI互动应用篇16 - 倒计时环形进度条
|
11天前
|
监控 开发工具 Android开发
ARMS 用户体验监控正式发布原生鸿蒙应用 SDK
阿里云 ARMS 用户体验监控(RUM)推出了针对原生鸿蒙应用的 SDK。SDK 使用 ArkTS 语言开发,支持页面采集、资源加载采集、异常采集及自定义采集等功能,能够全面监控鸿蒙应用的表现。集成简单,只需几步即可将 SDK 接入项目中,为鸿蒙应用的开发者提供了强有力的支持。