文章配图:onKeyEvent 三阶段触发、KeyCode 判定具体键、KeyEventSource 判定输入源、与

页面预览

前言

前面我们用 onTouch 处理手势、onHover 处理悬停——但都是「指针」类输入。还有种「按键」类输入:PC 键盘(WASD/方向键/空格)、TV 遥控器(方向键/确认/返回)、手机外接手柄/键盘。这类输入不走触摸/悬停,要走 onKeyEvent 事件——捕获按下/抬起/长按三阶段,配合 KeyCode 判定具体键。

本篇以「猫猫大作战」PC 端用方向键选列、空格投放、ESC 暂停为预演场景,把 onKeyEvent 三阶段触发KeyCode 判定具体键KeyEventSource 判定输入源与 onClick/onTouch 的取舍四大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–52 篇。本篇是阶段三第三篇。

一、场景拆解:PC 键盘操控游戏

「猫猫大作战」当前用 onClick 列投放(第 37 篇)——手机端玩家点击列。但 PC 端玩家想用键盘:

  • ←/→ 方向键:左右选列(高亮预览)
  • 空格键:投放选中列
  • ESC 键:暂停/恢复
  • R 键:重新开始

onClickonTouch 都捕获不到键盘——要用 onKeyEvent

// 预演:Index 加 onKeyEvent 键盘操控
@State highlightCol: number = 0;       // 键盘选中列(0-4)

build() {
  Stack() { /* ... */ }
    .onKeyEvent((event: KeyEvent) => {
      switch (event.keyCode) {
        case KeyCode.KEYCODE_LEFT:      // ← 左移
          this.highlightCol = Math.max(0, this.highlightCol - 1);
          break;
        case KeyCode.KEYCODE_RIGHT:     // → 右移
          this.highlightCol = Math.min(4, this.highlightCol + 1);
          break;
        case KeyCode.KEYCODE_SPACE:     // 空格投放
          if (event.type === KeyType.Down) {
            this.handleColumnClick(this.highlightCol);
          }
          break;
        case KeyCode.KEYCODE_ESCAPE:    // ESC 暂停/恢复
          if (event.type === KeyType.Down) {
            if (this.gameState === GameState.PLAYING) { this.pauseGame(); }
            else if (this.gameState === GameState.PAUSED) { this.resumeGame(); }
          }
          break;
        case KeyCode.KEYCODE_R:         // R 重新开始
          if (event.type === KeyType.Down) {
            this.clearTimers();
            this.startGame();
          }
          break;
      }
      return true;       // ← 消费事件
    })
}

核心问题

  1. onKeyEvent 的三阶段(Down/Up/Repeat)分别在何时触发?
  2. KeyCode 怎么判定按的是哪个键?
  3. KeyEventSource 怎么判定是键盘、遥控器还是手柄?
  4. 按键事件和点击事件怎么协同?

二、onKeyEvent 三阶段触发

2.1 三种阶段

键按下(down)→ onKeyEvent(KeyType.Down)
键松开(up)→ onKeyEvent(KeyType.Up)
键住不动(repeat)→ onKeyEvent(KeyType.Repeat)  持续触发
阶段 KeyType 触发时机 频率
按下 Down 键被按下 1 次
抬起 Up 键被松开 1 次
重复 Repeat 按住不放 高频(系统 repeatRate)

关键经验onKeyEvent 三阶段和 onTouch 类似——Down 开始、Repeat 持续、Up 结束。但「按键」走 onKeyEvent,「触摸」走 onTouch。

2.2 事件回调签名

.onKeyEvent((event: KeyEvent) => {
  // event.type: KeyType(Down/Up/Repeat)
  // event.keyCode: KeyCode(具体键)
  // event.keyText: 键文本(如 'A', 'Space')
  // event.source: KeyEventSource(输入源)
  // event.repeatCount: 重复次数(Repeat 阶段累加)
  // event.timestamp: 时间戳

  return true;     // 返回 true 消费事件,false 不消费(向上冒泡)
})

