HarmonyOS应用开发实战:猫猫大作战-透明覆盖层设计、onClick 事件回调、回调里 this 绑定、点击层与渲染层 z 序


前言
上一篇我们用 position 把猫咪精确摆到了棋盘格子里。但玩家要怎么「投放」猫咪到指定列?答案是在棋盘上方盖一层透明点击层——5 个等宽透明 Column,每个绑定 onClick 处理对应列的投放逻辑。这是棋盘类游戏的经典套路:渲染层只负责画,点击层独立负责交互,两层职责分离,互不干扰。
本篇以「猫猫大作战」游戏页第 3 层(列点击层)为锚点,把透明覆盖层设计、onClick 事件回调、回调里 this 绑定、点击层与渲染层 z 序四大要点讲透。读完本篇你将能独立写出:棋盘列点击层、透明热区覆盖、点击事件传递三种实战套路。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–16 篇。本篇是阶段一「棋盘渲染」的交互收尾篇。
一、场景拆解:列点击层
打开 entry/src/main/ets/pages/Index.ets 的 GameView,找到 Stack 第 3 层:
// 来源: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: number) => `click_${col}`)
}
这条透明覆盖层规格拆解:
| 元素 | 类型 | 作用 |
|---|---|---|
Row 外壳 |
横向容器 | 5 列横向并列 |
ForEach |
循环渲染 | 遍历 [0,1,2,3,4] |
Column 透明块 |
单列热区 | 60×480vp 透明矩形 |
.backgroundColor('rgba(0,0,0,0)') |
完全透明 | 不挡下层视觉 |
.onClick(() => ...) |
点击回调 | 投放到第 col 列 |
关键经验:点击层必须透明且置于渲染层之上——透明保证视觉不被遮,置于上层保证点击事件先被它捕获。
二、透明覆盖层的结构
2.1 三层 Stack 叠加
回顾整个 Stack 结构(第 9 篇讲过):
Stack() {
// 第 1 层:棋盘背景网格(zIndex 默认 0)
Column() { ForEach(this.rows, ...) }
// 第 2 层:猫咪渲染层(zIndex 默认 0,后声明覆盖在上)
ForEach(this.cats, (cat: Cat) => {
Column() { /* ... */ }.position({ x: ..., y: ... })
}, (cat: Cat) => cat.id)
// 第 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: number) => `click_${col}`)
}
}
.width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE)
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
.borderRadius(12)
.clip(true)
.backgroundColor('#D6EEF5')
叠层顺序:在 Stack 内,后声明的组件默认覆盖在先声明的组件之上。本项目声明顺序是「背景 → 猫咪 → 点击层」,所以点击层在最顶部,能捕获所有点击。
2.2 透明背景的两种写法
// 写法 1:rgba 第四位 alpha=0(本项目)
.backgroundColor('rgba(0,0,0,0)')
// 写法 2:Color.Transparent
.backgroundColor(Color.Transparent)
两种效果相同,Color.Transparent 更直观,推荐用写法 2。
提示:不要用
.opacity(0)做透明点击层——opacity(0)会让组件「完全不可见也不可点击」,点击事件会被丢弃。.backgroundColor(Color.Transparent)只是背景透明,组件本身依然可点击。
2.3 为什么用 Row + Column 而不是单个透明 Stack
// 方案 A:5 个 Column 横向并列(本项目)
Row() {
ForEach(this.cols, (col: number) => {
Column().width(CELL_SIZE).height(BOARD_H * CELL_SIZE)
.onClick(() => this.handleColumnClick(col))
}, ...)
}
// 方案 B:单个透明 Stack + onClick 算列号
Stack() {
/* 整个棋盘热区 */
}.onClick((event) => {
const col = Math.floor(event.x / CELL_SIZE)
this.handleColumnClick(col)
})
| 方案 | 优点 | 缺点 |
|---|---|---|
| A:5 个 Column | 每个 Column 独立 onClick,逻辑简单 | 多建 5 个组件 |
| B:单个 Stack + 算列号 | 只建 1 个组件,性能更优 | onClick 事件要算 event.x / CELL_SIZE,触摸坐标 API 较繁琐 |
实战经验:棋盘列数 ≤ 10 用方案 A(逻辑简单可读性高),列数 > 10 或需要精确像素热区时用方案 B。
三、onClick 事件回调
3.1 onClick 的回调签名
.onClick((event: GestureEvent) => {
// event.fingerList[0].localX → 触摸点相对组件左上角的 x
// event.fingerList[0].localY → 触摸点相对组件左上角的 y
// event.source → 触发源(SourceType.Mouse / Touch / etc.)
})
本项目回调不读 event,只关心 col(闭包捕获),所以省略了 event 参数:
.onClick(() => {
this.handleColumnClick(col);
})
3.2 闭包捕获 col
ForEach(this.cols, (col: number) => {
Column()
.onClick(() => {
this.handleColumnClick(col); // 闭包捕获当前 col
})
}, (col: number) => `click_${col}`)
关键:ForEach 每次迭代都创建一个新的闭包,捕获当时的 col 值。第 0 列的回调里 col = 0,第 4 列的回调里 col = 4——闭包是「按值捕获」,不是「按引用捕获」。
提示:JavaScript/TypeScript 用
var时会有「闭包共享变量」的坑,但 ArkTS 的ForEach第二参数(col: number) => {...}每次迭代都传一个新的col,所以不会共享。
3.3 onClick 触发时机
ArkUI 的 onClick 是点击抬起时触发(down → up 算一次 click):
| 手势阶段 | 触发回调 |
|---|---|
| 手指按下(down) | onTouch 的 Down |
| 手指移动(move) | onTouch 的 Move |
| 手指抬起(up) | onClick 触发 |
实战经验:长按要响应用 onTouch + 计时器,单纯点击用 onClick 就够了。本系列第 100 篇会专讲 Menu 长按菜单。
四、回调里 this 绑定(箭头函数)
4.1 箭头函数 this 保留外层
.onClick(() => {
this.handleColumnClick(col); // this 指向 Index struct
})
这里用箭头函数 () => {...},箭头函数没有自己的 this,继承外层作用域的 this。在 ArkUI 的 @Component 里,外层 this 就是组件实例(Index struct),所以能直接调 this.handleColumnClick。
4.2 普通函数 this 丢失(踩坑)
// ❌ 错误:普通函数 this 指向触发事件的组件,不是 Index
.onClick(function () {
this.handleColumnClick(col); // this 不是 Index,报错
})
// ✅ 正确:箭头函数保留外层 this
.onClick(() => {
this.handleColumnClick(col);
})
// ✅ 也可以:用 bind 显式绑定
.onClick(function () {
this.handleColumnClick(col);
}.bind(this))
实战经验:ArkUI 事件回调统一用箭头函数——避免 this 丢失。本系列第 38 篇会专讲箭头函数 this 绑定。
4.3 handleColumnClick 的实现
// 来源: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();
}
}
拆解:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 1 | if (this.gameState !== PLAYING) return |
仅在游戏中接受点击 |
| 2 | this.gameEngine.dropCat(column) |
引擎投放猫到第 column 列 |
| 3 | this.cats = this.gameEngine.getAllCats() |
同步最新猫数组到 state |
| 4 | this.nextCatLevel = ... |
更新下一只猫预览 |
关键经验:UI 层只负责调引擎、同步 state,不直接改棋盘逻辑——这是 MVC 分层的最小实践。
五、点击层与渲染层的 z 序管理
5.1 Stack 后声明覆盖前声明
Stack() {
Column() { /* 背景网格 */ } // 第 1 声明:底层
ForEach(this.cats, ...) { /* 猫咪 */ } // 第 2 声明:中层
Row() { /* 透明点击层 */ } // 第 3 声明:顶层
}
Stack 默认叠层规则:后声明的子组件覆盖在先声明的子组件之上。本项目点击层最后声明,所以在最顶层——玩家点击事件先被点击层捕获,下层猫咪和背景网格感知不到点击。
5.2 显式 zIndex 控制
Stack() {
Column() { /* 背景 */ }.zIndex(0)
ForEach(...) { /* 猫咪 */ }.zIndex(1)
Row() { /* 点击层 */ }.zIndex(2)
}
显式 zIndex 比「声明顺序」更清晰,团队协作时建议显式写。
5.3 弹窗层叠在所有之上
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%')
}
外层 Stack 里:游戏页先声明,弹窗后声明——弹窗永远覆盖游戏页,无论游戏页内部点击层 zIndex 多高,弹窗都在它之上。本系列第 93 篇会专讲暂停遮罩。
六、完整代码:列点击层
// 来源:entry/src/main/ets/pages/Index.ets GameView() 第 3 层
@Builder
GameView() {
Column() {
this.GameHUD()
Column() {
// 预告区(第 12 篇讲)
Row() { /* ... */ }
// 游戏棋盘 Stack(3 层叠)
Stack() {
// 第 1 层:棋盘背景网格(第 13 篇讲)
Column() { /* ForEach rows × cols */ }
// 第 2 层:猫咪渲染(第 15、16 篇讲)
ForEach(this.cats, (cat: Cat) => {
Column() { Text(CatConfig[cat.level].emoji) }
.position({ x: ..., y: ... })
.animation({ duration: 100 })
}, (cat: Cat) => cat.id)
// 第 3 层:列点击层(本篇重点)
Row() {
ForEach(this.cols, (col: number) => {
Column()
.width(GameConfig.CELL_SIZE) // 60vp 宽
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) // 480vp 高
.backgroundColor('rgba(0,0,0,0)') // 完全透明
.onClick(() => {
this.handleColumnClick(col); // 投放到第 col 列
})
}, (col: number) => `click_${col}`)
}
}
.width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE) // 300vp
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) // 480vp
.borderRadius(12)
.clip(true)
.backgroundColor('#D6EEF5')
}
.alignItems(HorizontalAlign.Center)
Spacer()
// 底部控制栏(第 18 篇讲)
Row() { /* 暂停 / 重新开始 */ }
}
}
handleColumnClick(column: number) {
if (this.gameState !== GameState.PLAYING) return;
if (this.gameEngine.dropCat(column)) {
this.cats = this.gameEngine.getAllCats();
this.nextCatLevel = this.gameEngine.getNextCatLevel();
}
}
七、踩坑提示
7.1 透明点击层挡住下层动画的点击
// ❌ 错误:背景网格也想响应点击,但被点击层挡了
Stack() {
Column() { /* 背景网格 */ }.onClick(() => { /* 想响应 */ })
Row() { /* 透明点击层 */ } // 后声明,挡住背景的 onClick
}
解法:把背景的 onClick 删掉,点击层统一处理;或者点击层用 .hitTestBehavior(HitTestMode.Transparent) 让点击穿透到下层。
7.2 onClick 在 Row 上不触发
// ❌ 错误:onClick 挂在 Row 外壳,但 Row 内的 Column 占满了空间
Row() {
ForEach(this.cols, (col: number) => {
Column().width(CELL_SIZE).height(480)
}, ...)
}.onClick(() => { /* 永远不触发 */ })
原因:ArkUI 的 onClick 只在组件自身的可点击区域触发,子组件占据的区域点击事件归子组件。Row 内的 Column 占满了 Row,所以 Row 的 onClick 永远不触发。
解法:把 onClick 挂在每个 Column 上(本项目做法),或者把 Row 改成 .hitTestBehavior(HitTestMode.Block) 强制拦截。
7.3 闭包 col 共享的坑
// ❌ 错误(JS 经典坑):用 var 共享变量
for (var col = 0; col < 5; col++) {
Column().onClick(() => { this.handleColumnClick(col) })
}
// 5 个 onClick 都捕获 col=5(循环结束后的值)
ArkTS 中不会发生:ArkTS 默认禁 var,强制用 let/const,且 ForEach 的迭代变量是每次新建。本系列第 38 篇会专讲箭头函数与闭包。
八、调试技巧
- 临时给点击层加半透明背景:
.backgroundColor('rgba(255,0,0,0.3)'),看清 5 列热区是否等宽对齐棋盘格。 - onClick 里打 log:
console.info('click col', col),追点击是否正确分发到对应列。 - 点击无响应排查:检查
gameState === PLAYING;检查上层是否有弹窗Stack挡住;检查.hitTestBehavior。 - 触摸坐标读不到:
onClick的 event 参数在 ArkUI 严格模式下需要显式类型GestureEvent,否则 TS 报错。
九、性能与最佳实践
- 点击层透明用 Color.Transparent,不要用
.opacity(0)——后者会让组件不可点击。 - 5 列热区用 5 个 Column + 独立 onClick,逻辑清晰;列数多用单 Stack 算列号。
- 箭头函数保留外层 this,回调里能直接调
this.handleColumnClick。 - 闭包按值捕获 col,ForEach 每次迭代都传新的 col,不会共享。
- z 序用显式 zIndex,比声明顺序更易维护,团队协作首选。
- MVC 分层:UI 只调引擎 + 同步 state,不写棋盘逻辑——本系列第 111 篇会专讲 GameEngine 引擎类。
十、阶段一收尾:UI 组件与布局实战总结
本篇是阶段一「UI 组件与布局实战(1-30)」中棋盘渲染的最后一篇。回顾阶段一已覆盖的内容:
| 篇 | 主题 | 核心组件 |
|---|---|---|
| 1-5 | 主菜单排版 | Text、Button、Spacer、Column |
| 6-10 | 主菜单视觉 | linearGradient、justifyContent、Row、Stack、if/else |
| 11-12 | 游戏页 HUD + 预告 | GameHUD、Circle 预告 |
| 13-14 | 棋盘背景 | ForEach 双层循环、隔行变色 |
| 15-16 | 猫咪渲染 | Emoji Text、Circle 背景、position 绝对定位 |
| 17(本篇) | 列点击层 | 透明 onClick 覆盖层 |
接下来第 18-20 篇会继续收尾游戏页:底部控制栏、暂停按钮样式、SymbolGlyph 系统图标。然后进入阶段一中后段:RelativeContainer、Blank、layoutWeight、displayPriority、Scroller、Badge、Image 资源、暗色模式、i18n、AttributeModifier。
总结
本篇我们从列点击层切入,掌握了透明覆盖层设计(5 个等宽透明 Column)、onClick 闭包按值捕获 col、箭头函数保留外层 this、Stack 后声明覆盖前声明的 z 序四大要点,并给出了列点击层 + handleColumnClick 的完整代码。核心要点:点击层透明且置于渲染层之上;onClick 用箭头函数保留 this;闭包按值捕获列号;UI 只调引擎不写逻辑。
下一篇我们将继续游戏页,拆解底部控制栏——Row 双按钮布局(暂停 / 重新开始)。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - onClick 通用点击事件官方指南
- hitTestBehavior 触摸测试官方指南
- ForEach 循环渲染官方指南
- ArkUI 手势处理最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md
更多推荐



所有评论(0)