前言前面我们用onClick处理点击——但onClick只是「抬起」一次触发捕获不到「按下」「移动」「长按」这些中间过程。实战中很多交互要全过程响应按下时高亮、移动时拖拽、抬起时复位——比如拖动猫猫、长按出菜单、滑动手势切换。HarmonyOS 的onTouch事件能拿到完整手势三阶段Down/Move/Up是手势交互的根基。本篇以「猫猫大作战」拖动列投放 长按菜单为预演场景把onTouch 三阶段触发、TouchType 类型判定、TouchObject 坐标信息、与 onClick 的取舍四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–50 篇。本篇是阶段三第一篇进入「交互与动画」阶段。一、场景拆解手势全过程「猫猫大作战」当前用onClick做列投放第 37 篇——点击某列投放猫。但玩家想要更自然的交互按下某列时高亮预览提示「要投这里」按住拖动到别列改主意改列抬起时投放确认列投放长按 800ms出菜单选猫等级/清空该列onClick只能拿到「抬起」一次做不了这些。要onTouch// 预演列点击层用 onTouch 替 onClick 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)) .onTouch((event: TouchEventObject) { switch (event.type) { case TouchType.Down: // 按下高亮 this.highlightCol col; this.startLongPressTimer(col); break; case TouchType.Move: // 移动暂不处理列投放不拖动 break; case TouchType.Up: // 抬起投放 清高亮 this.clearLongPressTimer(); if (this.highlightCol col) { this.handleColumnClick(col); } this.highlightCol -1; break; } }) }, (col: number) touch_${col})核心问题onTouch的三阶段Down/Move/Up分别在何时触发TouchType怎么判定当前阶段TouchObject的x/y是相对谁坐标onTouch和onClick怎么选二、onTouch 三阶段触发2.1 触发时机手指按下down→ onTouch(TouchType.Down) ↓ 手指移动move→ onTouch(TouchType.Move) 每移动一点触发一次高频 ↓ 手指抬起up→ onTouch(TouchType.Up) ↓ 之后如果算一次完整点击→ onClick阶段TouchType触发时机频率按下Down手指接触屏幕1 次移动Move手指在屏幕上移动高频每像素级抬起Up手指离开屏幕1 次关键经验onTouch 三阶段覆盖完整手势——Down 开始、Move 持续、Up 结束onClick 只在 Up 后「算一次点击」时触发丢掉了 Down/Move。2.2 事件回调签名.onTouch((event: TouchEventObject) { // event.type当前阶段Down/Move/Up // event.touches所有触摸点数组多指时多个 // event.changedTouches本阶段变化的触摸点 // event.time戳时间戳 // event.source事件源TouchScreen 等 })2.3 单指 vs 多指// 单指touches 和 changedTouches 都只有 1 个元素 const touch event.touches[0]; // 取第 0 指 // 多指如双指缩放touches 有多个 for (const touch of event.touches) { /* 处理每指 */ }实战经验消除类游戏只处理单指——取event.touches[0]即可图像缩放/地图才处理多指。三、TouchType 类型判定3.1 TouchType 枚举enum TouchType { Down 0, // 按下 Up 1, // 抬起 Move 2 // 移动 }判定当前阶段.onTouch((event: TouchEventObject) { switch (event.type) { case TouchType.Down: console.info(按下); break; case TouchType.Move: console.info(移动); break; case TouchType.Up: console.info(抬起); break; } })3.2 三阶段的典型副作用阶段典型副作用场景Down高亮、启动长按计时器、记录按下坐标按钮反馈、长按菜单、拖拽起点Move更新拖拽位置、判定滑动方向拖拽、滑动切换、手势识别Up复位高亮、清长按计时器、确认操作按钮复位、投放确认、滑动结束3.3 长按菜单的实现private longPressTimer: number -1; startLongPressTimer(col: number): void { this.longPressTimer setTimeout(() { // 800ms 后出菜单 this.showColumnMenu(col); }, 800); } clearLongPressTimer(): void { if (this.longPressTimer ! -1) { clearTimeout(this.longPressTimer); this.longPressTimer -1; } } showColumnMenu(col: number): void { // 弹出菜单第 54 篇会专讲 bindContextMenu console.info(长按列 ${col}出菜单); }流程Down →startLongPressTimer启动 800ms 计时。如果 800ms 内 Up提前抬起→clearLongPressTimer清计时算普通点击投放。如果 800ms 内没 Up长按住→ 计时到期触发showColumnMenu出菜单。关键经验长按 Down 启动计时 Up 清计时——Up 先于计时到期算点击计时先于 Up 算长按。本系列第 54 篇会专讲 bindContextMenu。四、TouchObject 坐标信息4.1 TouchObject 字段interface TouchObject { id: number; // 触摸点 id多指区分 x: number; // X 坐标 y: number; // Y 坐标 offsetX: number; // X 偏移相对上次 offsetY: number; // Y 偏移 offsetXFromStart: number; // 相对 Down 的 X 偏移 offsetYFromStart: number; // 相对 Down 的 Y 偏移 pressedTime: number; // 按下时间戳 // 等更多 }4.2 x/y 坐标系.onTouch((event: TouchEventObject) { const touch event.touches[0]; console.info(x: ${touch.x}, y: ${touch.y}); })坐标系x/y是相对触发 onTouch 的组件左上角的局部坐标。坐标含义(0, 0)触组件左上角(touch.x, touch.y)相对左上角的偏移超出组件范围也有效手指移出组件但还按着关键经验onTouch 坐标是相对组件局部的——要全局坐标用event.touches[0].xToScreen如果有或自己累加。4.3 offsetXFromStart 判定滑动.onTouch((event: TouchEventObject) { const touch event.touches[0]; switch (event.type) { case TouchType.Up: const dx touch.offsetXFromStart; const dy touch.offsetYFromStart; if (Math.abs(dx) 50 Math.abs(dy) 30) { // X 方向偏移 50 且 Y 偏移小判定左右滑动 if (dx 0) { console.info(右滑); } else { console.info(左滑); } } break; } })关键经验offsetXFromStart/offsetYFromStart是相对 Down 起点的总偏移——用来判定滑动方向最方便。4.4 pressedTime 判定长按.onTouch((event: TouchEventObject) { const touch event.touches[0]; switch (event.type) { case TouchType.Up: const duration Date.now() - touch.pressedTime; if (duration 800) { console.info(长按不靠 setTimeout 也能判定); } break; } })实战经验长按判定两种方式——Down 启动 setTimeout实时出菜单或 Up 时算Date.now() - pressedTimeUp 时判定。前者用户体验更即时800ms 到就出菜单后者要等 Up。五、onTouch vs onClick 取舍5.1 onClick 的局限.onClick(() { // 只在「完整点击」down up 且没移动太多后触发 // 拿不到 down 时机、拿不到 move 坐标、拿不到长按时长 })5.2 onTouch 的全面.onTouch((event: TouchEventObject) { // 三阶段都能响应能拿坐标、时长、偏移 })5.3 取舍决策要响应「按下」「移动」「长按」中间过程 ├─ 是 → onTouch └─ 否只要「抬起点击」一次触发 └─ onClick更简洁场景推荐普通按钮点击onClick列投放抬起确认onClick第 37 篇用的拖拽按下 移动 抬起onTouch长按菜单onTouch setTimeout滑动手势onTouch offsetXFromStart双指缩放onTouch 多指关键经验简单点击用 onClick全过程手势用 onTouch——onClick 是 onTouch 的「Up 简化版」。5.4 同时用会怎样Column() .onClick(() { console.info(click); }) .onTouch((event) { if (event.type TouchType.Up) { console.info(touch up); } }) // 抬起时先触发 onTouch(Up)再触发 onClick // 两者不冲突但通常没必要同时用实战经验通常只用一个——onClick 或 onTouch同时用要小心重复触发。六、完整预演代码拖动列投放 长按菜单// 预演Index 改造 onTouch 列投放不替换第 37 篇的 onClick作为预演 Entry Component struct Index { State gameState: GameState GameState.IDLE; State cats: Cat[] []; State highlightCol: number -1; // ← onTouch 高亮列 /* ... 其他 state */ private gameEngine: GameEngine new GameEngine(); private longPressTimer: number -1; 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(); } } // onTouch 用启动长按计时 startLongPressTimer(col: number): void { if (this.longPressTimer ! -1) clearTimeout(this.longPressTimer); this.longPressTimer setTimeout(() { this.showColumnMenu(col); }, 800); } clearLongPressTimer(): void { if (this.longPressTimer ! -1) { clearTimeout(this.longPressTimer); this.longPressTimer -1; } } showColumnMenu(col: number): void { // 弹出列菜单第 54 篇会专讲 bindContextMenu console.info(长按列 ${col}出菜单); } Builder GameView() { Column() { this.GameHUD() Column() { Row() { /* 预告区 */ } Stack() { /* 棋盘背景 */ ForEach(this.cats, (cat: Cat) { Column() { /* ... position ... */ } .position({ x: /* ... */, y: /* ... */ }) .animation({ duration: 100 }) }, (cat: Cat) cat.id) // 列点击层onTouch 替 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)) .onTouch((event: TouchEventObject) { // ← onTouch switch (event.type) { case TouchType.Down: // 按下 this.highlightCol col; // 高亮 this.startLongPressTimer(col); // 启长按计时 break; case TouchType.Move: // 移动 // 列投放不拖动暂不处理 break; case TouchType.Up: // 抬起 this.clearLongPressTimer(); // 清长按计时 if (this.longPressTimer -1) { // 计时已清没出菜单算普通点击投放 // ⚠️ 注意如果长按菜单已出这里不再投放 } else { // 简化只要抬起就投放实际要区分长按是否已触发菜单 } if (this.highlightCol col) { this.handleColumnClick(col); } this.highlightCol -1; // 清高亮 break; } }) }, (col: number) touch_${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) } /* GameHUD / MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */ }七、踩坑提示7.1 onTouch 用普通函数丢 this// ❌ 错误普通函数 this 不指向组件 .onTouch(function (event) { this.highlightCol col; }) // ✅ 正确箭头函数保留 this第 38 篇讲过 .onTouch((event: TouchEventObject) { this.highlightCol col; })7.2 忘清长按计时器// ❌ 错误Down 启动计时但 Up 忘清导致抬起后还出菜单 case TouchType.Down: this.startLongPressTimer(col); break; // 忘了 case TouchType.Up: this.clearLongPressTimer(); // ✅ 正确Up 清计时 case TouchType.Up: this.clearLongPressTimer(); // ... break;7.3 Move 阶段做重计算// ❌ 错误Move 高频触发做重计算会卡 case TouchType.Move: this.heavyCalc(); // 每像素级都调卡死 // ✅ 正确Move 轻量重计算延迟到 Up case TouchType.Move: this.lastMoveX event.touches[0].x; // 只记录 break; case TouchType.Up: this.heavyCalc(this.lastMoveX); // Up 时才算 break;7.4 多指取错触摸点// ❌ 错误多指时取 touches[0] 可能不是当前变化指 const touch event.touches[0]; // ✅ 正确用 changedTouches 取本阶段变化的指 const touch event.changedTouches[0];7.5 坐标系误解// ❌ 错误以为 touch.x 是全局坐标 const globalX touch.x; // 实际是相对组件左上角 // ✅ 正确知道是局部坐标 const localX touch.x; // 相对组件左上角 // 要全局坐标自己累加或用 screen 系 API ## 八、调试技巧 1. **console.info 打 event.type**追三阶段触发时机。 2. **console.info 打 touch.x/y**追坐标验证是否相对组件局部。 3. **长按菜单不出排查**检查 Down 是否启动计时检查 Up 是否过早清了计时计时还没到期就清。 4. **长按菜单抬起了还出排查**检查 Up 是否清计时检查 setTimeout 是否清成功。 ## 九、性能与最佳实践 1. **简单点击用 onClick全过程手势用 onTouch**——onClick 是 onTouch 的 Up 简化版。 2. **onTouch 回调用箭头函数保留 this**——普通函数 this 丢失第 38 篇。 3. **Down 启动计时Up 清计时**——长按菜单的标准模式Up 必清避免泄漏。 4. **Move 阶段别做重计算**——高频触发只记录坐标重计算延迟到 Up。 5. **单指取 touches[0]多指用 changedTouches**——区分当前变化的指。 6. **坐标是相对组件局部的**——要全局自己累加或用 screen API。 ## 十、阶段三进度开篇51–55 本篇是阶段三「交互与动画」第 1 篇开篇覆盖 | 篇 | 主题 | 核心要点 | |----|------|----------| | 51本篇 | onTouch | 手势三阶段 Down/Move/UpTouchType 判定坐标信息 | | 52 | onHover | 悬停事件鼠标/遥控器悬停反馈 | | 53 | onKeyEvent | 键盘/遥控器按键事件KeyCode 判定 | | 54 | bindContextMenu | 右键/长按上下文菜单 | | 55 | animateTo | 显式动画触发动画闭包 | 接下来阶段三还会覆盖animation 属性动画、Hero 共享元素、transition 转场、spring 物理、Hero Style Player 等。 ## 总结 本篇我们从 onTouch 手势切入掌握了**三阶段触发Down/Move/Up**、**TouchType 判定**、**TouchObject 坐标局部 offsetFromStart**、**与 onClick 的取舍**四大要点并给出了拖动列投放 长按菜单的完整预演代码。核心要点**onClick 只触发抬起onTouch 覆盖全过程Down 启计时 Up 清计时做长按Move 轻量重计算延到 Up坐标相对组件局部**。 下一篇我们将拆解 onHover——悬停事件。 如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力 --- **相关资源** - 「猫猫大作战」项目源码本仓库 entry/src/main/ets/pages/Index.ets - [ArkUI onTouch 触摸事件官方指南](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-common-events-touch) - [TouchType 枚举官方文档](https://developer.huawei.com/consumer/cn/references/ts-touch-type) - [TouchEventObject 接口官方文档](https://developer.huawei.com/consumer/cn/references/ts-touch-event) - [HarmonyOS 手势处理最佳实践](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/bpta-gesture-processing) - [开源鸿蒙跨平台社区](https://openharmonycrossplatform.csdn.net) - [HarmonyOS 开发者官方文档首页](https://developer.huawei.com/consumer/cn/doc/) - 系列索引本仓库 articles/INDEX.md