2.3 返回值:消费 vs 冒泡

.onKeyEvent((event: KeyEvent) => {
  if (event.keyCode === KeyCode.KEYCODE_SPACE) {
    this.handleColumnClick(this.highlightCol);
    return true;     // 消费空格,不向上冒泡
  }
  return false;      // 其他键不消费,向上冒泡给父组件
})

关键经验返回 true 消费事件,false 向上冒泡——父组件的 onKeyEvent 会收到子未消费的键。本系列第 66 篊会专讲事件冒泡。

三、KeyCode 判定具体键

3.1 常用 KeyCode

enum KeyCode {
  // 方向键
  KEYCODE_LEFT = 2,
  KEYCODE_RIGHT = 3,
  KEYCODE_UP = 4,
  KEYCODE_DOWN = 5,

  // 功能键
  KEYCODE_SPACE = 49,
  KEYCODE_ESCAPE = 6,
  KEYCODE_ENTER = 66,
  KEYCODE_BACK = 4,

  // 字母键
  KEYCODE_A = 29,
  KEYCODE_R = 46,
  KEYCODE_P = 44,

  // 数字键
  KEYCODE_0 = 7,
  KEYCODE_1 = 8,
  // ...

  // 遥控器专有
  KEYCODE_DPAD_CENTER = 23,    // 遥控器确认键
  KEYCODE_DPAD_LEFT = 21,
  KEYCODE_DPAD_RIGHT = 22,
  KEYCODE_DPAD_UP = 19,
  KEYCODE_DPAD_DOWN = 20,
}

3.2 判定具体键

.onKeyEvent((event: KeyEvent) => {
  switch (event.keyCode) {
    case KeyCode.KEYCODE_LEFT:
    case KeyCode.KEYCODE_DPAD_LEFT:    // ← 键和遥控左键都算左移
      this.highlightCol = Math.max(0, this.highlightCol - 1);
      break;
    case KeyCode.KEYCODE_RIGHT:
    case KeyCode.KEYCODE_DPAD_RIGHT:
      this.highlightCol = Math.min(4, this.highlightCol + 1);
      break;
    case KeyCode.KEYCODE_SPACE:
    case KeyCode.KEYCODE_DPAD_CENTER:  // 空格和遥控确认都算投放
      if (event.type === KeyType.Down) {
        this.handleColumnClick(this.highlightCol);
      }
      break;
  }
  return true;
})

关键经验PC 键盘和 TV 遥控器键码可能不同——KEYCODE_LEFT vs KEYCODE_DPAD_LEFT,用 case 并列兼容多设备。

3.3 keyText 辅助判定

.onKeyEvent((event: KeyEvent) => {
  console.info(`按了 ${event.keyText}(keyCode: ${event.keyCode}`);
  // 打印:按了 A(keyCode: 29)
})

实战经验debug 用 keyText 直观,逻辑用 keyCode 精确——keyText 可能因输入法/键盘布局变,keyCode 固定。

四、KeyEventSource 判定输入源

4.1 三种输入源

enum KeyEventSource {
  KEYBOARD = 0,       // PC 键盘
  REMOTE = 1,         // TV 遥控器
  GAMEPAD = 2,        // 手柄
  OTHER = 3,
}

4.2 判定输入源做差异化

.onKeyEvent((event: KeyEvent) => {
  switch (event.source) {
    case KeyEventSource.KEYBOARD:
      console.info('PC 键盘输入');
      break;
    case KeyEventSource.REMOTE:
      console.info('TV 遥控输入');
      break;
    case KeyEventSource.GAMEPAD:
      console.info('手柄输入');
      break;
  }
  /* ... 按键逻辑 ... */
})

实战经验通常不区分输入源——KeyCode 已兼容多设备。只在「手柄摇杆模拟方向」「遥控长按确认」特殊场景才区分 source。

