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

发布时间:2026/7/28 3:17:03

HarmonyOS应用开发实战:猫猫大作战-onTouch 三阶段触发、TouchType 类型判定、TouchObject 坐标信息、与 onCli
前言前面我们用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

相关新闻

HarmonyOS应用开发实战:猫猫大作战-浅观察陷阱与嵌套对象更新

HarmonyOS应用开发实战:猫猫大作战-浅观察陷阱与嵌套对象更新

2026/7/28 3:17:03

前言 前面我们多次遇到「浅观察」——State combo: ComboInfo,改 this.combo.count 5 不触发重渲染,必须 this.combo {...} 整体赋值。这是 V1 状态管理最经典的坑——嵌套对象/数组内部属性变化监听不到。本篇专攻这个陷阱,把浅观察的本质…

HarmonyOS应用开发实战:猫猫大作战-@Link 声明与 $val 传参、双向同步机制、@Link vs @Prop vs @Event 取舍、

HarmonyOS应用开发实战:猫猫大作战-@Link 声明与 $val 传参、双向同步机制、@Link vs @Prop vs @Event 取舍、

2026/7/28 3:17:03

前言 上一篇我们用 Prop 把得分从父传给子 HUD——单向只读,子改不了父。但有些场景子组件要直接改父的数据:表单子组件改父的 username、滑块子组件改父的音量、暂停弹窗里的「继续/重开」按钮改父的 gameState。这时 Prop 不够用,要用 Lin…

影刀RPA实战教程:7个真实案例带你从入门到独立开发

影刀RPA实战教程:7个真实案例带你从入门到独立开发

2026/7/28 3:17:03

影刀RPA实战教程:7个真实案例带你从入门到独立开发 教程看了不少,一到自己写就无从下手。这很正常——RPA是做的技能,不是看的技能。这篇直接用7个真实案例串起来,从简单到复杂,每个案例都能直接拿去用或者改成你自己…

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩

2026/7/28 4:27:14

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩2026年6月,《商标法》迎来四十余年来首次全面修订,条文由73条增至87条,将于2027年1月1日起施行-1。新法在收紧注册规则的同时,也赋予了商标管理部门更多…

商标取名总是被驳回?审查员最爱3个命名套路一次讲透

商标取名总是被驳回?审查员最爱3个命名套路一次讲透

2026/7/28 4:27:14

商标取名总是被驳回?审查员最爱的3个命名套路一次讲透在深圳,商标驳回率常年居高不下。2026年新《商标法》对商标注册的审查标准进一步收紧——从囤标拦截到显著性审查,全面从严-1-5。很多创业者抱怨:“我取的名字明明很特别&…

创业公司必看:为什么说“市场未动,商标先行

创业公司必看:为什么说“市场未动,商标先行

2026/7/28 4:27:14

创业公司必看:为什么说“市场未动,商标先行”是条铁律?“公司名想好了,产品也快上线了,注册商标?不急,等做大了再说。”这是不少深圳创业者的真实想法。但这个看似“省事”的选择,可…

EVA泡沫手工制作《我的世界》钻石镐:从材料选择到上色涂装全流程解析

EVA泡沫手工制作《我的世界》钻石镐:从材料选择到上色涂装全流程解析

2026/7/28 4:27:14

1. 项目概述:从像素到现实,一把镐子的诞生如果你和我一样,既是《我的世界》的忠实玩家,又是手工爱好者,那么看到游戏里那些标志性的工具,比如一把钻石镐,心里总会痒痒的:要是能把它从…

基于MIT App Inventor与CloudDB的实时聊天应用开发实践

基于MIT App Inventor与CloudDB的实时聊天应用开发实践

2026/7/28 4:27:13

1. 项目概述:当AI遇上信使,一次低门槛的云端对话实践最近在捣鼓一个挺有意思的小项目,叫“信使”。这其实是“Hour of AI”系列教程中的一个经典案例,核心目标很简单:用最直观的方式,让你亲手搭建一个能跨设…

Langgraph Loop Control Strategies 循环控制策略

Langgraph Loop Control Strategies 循环控制策略

2026/7/28 4:17:09

Loop Control Strategies 循环控制策略1. Counter-Based Loops&#xff5c;基于计数器的循环if state["iteration"] < MAX_ITERATIONS:return "continue"逻辑&#xff1a;依靠迭代次数限制循环规则&#xff1a;如果当前迭代次数小于最大迭代上限&#x…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/27 8:45:59

目标&#xff1a;电脑作为RTSP 服务端&#xff0c;循环推送 H264/H265 视频流&#xff1b; RDK X5 通过 rtsp2display 拉流预览&#xff0c;完全不需要在开发板编译 live555。 提供两套成熟方案&#xff1a; ✅ 方案 A&#xff1a;FFmpeg&#xff08;最简单&#xff0c;优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/27 8:42:17

一、背景与测试方案 在实际项目交付中&#xff0c;PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及&#xff1a;多源PDF的文件流合并、页面级水印渲染&#xff08;含透明度混合与图层叠加&#xff09;、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/27 14:56:57

说实话&#xff0c;提到PDF拆分再压缩&#xff0c;我真是被折腾得够呛。 上个月公司年度合同归档&#xff0c;一份300多页的PDF总合同&#xff0c;需要按年份拆分成三个独立文件&#xff0c;再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单&#xff1f;先找个海…

零基础搭建桌面智能体,OpenClaw 2.7.9 分步实操,避开绝大多数部署陷阱

零基础搭建桌面智能体,OpenClaw 2.7.9 分步实操,避开绝大多数部署陷阱

2026/7/28 0:06:55

&#x1f4cc; 一、工具核心优势盘点 数据本地存储&#xff0c;安全系数高所有操作日志、文档资料均保存在本机&#xff0c;不会上传至云端&#xff0c;能够有效保护企业文件与个人隐私&#xff0c;规避数据泄露风险。 上手简单&#xff0c;零编程门槛采用全图形化可视化界面&…

计算机毕业设计之基于springboot的购物平台设计与实现

计算机毕业设计之基于springboot的购物平台设计与实现

2026/7/28 0:06:55

由于移动应用技术的持续性的快速发展&#xff0c;现实生活中人们大多数都是通过移动手机、电脑等智能设备来完成生活中的事务。因此&#xff0c;许多的人工传统行业也开始与互联网结合&#xff0c;不再一味的依靠人工手动&#xff0c;努力打造半自动数字化甚至是全自动数字化模…

豆包AI绘图提示词失效真相:NLP模型层token截断机制首次披露,3招绕过字数限制

豆包AI绘图提示词失效真相:NLP模型层token截断机制首次披露,3招绕过字数限制

2026/7/28 0:06:55

更多请点击&#xff1a; https://codechina.net 第一章&#xff1a;豆包AI绘图提示词失效现象全景扫描 近期大量用户反馈&#xff0c;豆包&#xff08;Doubao&#xff09;AI绘图功能对常规提示词&#xff08;Prompt&#xff09;响应异常&#xff1a;语义明确的指令被忽略、中英…