在这里插入图片描述
在这里插入图片描述

实例:简易钢琴(Piano)|技术:全量代码解读、操作剧本、触摸乐器模式抽象

一、本篇定位

前两篇分别讲了几何层与交互层,本篇完成三件事:

  1. 全量源码逐段解读——PianoPage 从头到尾过一遍;
  2. 运行效果演示——从音阶弹奏到静音开关的完整操作;
  3. 模式抽象——"触摸发音乐器"通用模板,附两个改写场景。

二、完整源码:PianoPage.ets

/**
 * 简易钢琴 —— 自定义 Canvas(CanvasRenderingContext2D)应用 10
 * 对应博客:Canvas_2d/10_简易钢琴/10-1 ~ 10-3
 *
 * 功能:
 *  1. 钢琴键盘:一个八度(C3~B3 白键 7 + 黑键 5)
 *  2. Canvas 琴键绘制:白键/黑键布局,按下高亮
 *  3. 触摸发音:Down 命中琴键 → 高亮 + AVPlayer 播放音阶
 *  4. 音高提示:显示当前按下音符名
 */
import { audio } from '@kit.AudioKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'Piano';

/** 白键(C 大调音阶) */
const WHITE_KEYS: string[] = ['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3'];
/** 黑键位置:白键之间的间隔序号 */
const BLACK_POS: number[] = [1, 3, 5, 6, 7];
/** 黑键音符名 */
const BLACK_NAMES: string[] = ['C#3', 'D#3', 'F#3', 'G#3', 'A#3'];

@Entry
@Component
struct PianoPage {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private canvasW: number = 0;
  private canvasH: number = 0;

  private whiteW: number = 44;
  private whiteH: number = 200;
  private blackW: number = 26;
  private blackH: number = 126;
  private startX: number = 10;

  private players: Array<audio.AudioPlayer | null> = [];
  private muted: boolean = false;

  @State pressedKey: string = '';
  @State noteText: string = '触摸琴键弹奏';
  @State mutedText: string = '';

  aboutToDisappear(): void {
    for (const p of this.players) {
      if (p !== null) {
        p.release();
      }
    }
    this.players = [];
  }
  // whiteX / blackX / playNote / onTouch / hitKey / draw / build
  // (见上两篇逐段解读)
}

逐段解读总表

代码段 职责 关键决策
WHITE_KEYS 白键音名 C 大调八度
BLACK_POS 黑键间隔序号 布局参数化
BLACK_NAMES 黑键音名 升号记谱
几何常量 whiteW/blackW 等 键盘比例
whiteX/blackX 坐标计算 白等宽黑跨交界
hitKey 命中判定 先黑后白 + Y 分区
playNote 发音(降级日志) 无资源不报错
draw 键盘绘制 按下高亮
onTouch 三态交互 Down/Move 弹奏
播放器数组 资源管理 release 清理
静音开关 muted 演示控制

三、接入工程三步

Step 1:放文件

  • PianoPage.etsentry/src/main/ets/pages/canvas_2d/

Step 2:注册路由(main_pages.json)

"pages/canvas_2d/PianoPage",

Step 3:首页入口(Index.ets)

Button('🎹 38 简易钢琴')
  .fontSize(14)
  .width('80%')
  .onClick(() => {
    this.getUIContext().getRouter().pushUrl({ url: 'pages/canvas_2d/PianoPage' });
  })

四、运行效果演示(操作剧本)

场景一:首次打开

步骤 界面表现 数据来源
1 标题栏:“🎹 简易钢琴”,提示"触摸琴键弹奏",🔊
2 画布:白底 7 个白键(C3~B3 带标签),交界处 5 个深色黑键 draw
3 底部说明:“C3 D3 E3 F3 G3 A3 B3 · 黑白键触摸弹奏”

场景二:C 大调音阶弹奏

依次点白键 C3→B3:

  • 每个键按下瞬间变浅蓝、提示"♩ C3"、“♩ D3”…;
  • 抬起恢复白色、提示复位;
  • 画布日志输出 “play note: C3” 等(无资源环境)。

场景三:黑键弹奏

点 C3-D3 之间的黑键 → 深蓝高亮 + “♩ C#3”。五个黑键都能触发。

场景四:滑动滑音

手指从 C3 滑到 G3 → 沿途白键逐个点亮(Move 连续命中),松开全部复位。