五、onKeyEvent vs onClick/onTouch 取舍

5.1 三种事件覆盖输入

事件 输入设备 触发条件
onClick 鼠标、手指、遥控确认 完整点击
onTouch 鼠标、手指 接触全过程
onHover 鼠标、遥控焦点 不接触接近
onKeyEvent 键盘、遥控、手柄 按键全过程

5.2 取舍决策

输入设备?
  ├─ 手指/鼠标 → onTouch + onClick + onHover
  └─ 键盘/遥控/手柄 → onKeyEvent

关键经验跨设备应用同时加 onTouch/onHover/onKeyEvent——手机端用 onTouch,PC 端用 onHover+onKeyEvent,TV 端用 onHover+onKeyEvent,各自触发不冲突。

5.3 同组件共存

Stack() { /* ... */ }
  .onClick(() => { /* 鼠标/手指点击 */ })
  .onTouch((event) => { /* 手指/鼠标全过程 */ })
  .onHover((isHover) => { /* 鼠标/遥控悬停 */ })
  .onKeyEvent((event) => { /* 键盘/遥控按键 */ })
// 四种事件各自独立,不冲突

六、实战:PC 键盘操控游戏

6.1 改造 Index 加 onKeyEvent

// 预演:Index 加 onKeyEvent 键盘操控
import { KeyCode, KeyType, KeyEvent, KeyEventSource } from '@ohos.multimodalInput';

@Entry
@Component
struct Index {
  @State gameState: GameState = GameState.IDLE;
  @State cats: Cat[] = [];
  @State highlightCol: number = 0;        // ← 键盘选中列(0-4)
  /* ... 其他 state */

  private gameEngine: GameEngine = new GameEngine();
  private readonly cols: number[] = [0, 1, 2, 3, 4];

  /* startGame / pauseGame / resumeGame / endGame / clearTimers / formatTime / aboutToDisappear 筑略 */

  handleColumnClick(column: number) {
    if (this.gameState !== GameState.PLAYING) return;
    if (this.gameEngine.dropCat(column)) {
      this.cats = this.gameEngine.getAllCats();
      this.nextCatLevel = this.gameEngine.getNextCatLevel();
    }
  }

  // onKeyEvent 键盘操控处理(本篇重点)
  onGameKeyEvent(event: KeyEvent): boolean {
    // 只在 PLAYING 态响应方向键和空格
    if (this.gameState !== GameState.PLAYING && this.gameState !== GameState.PAUSED) {
      return false;
    }

    switch (event.keyCode) {
      case KeyCode.KEYCODE_LEFT:
      case KeyCode.KEYCODE_DPAD_LEFT:
        if (event.type === KeyType.Down) {
          this.highlightCol = Math.max(0, this.highlightCol - 1);
        }
        return true;

      case KeyCode.KEYCODE_RIGHT:
      case KeyCode.KEYCODE_DPAD_RIGHT:
        if (event.type === KeyType.Down) {
          this.highlightCol = Math.min(4, this.highlightCol + 1);
        }
        return true;

      case KeyCode.KEYCODE_SPACE:
      case KeyCode.KEYCODE_DPAD_CENTER:
        if (event.type === KeyType.Down) {
          this.handleColumnClick(this.highlightCol);
        }
        return true;

      case KeyCode.KEYCODE_ESCAPE:
        if (event.type === KeyType.Down) {
          if (this.gameState === GameState.PLAYING) { this.pauseGame(); }
          else if (this.gameState === GameState.PAUSED) { this.resumeGame(); }
        }
        return true;

      case KeyCode.KEYCODE_R:
        if (event.type === KeyType.Down) {
          this.clearTimers();
          this.startGame();
          this.highlightCol = 0;
        }
        return true;
    }
    return false;      // 未识别键不消费,向上冒泡
  }

