当前位置: 首页 > news >正文

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}`)

核心问题

  1. onTouch的三阶段(Down/Move/Up)分别在何时触发?
  2. TouchType怎么判定当前阶段?
  3. TouchObjectx/y是相对谁坐标?
  4. onTouchonClick怎么选?

二、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`
http://www.cnnetsun.cn/news/3697512.html

相关文章:

  • PCA算法在三维点云平面拟合中的原理与实践
  • K210开发实战:从硬件连接到AI模型部署的完整排错指南
  • DDD视角下的Openfeign设计与实践
  • 基于ESP32的四足机器人DIY:从硬件选型到步态算法全解析
  • LiveKit终极指南:5分钟搭建企业级实时音视频服务器
  • 变异粒子群算法在主动配电网故障恢复中的应用与Matlab实现
  • 当我把 Docker 迁移交给 AI 之后……符号链接的致命陷阱
  • 装Office被坑过的,这个10MB小工具能救命!
  • AI聊天应用开发实战:从AnuNeko关闭看技术架构与成本优化
  • Chili3D:基于WebAssembly的浏览器端3D CAD技术深度解析
  • 解决Blur常见问题:消除运动模糊中的拖影 artifacts 实用技巧
  • 终极开源预测引擎:MiroFish群体智能实战指南
  • Python控制乐高EV3机器人:从环境搭建到自动避障项目实战
  • MiroFish完整指南:三步开启未来预测革命,用群体智能引擎看见每一个“如果“
  • Linux中断处理中的Tasklet机制详解
  • C++文件操作类封装:RAII设计、跨平台实现与性能优化实战
  • AI驱动数据仓库模型评审:LLM技术实践与效率提升
  • 10机39节点电力系统仿真建模与Matlab实践
  • 嵌入式Wi-Fi模块AT指令驱动故障排查与修复实战
  • Anime.js实战指南:深度解析现代JavaScript动画引擎的完整应用
  • .NET MAUI跨平台应用开发终极指南:10步构建原生移动与桌面应用
  • 如何用BiliBiliToolPro轻松实现B站任务自动化:从新手到高手的完整指南
  • ThinkPHP 8框架与TCP协议交互机制解析
  • Genesis机器人仿真平台:解锁下一代具身智能研究的5大核心优势
  • 乐高EV3变身智能新闻播报员:Python+百度AI语音合成实战
  • Mind+与Maixduino入门:图形化编程实现嵌入式AI视觉Hello World
  • 英雄联盟全皮肤免费体验:5分钟快速上手R3nzSkin国服换肤工具
  • 突破7B参数极限:openPangu-Embedded-7B-V1.1如何用昇腾NPU重塑边缘AI推理体验
  • 让文字开口说话:eSpeak NG语音合成引擎的奇妙世界
  • 三分钟搞定!免费开源中文字体霞鹜文楷终极安装使用指南