尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

HarmonyOS应用开发实战:猫猫大作战-onTouch 三阶段触发、TouchType 类型判定、TouchObject 坐标信息、与 onCli

HarmonyOS应用开发实战:猫猫大作战-onTouch 三阶段触发、TouchType 类型判定、TouchObject 坐标信息、与 onCli
📅 发布时间:2026/7/28 3:17:42

前言

前面我们用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}`)

核心问题:

  1. onTouch的三阶段(Down/Move/Up)分别在何时触发?
  2. TouchType怎么判定当前阶段?
  3. TouchObject的x/y是相对谁坐标?
  4. 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},出菜单`); }

流程:

  1. Down →startLongPressTimer启动 800ms 计时。
  2. 如果 800ms 内 Up(提前抬起)→clearLongPressTimer清计时,算普通点击投放。
  3. 如果 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() - pressedTime(Up 时判定)。前者用户体验更即时(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/Up,TouchType 判定,坐标信息 | | 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`

相关新闻

  • 基于Arduino与超声波传感器的智能感应灯制作与优化指南
  • ChatTTS-ui终极指南:5分钟打造本地文字转语音系统
  • 2021创客硬件年度盘点:从边缘AI到开发体验的技术演进与选型指南

最新新闻

  • ThinkAdmin快速入门指南:10分钟搭建企业级后台管理系统
  • 2026内江毛坯房配门窗推荐榜:5家门店实探,从初始配置就选对 - 家居装修资讯
  • 电动汽车V2G技术:双向充放电与电网调峰实践
  • Play Integrity Fix终极指南:让你的Android设备重新获得官方认证
  • 终极免费跨平台模拟器:mGBA带你重温经典GBA游戏 [特殊字符]
  • Linux应用商店终极指南:星火应用商店完整使用手册

日新闻

  • 力旷智能:伺服驱动系统在制药收瓶设备中的应用解析
  • 2026 网安入门避坑指南,零基础如何避开无效学习直接上手实战
  • 揭秘CFC项目:如何通过手机摄像头实现850kbps无网络文件传输

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号