  @Builder
  GameView() {
    Column() {
      this.GameHUD()
      Column() {
        Row() { /* 预告区 */ }
        Stack() {
          /* 棋盘背景 */
          ForEach(this.cats, (cat: Cat) => { /* ... */ }, (cat: Cat) => cat.id)

          // 列点击层:onClick + 键盘高亮
          Row() {
            ForEach(this.cols, (col: number) => {
              Column()
                .width(GameConfig.CELL_SIZE)
                .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
                // 键盘选中列高亮
                .backgroundColor(this.highlightCol === col ? 'rgba(46, 204, 113, 0.3)' : 'rgba(0,0,0,0)')
                .onClick(() => { this.handleColumnClick(col); })
            }, (col: number) => `click_${col}`)
          }
        }
        .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE)
        .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
        .borderRadius(12).clip(true).backgroundColor('#D6EEF5')
      }.alignItems(HorizontalAlign.Center)

      Spacer()
      Row() { /* 底部控制栏 */ }
        .width('100%').padding({ left: 24, right: 24, bottom: 24, top: 12 })
    }
    .width('100%').height('100%')
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
    })
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Stack() {
      if (this.gameState === GameState.IDLE) {
        this.MainMenuView()
      } else {
        this.GameView()
      }
      if (this.gameState === GameState.PAUSED) {
        this.PauseOverlay()
      }
      if (this.gameState === GameState.GAME_OVER) {
        this.GameOverOverlay()
      }
    }
    .width('100%').height('100%')
    .onKeyEvent((event: KeyEvent) => this.onGameKeyEvent(event))   // ← onKeyEvent
  }

  /* GameHUD / MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */
}

6.2 触发流程

PC 键盘场景

  1. 按 → 键 → onKeyEvent(Down, KEYCODE_RIGHT)highlightCol 从 0 变 1 → 选中列高亮。
  2. 按 → 键 → highlightCol 从 1 变 2。
  3. 按空格 → onKeyEvent(Down, KEYCODE_SPACE)handleColumnClick(2) → 第 2 列投放猫。
  4. 按 ESC → onKeyEvent(Down, KEYCODE_ESCAPE)pauseGame()
  5. 按 ESC → onKeyEvent(Down, KEYCODE_ESCAPE)resumeGame()
  6. 按 R → onKeyEvent(Down, KEYCODE_R) → 重新开始。

TV 遥控场景

  1. 按 → 方向键 → onKeyEvent(Down, KEYCODE_DPAD_RIGHT) → 同 KEYCODE_RIGHT 效果。
  2. 按确认键 → onKeyEvent(Down, KEYCODE_DPAD_CENTER) → 同空格效果。
  3. 按返回键 → onKeyEvent(Down, KEYCODE_BACK) → 通常路由返回(未在本例处理)。

手机手指场景

  • onKeyEvent 不触发(手机无物理键盘)——但外接蓝牙键盘会触发。

七、踩坑提示

7.1 onKeyEvent 用普通函数丢 this

// ❌ 错误:普通函数 this 不指向组件
.onKeyEvent(function (event) { this.highlightCol++; })

// ✅ 正确:箭头函数保留 this(第 38 篇讲过)
.onKeyEvent((event: KeyEvent) => { this.highlightCol++; })

7.2 忘判 KeyType.Down 重复触发

// ❌ 错误:没判 Down,按住空格 Repeat 阶段也投放,连投放多只
case KeyCode.KEYCODE_SPACE:
  this.handleColumnClick(this.highlightCol);    // Down + Repeat 都触发
  break;

// ✅ 正确:只在 Down 触发,Repeat 忽略
case KeyCode.KEYCODE_SPACE:
  if (event.type === KeyType.Down) {
    this.handleColumnClick(this.highlightCol);
  }
  break;

关键经验「单次操作」只判 Down,忽略 Repeat——否则按住连发。连发场景(如移动)才用 Repeat。

7.3 忘返回值冒泡

