当前位置: 首页 > news >正文

HarmonyOS应用开发实战:猫猫大作战-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_LEFTvsKEYCODE_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 篇:

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

总结

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

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

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


相关资源:

  • 「猫猫大作战」项目源码:本仓库entry/src/main/ets/pages/Index.ets
  • ArkUI onKeyEvent 按键事件官方指南
  • KeyCode 键码枚举官方文档
  • HarmonyOS 多模态输入最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md
http://www.jsqmd.com/news/1277366/

相关文章:

  • HarmonyOS应用开发实战:猫猫大作战-bindSheet 的使用方式
  • 音频转换成文字在线怎么操作:逐步点哪里 - AI测评专家
  • 2026年提升内容AI收录的视频拍摄服务相关了解分享 - 起跑123
  • 2026东莞装修避坑指南!280亿市场大浪里,挑靠谱装修就看这波口碑榜 - dgqw123456
  • c++14 新增内容
  • 2026 年现阶段,邻水专业的楼板切割制造商格局重塑与选型新思路,装修时悄悄动这处结构,竟能省下十几万?很多人都踩了它的坑-洪春绳锯建筑 - 行业推荐官【官方】
  • 查询阿维菌素肥料优质厂商联系方式 正规靠谱源头农资商家汇总 - 热点品牌推荐
  • HarmonyOS应用开发实战:猫猫大作战-transition 装饰绑定、TransitionType 推/拉/全部、与 if 条件渲染协同、与 H
  • 扒开 HTTP/1.1 的底裤:报文结构、状态码与连接管理一次讲透
  • (2026最新)临沂本地漏水检测维修公司靠谱推荐:正规防水补漏上门维修-墙面/屋顶/外墙/暗管漏水检测精准定位 - 即刻修防水
  • 2026年国内高口碑淋浴房生产厂家实用选购参考指南 - 热点品牌推荐
  • 2026保姆级教程:Excel转PDF免费无水印,WPS/Office/在线工具全方法手把手教学 - 爱上科技热点
  • 2026年陕西西安GEO优化公司对比之沄启智能相关介绍 - 起跑123
  • 非洲黑石材源头工厂对接渠道 正规联系方式实用查询攻略 - 热点品牌推荐
  • 海南本地电视柜造型公司报价明细及采购选型参考 - 热点品牌推荐
  • 2026 北京业主常见疑惑汇总:漏水着急入住能不能加急施工,防水施工需要全天留人吗,北京各区上门时效,各类渗漏维修工期长短,轻微小漏水起步收费标准。 - 伶鹿到家
  • 2026年四川木质套装门制造商推荐几家 高适配性厂商选型参考 - 热点品牌推荐
  • 辽宁性价比高的水泥房源头工厂靠谱选购实用指南 - 热点品牌推荐
  • 告别网盘限速:netdisk-fast-download让你的下载速度飞起来
  • 2026年济南企业搬家连锁店推荐哪家 实用避坑选择指南 - 热点品牌推荐
  • 服装AI质检的标准边缘包含哪些?
  • 2026年景观铜雕塑源头工厂实力厂商参考指引 - 热点品牌推荐
  • 半导体芯片发明专利争议法律解决方案|京都律所杨移律师半导体、电子信息专利无效与维权实务 - 好物分享知识传播
  • (2026最新)丽水本地人必选的靠谱漏水检测维修推荐:正规防水补漏防水-卫生间/厨房/屋顶/阳台/外墙渗漏水精准测漏,本地人的信赖之选 - 安佳防水
  • 2026年二七区高层外墙渗水防水维修服务企业选择全指南 - 热点品牌推荐
  • Mission Planner:从飞行控制到任务执行的3种实用解决方案
  • 川渝地区茶饮餐饮从业者 饮品原材料公司怎么选更省心 - 热点品牌推荐
  • 2026年孕妈照相门店帮我推荐 大庆本地高适配拍摄机构指南 - 热点品牌推荐
  • 2026 北京高层业主高频提问,外墙渗水需要吊绳施工吗,北京可做外墙防水的正规商家有哪些,高层漏水排查难点,收费明细,如何确认施工无转包,保障高层作业安全。 - 伶鹿到家
  • 绝区零蕾米埃尔什么时候上线 绝区零蕾米埃尔介绍