场景五:静音开关

点 🔊 → 变 🔇 → 再弹琴键:高亮与提示正常、无发音日志(muted 短路)。再点恢复。

五、模式抽象:触摸发音乐器通用模板

5.1 模板骨架

┌─────────────────────────────────────────────┐
│  键位几何层                                  │
│  键数组(名称/位置编码)+ 坐标计算函数        │
│  白等宽排布、黑跨交界(间隔序号编码)         │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│  命中判定层                                  │
│  先查覆盖层(黑键)→ 再查底层(白键)         │
│  Y 分区限定各键有效高度                      │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│  反馈层                                     │
│  视觉:按下高亮(pressedKey 比较)           │
│  听觉:playNote(AVPlayer / 降级日志)       │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│  生命周期层                                  │
│  播放器数组懒创建 + release 统一清理          │
└─────────────────────────────────────────────┘

5.2 六个约定

  1. 键位参数化:白键等宽、黑键间隔序号——调整几何改常量即可;
  2. 先查覆盖层:黑键盖白键,命中先黑后白;
  3. Y 分区判定:黑键半高、白键全高,各限区域;
  4. 视觉听觉分离:高亮(绘制)+ 发音(播放)各自独立,静音只关声音;
  5. 声音可降级:无资源环境降级为日志,功能不报错;
  6. 资源统一清理:播放器数组 release,页面销毁不留泄漏。

5.3 两个改写场景

场景 A:鼓垫(Drum Pad)

钢琴实例 鼓垫改写
白键 7 个 打击垫 4×4 网格
黑键 特殊音效垫
命中黑键优先 网格矩形命中(无覆盖层)
playNote 播放鼓点采样

场景 B:MIDI 键盘

钢琴实例 MIDI 改写
一个八度 88 键(八度模式重复)
playNote MIDI 事件输出(外部音源)
按下高亮 力度感应(按压深度→音量)

场景 C:木琴/竖琴 App——键位几何 + 触摸发音,模板复用。

三个场景的共同点:“键位几何 + 命中判定 + 视觉听觉反馈 + 资源生命周期”。掌握 38,任何"触摸发音乐器"都能起步。

六、三篇内容总对照

篇章 核心知识点 一句话总结
38-1 琴键布局与触摸演奏 黑键间隔编码、先黑后白 几何参数化 + 分区命中
38-2 页面UI与弹奏交互 双状态量、静音开关、播放器管理 一事件双通道反馈
38-3 完整代码与运行效果 全量源码、操作剧本、乐器模板 触摸乐器四层结构

七、FAQ(三篇汇总补遗)

Q1:38 与 21-37 的最大不同是什么?
A:引入声音维度——视觉 + 听觉双通道反馈。此前全是纯视觉应用。视听结合是乐器类应用的特征,也是 Canvas 应用的新方向。

Q2:真机怎么接入真实音效?
A:三步——① 准备音符 wav 到 resources/rawfile/notes/;② audio.createAudioPlayer() 建播放器;③ player.src = 'rawfile:///notes/C3.wav' + player.play()。替换 playNote 即可,其余零改动。接入点单一

Q3:为什么黑键用升号(C#)而不是降号(Db)?
A:C 大调黑键习惯用升号记谱。记谱法约定——换调式(如 F 大调)可改降号。命名是显示问题,与实现无关。

Q4:按下高亮后为什么必须重绘?
A:Canvas 是"绘制后保持"的位图,不会自动更新。pressedKey 变化后必须 draw() 重画当前帧——Canvas 应用都要"状态变化 → 主动重绘"(全系列一致)。

Q5:滑音时 Move 高频触发会影响性能吗?
A:每帧 hitKey + draw(约 7 白 5 黑循环),开销小。高频触摸下 60fps 无压力。先测再优化

Q6:能录下弹奏吗?
A:能——Down 时记录 {name, time} 数组,回放用定时器依次 playNote + 高亮。弹奏录制是教学好功能,留作练习。

八、结语

三篇文章完整走完了简易钢琴的"几何 → 交互 → 复用"闭环:

  • 1 教你琴键建模:黑键间隔编码、先黑后白命中、按下高亮;
  • 2 教你反馈设计:双状态量、静音开关、播放器生命周期;
  • 3 教你模式复用:触摸乐器四层模板,改几何即覆盖鼓垫、MIDI。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