// ❌ 错误:没返回,事件可能被父组件误处理
.onKeyEvent((event: KeyEvent) => {
  if (event.keyCode === KeyCode.KEYCODE_SPACE) {
    this.handleColumnClick(this.highlightCol);
  }
  // 忘了 return,默认 undefined(非 true)
})

// ✅ 正确:返回 true 消费,false 冒泡
.onKeyEvent((event: KeyEvent) => {
  if (event.keyCode === KeyCode.KEYCODE_SPACE) {
    this.handleColumnClick(this.highlightCol);
    return true;
  }
  return false;
})

7.4 忘兼容 PC 和 TV 键码

// ❌ 错误:只判 KEYCODE_LEFT,TV 遥控的 KEYCODE_DPAD_LEFT 不响应
case KeyCode.KEYCODE_LEFT:
  this.highlightCol--;
  break;

// ✅ 正确:并列 PC 和 TV 键码
case KeyCode.KEYCODE_LEFT:
case KeyCode.KEYCODE_DPAD_LEFT:
  this.highlightCol--;
  break;

7.5 暂停态忘屏蔽方向键

// ❌ 错误:暂停态还能按方向键改 highlightCol,但游戏暂停了没意义
onGameKeyEvent(event: KeyEvent): boolean {
  // 忘了守卫,PAUSE 态也响应
  switch (event.keyCode) { /* ... */ }
}

// ✅ 正确:守卫屏蔽非 PLAYING 态
onGameKeyEvent(event: KeyEvent): boolean {
  if (this.gameState !== GameState.PLAYING && this.gameState !== GameState.PAUSED) {
    return false;
  }
  // PAUSED 态只响应 ESC 恢复,不响应方向键
  if (this.gameState === GameState.PAUSED && event.keyCode !== KeyCode.KEYCODE_ESCAPE) {
    return false;
  }
  switch (event.keyCode) { /* ... */ }
}

八、调试技巧

  1. console.info 打 keyCode 和 keyText:追按了什么键,确认键码。
  2. console.info 打 type:追 Down/Up/Repeat,确认是否只 Down 触发。
  3. 不触发排查:确认焦点在组件上(onKeyEvent 需组件有焦点);确认设备有物理键盘(手机无)。
  4. 连发排查:检查是否忘判 KeyType.Down,Repeat 阶段也会触发。

九、性能与最佳实践

  1. 跨设备同时加 onTouch/onHover/onKeyEvent——各自独立不冲突。
  2. KeyCode 用 case 并列兼容 PC 和 TV——KEYCODE_LEFT 和 KEYCODE_DPAD_LEFT 都算。
  3. 「单次操作」只判 Down,连发用 Repeat——避免按住连投放。
  4. 返回 true 消费,false 冒泡——父组件能收到子未消费的键。
  5. 守卫屏蔽非游戏态——暂停/结束时只响应特定键(ESC/R),不响应方向键。
  6. 回调用箭头函数保留 this——普通函数 this 丢失。

十、阶段三进度(51–55)

本篇是阶段三「交互与动画」第 3 篇:

主题 核心要点
51 onTouch 手势三阶段 Down/Move/Up
52 onHover 悬停进入/离开,TV/PC 场景
53(本篇) onKeyEvent 键盘/遥控按键三阶段,KeyCode 判定
54 bindContextMenu 右键/长按上下文菜单
55 animateTo 显式动画触发

总结

本篇我们从 onKeyEvent 按键切入,掌握了三阶段触发(Down/Up/Repeat)KeyCode 判定具体键(PC + TV 兼容)KeyEventSource 输入源与 onClick/onTouch 的取舍四大要点,并给出了 PC 键盘操控游戏的完整代码。核心要点:按键走 onKeyEvent 不走 onTouch;KeyCode 并列兼容 PC/TV;单次操作只判 Down 避免连发;返回 true 消费 false 冒泡

下一篇我们将拆解 bindContextMenu——右键/长按上下文菜单。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

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

更多推荐