HarmonyOS应用开发实战:猫猫大作战-onClick 回调与闭包、handleColumnClick 处理流程、MVC 分层(UI 调引擎 + 同


前言
上一篇我们拆透了定时器泄漏的坑——clearTimers 兜底防泄漏。但定时器只是「时间驱动」,游戏还要「事件驱动」——玩家点击某列时投放猫咪。这需要 ArkUI 的 onClick 事件配合透明点击层(第 17 篇讲过结构)和 handleColumnClick 处理函数(第 17 篇简提,本篇专讲)。
本篇以「猫猫大作战」列投放的事件处理为锚点,把onClick 回调与闭包、handleColumnClick 处理流程、MVC 分层(UI 调引擎 + 同步 state)、投放失败的静默处理四大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–36 篇。本篇是阶段二第七篇。
一、场景拆解:玩家点击列投放
回顾「猫猫大作战」透明点击层(第 17 篇)和 handleColumnClick(第 17 篇简提):
// 来源:entry/src/main/ets/pages/Index.ets GameView() 第 3 层
Row() {
ForEach(this.cols, (col: number) => {
Column()
.width(GameConfig.CELL_SIZE)
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
.backgroundColor('rgba(0,0,0,0)')
.onClick(() => {
this.handleColumnClick(col); // ← 闭包捕获 col
})
}, (col: number) => `click_${col}`)
}
// 来源:entry/src/main/ets/pages/Index.ets
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return; // ← 状态守卫
if (this.gameEngine.dropCat(column)) { // ← 引擎投放
this.cats = this.gameEngine.getAllCats(); // ← 同步猫数组
this.nextCatLevel = this.gameEngine.getNextCatLevel(); // ← 更新预告
}
}
核心问题:
onClick回调里的col怎么绑定到对应列?handleColumnClick的三步流程(守卫→投放→同步)有什么讲究?dropCat返回false时(列已满)怎么处理?- UI 层和引擎层的职责怎么分?
二、onClick 回调与闭包
2.1 闭包按值捕获 col
ForEach(this.cols, (col: number) => {
Column()
.onClick(() => {
this.handleColumnClick(col); // col 被闭包捕获
})
}, (col: number) => `click_${col}`)
机制:ForEach 每次迭代都创建一个新的箭头函数闭包,捕获当时的 col 值。
执行流程:
| 迭代 | col 值 | 创建的闭包 |
|---|---|---|
| 第 1 次 | 0 | () => this.handleColumnClick(0) |
| 第 2 次 | 1 | () => this.handleColumnClick(1) |
| 第 3 次 | 2 | () => this.handleColumnClick(2) |
| 第 4 次 | 3 | () => this.handleColumnClick(3) |
| 第 5 次 | 4 | () => this.handleColumnClick(4) |
关键经验:ForEach 的迭代变量是「每次新建」的——不是 JS 经典的 var 共享坑,每列绑定到自己的 col。
2.2 闭包 vs 普通函数的 this
// ❌ 错误:普通函数 this 不指向组件
.onClick(function () {
this.handleColumnClick(col); // this 是触发事件的 Column,不是 Index
})
// ✅ 正确:箭头函数保留外层 this
.onClick(() => {
this.handleColumnClick(col); // this 指向 Index struct
})
关键经验:ArkUI 事件回统一用箭头函数——保留外层 this,能直接调 this.xxx。本系列第 38 篇会专讲箭头函数 this 绑定。
2.3 onClick 触发时机
ArkUI 的 onClick 是点击抬起时触发(down → up 算一次 click):
| 手势阶段 | 触发回调 |
|---|---|
| 手指按下(down) | onTouch 的 Down |
| 手指移动(move) | onTouch 的 Move |
| 手指抬起(up) | onClick 触发 |
实战经验:长按要响应用 onTouch + 计时器,单纯点击用 onClick 就够。本系列第 100 篇会专讲 Menu 长按菜单。
三、handleColumnClick 三步流程
3.1 步骤一:状态守卫
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return; // 仅 PLAYING 接受点击
/* ... */
}
守卫的作用:
| gameState | handleColumnClick 行为 | 原因 |
|---|---|---|
IDLE |
短路返回 | 主菜单态不投放 |
PLAYING |
正常投放 | 游戏中接受点击 |
PAUSED |
短路返回 | 暂停态不接受操作 |
GAME_OVER |
短路返回 | 结束态不投放 |
关键经验:UI 层守卫比引擎层守卫更省——if 短路避免无效的 dropCat 调用,省一次引擎遍历。
3.2 步骤二:引擎投放
if (this.gameEngine.dropCat(column)) {
/* ... 同步 state */
}
dropCat 引擎实现(第 121 篇会专讲重力下落,此处简提):
// 来源:entry/src/main/ets/components/GameEngine.ets
dropCat(column: number): boolean {
if (column < 0 || column >= GameConfig.BOARD_WIDTH) return false; // 越界守卫
// 检查该列顶部是否有空间
if (this.board[0][column] !== null) return false; // 列满守卫
const cat: Cat = {
id: `cat_${this.catCounter++}`,
level: this.nextCatLevel,
x: column,
y: 0, // 从顶部投放
falling: true // 标记下落态
};
this.cats.set(cat.id, cat);
// 生成下一只预览
this.nextCatLevel = this.randomLevel();
return true;
}
dropCat 双重守卫:
| 守卫 | 作用 |
|---|---|
| `column < 0 | |
this.board[0][column] !== null |
防列满(顶部已有猫) |
返回值:
| 返回 | 含义 | UI 层行为 |
|---|---|---|
true |
投放成功 | 同步 cats + nextCatLevel |
false |
投放失败(越界/列满) | 静默忽略,不更新 UI |
3.3 步骤三:同步 state
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats(); // 同步猫数组
this.nextCatLevel = this.gameEngine.getNextCatLevel(); // 更新预告
}
为什么同步两个 state:
| state | 同步原因 |
|---|---|
this.cats |
投放后猫数组增 1,UI 要渲染新猫 |
this.nextCatLevel |
dropCat 内部 this.nextCatLevel = this.randomLevel() 生成了新预览,UI 要显示 |
关键经验:投放后必同步两个 state——漏 cats 猫不显示,漏 nextCatLevel 预告区不准。
四、MVC 分层:UI 调引擎 + 同步 state
4.1 职责分层
UI 层(Index.ets)
├─ onClick 回调(闭包捕获 col)
└─ handleColumnClick(守卫 → 调引擎 → 同步 state)
↓
引擎层(GameEngine.ets)
└─ dropCat(越界守卫 → 列满守卫 → 创建 Cat → 加入 Map)
↓
数据层
├─ this.cats(Map<string, Cat>)
└─ this.board(二维数组)
``
**分层原则**:
| 层 | 职责 | 不做的事 |
|----|------|----------|
| UI 层 | 事件分发、状态守卫、state 同步 | 直接改棋盘、算物理 |
| 引擎层 | 棋盘操作、物理计算、规则判定 | 渲染、事件处理 |
| 数据层 | 存储猫和棋盘 | 逻辑 |
**关键经验**:**UI 层只调引擎 + 同步 state,不直接改棋盘**——这是 MVC 分层的最小实践。本系列第 111 篇会专讲 GameEngine 引擎类。
### 4.2 UI 层的边界
```ts
// ❌ 错误:UI 层直接改棋盘(绕过引擎)
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return;
// 直接创建猫,绕过引擎
const cat: Cat = { id: `cat_${Date.now()}`, level: this.nextCatLevel, x: column, y: 0, falling: true };
this.cats.push(cat); // UI 层直接改数组
// 但引擎的 cats Map 和 board 二维数组没更新!物理循环会错乱
}
// ✅ 正确:UI 层调引擎,引擎改数据,UI 同步快照
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return;
if (this.gameEngine.dropCat(column)) { // 引擎改数据
this.cats = this.gameEngine.getAllCats(); // UI 同步快照
this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
}
4.3 引擎是单一数据源
// UI 的 this.cats 只是引擎 Map 的快照
this.cats = this.gameEngine.getAllCats(); // Array.from(this.cats.values())
// 引擎内部数据:
private board: (Cat | null)[][] = []; // 二维棋盘
private cats: Map<string, Cat> = new Map(); // id → Cat 映射
关键经验:引擎是单一数据源——board 和 cats Map 由引擎统一管,UI 的 this.cats 只是快照。多次同步无竞态(都从引擎读)。
五、投放失败的静默处理
5.1 列已满时 dropCat 返回 false
if (this.gameEngine.dropCat(column)) {
/* 投放成功,同步 state */
}
// 投放失败(返回 false):if 不通过,啥也不做
``
**静默处理的原因**:
| 处理方式 | 用户体验 | 适合 |
|----------|----------|------|
| **静默忽略(本项目)** | 点击没反应,自己看棋盘知道列满 | 消除类游戏 |
| Toast 提示 | 「该列已满」 | 新手友好 |
| 振动反馈 | 短振一下 | 触觉反馈型 |
**关键经验**:**消除类游戏列满是常态,静默处理最不打扰**——玩家看到列顶部有猫就知道满了,不需要每次提示。
### 5.2 进阶:Toast 投放失败提示
```ts
import { promptAction } from '@kit.ArkUI';
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return;
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
this.nextCatLevel = this.gameEngine.getNextCatLevel();
} else {
// 投放失败提示
promptAction.showToast({
message: '该列已满',
duration: 800,
bottom: '40%'
});
}
}
实战经验:新手模式加 Toast,高手模式静默——可用配置切换。本系列第 95 篇会专讲 Toast 提示。
5.3 越界守卫的兜底
// 引擎内越界守卫
dropCat(column: number): boolean {
if (column < 0 || column >= GameConfig.BOARD_WIDTH) return false;
/* ... */
}
为什么需要越界守卫:
handleColumnClick的column来自ForEach(this.cols, ...),cols =[0,1,2,3,4],理论上不会越界。- 但防御式编程:万一未来 cols 数组改错,引擎的越界守卫兜底,不会数组越界崩溃。
关键经验:系统边界做输入校验——UI 层传入的数据不可信,引擎层兜底校验。
六、完整代码:onClick 列投放
// 来源:entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
@State gameState: GameState = GameState.IDLE;
@State cats: Cat[] = [];
@State nextCatLevel: CatLevel = CatLevel.SMALL;
/* ... 其他 state */
private gameEngine: GameEngine = new GameEngine();
private readonly cols: number[] = [0, 1, 2, 3, 4];
// 列投放处理函数(本篇重点)
handleColumnClick(column: number) {
// 步骤一:状态守卫
if (this.gameState !== GameState.PLAYING) return;
// 步骤二:引擎投放 + 步骤三:同步 state
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
// 投放失败静默忽略
}
@Builder
GameView() {
Column() {
this.GameHUD()
Column() {
// 预告区
Row() { /* ... */ }
// 游戏棋盘 Stack(3 层叠)
Stack() {
// 第 1 层:棋盘背景网格
Column() { /* ForEach rows × cols */ }
// 第 2 层:猫咪渲染
ForEach(this.cats, (cat: Cat) => {
Column() { /* ... */ }
.position({ x: ..., y: ... })
.animation({ duration: 100 })
}, (cat: Cat) => cat.id)
// 第 3 层:列点击层(onClick 本篇重点)
Row() {
ForEach(this.cols, (col: number) => {
Column()
.width(GameConfig.CELL_SIZE)
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
.backgroundColor('rgba(0,0,0,0)')
.onClick(() => {
this.handleColumnClick(col); // 闭包捕获 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)
}
}
七、踩坑提示
7.1 onClick 用普通函数丢 this
// ❌ 错误:普通函数 this 不指向组件
.onClick(function () {
this.handleColumnClick(col); // this 是 Column,不是 Index
})
// ✅ 正确:箭头函数保留外层 this
.onClick(() => {
this.handleColumnClick(col);
})
7.2 忘记状态守卫,暂停时还能投放
// ❌ 错误:没守卫,暂停时点击还能投放
handleColumnClick(column: number) {
if (this.gameEngine.dropCat(column)) { /* ... */ }
}
// ✅ 正确:守卫在前
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return;
if (this.gameEngine.dropCat(column)) { /* ... */ }
}
7.3 忘同步 nextCatLevel,预告区不准
// ❌ 错误:只同步 cats,没同步 nextCatLevel
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
// 忘了 this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
// 预告区显示的还是上一只的等级,不准
// ✅ 正确:同步两个 state
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
7.4 UI 层直接改棋盘,绕过引擎
// ❌ 错误:UI 层直接 push 猫到数组
handleColumnClick(column: number) {
const cat: Cat = { /* ... */ };
this.cats.push(cat); // 引擎的 board 和 cats Map 没更新!
}
// ✅ 正确:调引擎,引擎改数据
handleColumnClick(column: number) {
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
}
八、调试技巧
console.info打 column:onClick 回调里 log'click col', col,追点击分发。- 点击无响应排查:检查
gameState === PLAYING;检查上层是否有弹窗 Stack 挡住;检查.hitTestBehavior。 - 投放后猫不显示:检查
this.cats = this.gameEngine.getAllCats()是否调用;检查 ForEach 密钥cat.id是否冲突。 - 预告区不准:检查
this.nextCatLevel = this.gameEngine.getNextCatLevel()是否调用。
九、性能与最佳实践
- onClick 用箭头函数保留 this——普通函数 this 不指向组件。
- ForEach 闭包按值捕获 col——每列绑定到自己的 col,不共享。
- handleColumnClick 三步流程——守卫 → 引擎投放 → 同步 state。
- MVC 分层——UI 只调引擎 + 同步,不直接改棋盘。
- 投放失败静默处理——消除类游戏列满是常态,静默最不打扰。
- 引擎越界守卫兜底——UI 传入数据不可信,引擎层校验。
总结
本篇我们从 onClick 列投放切入,掌握了onClick 回调与闭包按值捕获 col、handleColumnClick 三步流程(守卫→投放→同步)、MVC 分层(UI 调引擎 + 同步 state)、投放失败的静默处理四大要点,并给出了完整 onClick 列投放代码。核心要点:闭包按值捕获列号;UI 只调引擎不写逻辑;投放失败静默;引擎越界守卫兜底。
下一篇我们将专讲箭头函数 this 绑定——callback 里 this 的核心机制。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/GameEngine.ets - onClick 通用点击事件官方指南
- ForEach 循环渲染官方指南
- HarmonyOS 事件处理最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md
更多推荐


所有评论(0)