标签 LongPressGesture 下的文章

写在前面

在万物互联的全场景时代,智能设备的形态正从手机、平板向车载、穿戴、智能家居等多端快速延伸,用户对交互体验的要求早已突破 “能用” 的基础阈值,转而追求 “自然、无感、高效” 的跨设备操作体验。手势交互作为一种摆脱物理按键束缚的自然交互方式,凭借其直观性和沉浸感,已成为 HarmonyOS 构建全场景生态的核心交互语言。HarmonyOS作为新一代面向万物互联的操作系统,不仅重构了多设备协同的底层逻辑,更在手势识别能力上完成了升级,它提供了一套轻量化、高适配的手势开发框架,让开发者仅通过几行代码就能实现丰富的手势交互,从而在不同设备上打造统一且流畅的用户体验。单一手势是 HarmonyOS 手势体系的基础单元,所有复杂的组合手势都由它演化而来。本文将从技术原理入手,系统拆解单一手势的类型、实现逻辑与设计原则,并结合真实场景代码案例,帮助开发者快速掌握这一核心能力,为打造全场景交互体验筑牢基础。

image.png

单一手势什么是?

在 HarmonyOS 的交互框架中,系统将通用输入事件划分为触屏、键鼠、焦点及拖拽等核心类型。而手势交互则是通过 “手势绑定方法 + 具体手势实例” 的组合来实现,根据交互复杂度又可细分为单一手势与组合手势两大类别。
单一手势是构成所有复杂交互的 “原子单元”,它仅通过单次、独立的触摸动作(如点击、滑动、按压)触发功能,是组合手势的基础组件,也是开发者入门 HarmonyOS 交互开发的第一课。

手势操作的类型及实现

关于单一手势操作的类型有点击、长按、拖动、捏合、旋转、滑动六大类型,具体实现如下所示。

1、点击手势(TapGesture):交互的起点

点击手势是最基本的手势操作,支持单次点击和多次点击。通过TapGesture可以轻松实现按钮点击、菜单打开等功能,以下代码展示了如何实现一个双击操作:

@Entry
@Component
struct Index {
  @State value: string = "";

  build() {
    Column() {
      Text('双击').fontSize(28)
        .gesture(
          TapGesture({ count: 2 }) // 绑定双击事件,绑定count为2的TapGesture
            .onAction((event: GestureEvent|undefined) => {
              this.value = JSON.stringify(event.fingerList[0]);
            }))
      Text(this.value)
    }
    .height(200)
    .width(250)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

2、长按手势(LongPressGesture):触发深度操作

长按手势常用于触发 “二次确认” 类功能,如长按复制、弹出操作菜单等,支持重复触发模式以实现连续交互。长按手势用于触发长按手势事件,通过LongPressGesture可以轻松实现长按复制等功能,以下代码展示了如何实现在Text组件上绑定可以重复触发的长按手势:

@Entry
@Component
struct Index {
  @State count: number = 0;

  build() {
    Column() {
      Text('长按').fontSize(28)
        .gesture(
          // 绑定可以重复触发的LongPressGesture
          LongPressGesture({ repeat: true })
           .onAction((event: GestureEvent|undefined) => {
              if(event){
                if (event.repeat) {
                  this.count++;
                }
              }
            })
            .onActionEnd(() => {
              this.count = 0;
            })
        )
    }
    .height(200)
    .width(250)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

3、拖动手势(PanGesture):实现元素自由位移

拖动手势在滑动距离超过系统阈值(默认 5vp)时触发,常用于实现组件拖拽、位置调整等交互,核心是通过回调实时更新布局参数。拖动手势用于触发拖动手势事件,以下代码展示了在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动:

@Entry
@Component
struct Index {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;

  build() {
    Column() {
      Text('拖动')
        .fontSize(28)
        .height(200)
        .width(300)
        .padding(20)
        .border({ width: 3 })
          // 在组件上绑定布局位置信息
        .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
        .gesture(
          // 绑定拖动手势
          PanGesture()
           .onActionStart((event: GestureEvent|undefined) => {
            })
              // 当触发拖动手势时,根据回调函数修改组件的布局位置信息
            .onActionUpdate((event: GestureEvent|undefined) => {
              if(event){
                this.offsetX = this.positionX + event.offsetX;
                this.offsetY = this.positionY + event.offsetY;
              }
            })
            .onActionEnd(() => {
              this.positionX = this.offsetX;
              this.positionY = this.offsetY;
            })
        )
    }
    .height(200)
    .width(250)
  }
}

4、捏合手势(PinchGesture):控制元素缩放

捏合手势通过识别多指(支持 2-5 指)的距离变化计算缩放比例,是图片浏览、地图缩放等场景的核心交互方式。捏合手势用于触发捏合手势事件,常用于图片和地图的缩放操作,这里举例以在Column组件上绑定三指捏合手势为例,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大:

@Entry
@Component
struct Index {
  @State scaleValue: number = 1;
  @State pinchValue: number = 1;
  @State pinchX: number = 0;
  @State pinchY: number = 0;

  build() {
    Column() {
      Column() {
        Text('捏合')
      }
      .height(200)
      .width(300)
      .border({ width: 3 })
      .margin({ top: 100 })
      // 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大
      .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
      .gesture(
        // 在组件上绑定三指触发的捏合手势
        PinchGesture({ fingers: 3 })
          .onActionStart((event: GestureEvent|undefined) => {
          })
            // 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例
          .onActionUpdate((event: GestureEvent|undefined) => {
            if(event){
              this.scaleValue = this.pinchValue * event.scale;
              this.pinchX = event.pinchCenterX;
              this.pinchY = event.pinchCenterY;
            }
          })
          .onActionEnd(() => {
            this.pinchValue = this.scaleValue;
          })
      )
    }
  }
}

5、旋转手势(RotationGesture):实现元素角度调整

旋转手势通过识别触摸点的角度变化计算旋转值,常用于图片编辑、表盘调整等需要角度控制的场景。旋转手势用于触发旋转手势事件,这里以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转:

@Entry
@Component
struct Index {
  @State angle: number = 0;
  @State rotateValue: number = 0;

  build() {
    Column() {
      Text('旋转').fontSize(28)
        // 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转
        .rotate({ angle: this.angle })
        .gesture(
          RotationGesture()
           .onActionStart((event: GestureEvent|undefined) => {
            })
              // 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度
            .onActionUpdate((event: GestureEvent|undefined) => {
              if(event){
                this.angle = this.rotateValue + event.angle;
              }
            })
              // 当旋转结束抬手时,固定组件在旋转结束时的角度
            .onActionEnd(() => {
              this.rotateValue = this.angle;
            })
            .onActionCancel(() => {
            })
        )
        .height(200)
        .width(300)
        .padding(20)
        .border({ width: 3 })
        .margin(100)
    }
  }
}

6、滑动手势(SwipeGesture):触发页面级交互

滑动手势在滑动速度超过 100vp/s 时触发,支持上下左右四个方向,是列表滚动、页面切换等场景的核心交互。滑动手势用于触发滑动事件,可以实现上下左右滑动操作,常用于列表滚动和页面切换,以在Column组件上绑定滑动手势实现组件的旋转为例:

@Entry
@Component
struct Index {
  @State rotateAngle: number = 0;
  @State speed: number = 1;

  build() {
    Column() {
      Column() {
        Text('滑动')
      }
      .border({ width: 3 })
      .width(300)
      .height(200)
      .margin(100)
      // 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度
      .rotate({ angle: this.rotateAngle })
      .gesture(
        // 绑定滑动手势且限制仅在竖直方向滑动时触发
        SwipeGesture({ direction: SwipeDirection.Vertical })
          // 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改
          .onAction((event: GestureEvent|undefined) => {
            if(event){
              this.speed = event.speed;
              this.rotateAngle = event.angle;
            }
          })
      )
    }
  }
}

手势操作的设计原则

优质的手势交互不仅需要技术实现,更需要遵循用户体验设计的底层逻辑,核心原则可归纳为两点:自然直观和一致性。

1、自然直观

手势设计应模拟现实世界中的操作习惯,让用户能够凭借直觉进行交互,比如从屏幕底部边缘向上滑动返回主屏幕,这一操作类似于翻开书页的动作。

2、一致性

在整个HarmonyOS生态系统中,手势操作的含义和效果应保持一致,比如双指缩放手势在不同应用中都应实现放大或缩小的功能。

实战落地:单一手势在场景化开发中的典型应用

这里举两个简单的实用示例,方便各位学习使用。

1、音乐播放器应用

在音乐播放器应用中,用户可以通过左右滑动切换歌曲,双指缩放调整封面大小,以下代码展示了如何实现左右滑动切换歌曲:

@Entry
@Component
struct MusicPlayerSwipe {
  @State currentSongIndex: number = 0;
  @State songs: string[] = ['Song 1', 'Song 2', 'Song 3'];

  build() {
    Column() {
      Text(this.songs[this.currentSongIndex])
    }
    .swipe({
      start: SwipeDirection.Left,
      onSwipe: () => {
        if (this.currentSongIndex < this.songs.length - 1) {
          this.currentSongIndex++;
        }
      }
    })
    .swipe({
      start: SwipeDirection.Right,
      onSwipe: () => {
        if (this.currentSongIndex > 0) {
          this.currentSongIndex--;
        }
      }
    })
  }
}

2、手势截屏

手势截屏是另一个实用的功能,用户可以通过下滑手势调用全屏截图功能,通过双击手势调用区域截图功能,以下代码展示了如何实现全屏截图:

Stack() {
  Column() {
    ...
  }
  .gesture(
    PanGesture({
      fingers: 1,
      direction: PanDirection.Down,
      distance: CommonConstants.MINIMUM_FINGER_DISTANCE
    })
      .onActionStart(() => {
        let screenshotOptions: screenshot.ScreenshotOptions = {
          rotation: 0
        };
        screenshot.save(screenshotOptions, (err: Error, data: image.PixelMap) => {
          if (err) {
            Logger.error(`Failed to save the screenshot. Error:${JSON.stringify(err)}`);
          }
          if (this.pixelMap !== undefined) {
            this.pixelMap.release();
          }
          this.pixelMap = data;
          this.dialogController.open();
        });
      })
  )
}

最后

随着 HarmonyOS 6 的发布,全场景交互的边界正在被重新定义,单一手势作为交互体系的 “原子单元”,不仅是开发者构建基础交互的必备技能,更是打造复杂组合手势、实现多设备协同交互的核心基石。本文从技术原理、类型实现、设计原则到场景实战,系统拆解了 HarmonyOS 单一手势的开发逻辑。这些看似基础的交互能力,恰恰是构建 “自然、无感、高效” 全场景体验的关键:无论是手机端的流畅操作,还是车载、穿戴设备的极简交互,都离不开对单一手势的精准把控。对于 HarmonyOS 开发者而言,掌握单一手势的底层逻辑与实践技巧,不仅能提升应用的用户体验,更能为未来探索多设备协同手势、AI 增强手势等进阶能力筑牢基础。在万物互联的浪潮中,唯有以用户体验为核心,以技术实力为支撑,才能打造出真正适配全场景的优质应用。希望本文能成为你探索 HarmonyOS 交互开发的起点,在构建智能生态的道路上持续进阶。

在 ArkUI 里,单个手势(点击、长按、滑动、缩放…)已经够好用,但一旦你要做这种交互:

  • 长按后才能拖动
  • 同一区域支持 单击 / 双击 且行为不同
  • 两个手势要 同时识别 或互斥

就会发现仅靠 TapGesture / PanGesture 这些基础手势不太好管理——这时候就轮到主角 GestureGroup 登场了。

本文定位就是一篇可以直接发社区的实战向自学笔记,按这几个问题展开:

  1. GestureGroup 是什么?解决什么问题?
  2. 三种 GestureMode 到底怎么选?
  3. 如何正确组合单击 / 双击、长按 + 拖动?
  4. onCancel 在真实项目中有什么用?

一、GestureGroup 是什么?

官方一句话定义:

GestureGroup 用来把多个基础手势组合在一起,根据指定的识别模式统一管理。
  • API Version 7 开始支持
  • 元服务 从 API 11 开始支持
  • 系统能力:SystemCapability.ArkUI.ArkUI.Full

核心接口只有一个:

GestureGroup(mode: GestureMode, ...gesture: GestureType[])

参数说明:

  • mode: GestureMode(必填)
    组合手势的“识别策略”,即三种模式:Sequence / Parallel / Exclusive
  • ...gesture: GestureType[](可选)

    • 一个或多个基础手势实例(TapGestureLongPressGesturePanGesture 等)
    • 如果这里不填,那这个 GestureGroup 相当于白写,组合识别不生效
⚠️ 官方特别说明:
当一个组件要同时支持 单击 + 双击 时,必须把双击放前面,单击放后面,才能正确识别。

二、GestureMode 三种模式,搞清区别就成功一半

GestureMode 枚举定义了组合手势的识别方式:

enum GestureMode {
  Sequence,   // 顺序识别
  Parallel,   // 并发识别
  Exclusive   // 互斥识别
}

2.1 Sequence:顺序识别(默认值)

按照注册顺序,一个一个识别。前面的失败,后面的都不会触发。

特点:

  • 只有当 前一个手势识别完成,才会进入下一个手势识别;
  • 任意一个中途失败,后面的通通不再识别;
  • 在顺序识别模式下,只有最后一个手势能触发 onActionEnd 事件

典型场景:

  • 长按后才允许拖动(长按没触发,就不让拖)
  • 双击成功则不再触发单击回调
  • 复杂手势链:长按 → 拖动 → 抬手触发某种状态收束

2.2 Parallel:并发识别

所有手势同时识别,互不干扰。

特点:

  • 注册的所有手势“并行”识别;
  • 各自成功或失败 互不影响
  • 适合“多个手势可以同时成立”的场景。

典型场景:

  • 同一组件既要识别 PinchGesture(缩放)又要识别 RotateGesture(旋转);
  • 类似“边拖动边缩放”的复杂交互。

2.3 Exclusive:互斥识别

所有手势一起识别,谁先成功,就“赢”,其余都视为失败。

特点:

  • 有点像“抢占式”的识别模式;
  • 一旦其中一个手势识别成功:

    • 其他手势立即失败;
    • 结束整个组合手势识别。

典型场景:

  • 同区域要么触发“滑动删除”,要么触发“点击打开”,不能两者都触发;
  • 导航区域:水平滑动切换 Tab vs 垂直滑动滚动列表,二选一。

三、事件:onCancel 什么时候触发?

GestureGroup 自己只有一个事件:

onCancel(event: () => void)

含义:

  • 手势识别成功后,如果收到触摸取消事件,会触发这个回调;
  • 常见情况:

    • 系统打断(来电、系统弹窗)
    • 父组件拦截或其它手势优先级更高
    • 触摸被提前终止

在实际项目里,onCancel 通常用来做:

  • 恢复 UI 状态(比如把变成虚线的边框改回实线);
  • 取消动画、清理资源
  • 重置一些临时的状态变量,避免后续交互异常。

四、官方示例拆解:长按 + 拖动(顺序识别)

先看一下官方示例的完整版,然后逐块拆解思路。

// xxx.ets
@Entry
@Component
struct GestureGroupExample {
  @State count: number = 0;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;
  @State borderStyles: BorderStyle = BorderStyle.Solid;

  build() {
    Column() {
      Text('sequence gesture\n' +
        'LongPress onAction:' + this.count + '\n' +
        'PanGesture offset:\nX: ' + this.offsetX + '\n' +
        'Y: ' + this.offsetY)
        .fontSize(15)
    }
    .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
    .height(150)
    .width(200)
    .padding(20)
    .margin(20)
    .border({ width: 3, style: this.borderStyles })
    .gesture(
      // 顺序识别:长按成功后,才会识别拖动
      GestureGroup(GestureMode.Sequence,
        LongPressGesture({ repeat: true })
          .onAction((event?: GestureEvent) => {
            if (event && event.repeat) {
              this.count++
            }
            console.info('LongPress onAction')
          }),
        PanGesture()
          .onActionStart(() => {
            this.borderStyles = BorderStyle.Dashed
            console.info('pan start')
          })
          .onActionUpdate((event?: GestureEvent) => {
            if (event) {
              this.offsetX = this.positionX + event.offsetX
              this.offsetY = this.positionY + event.offsetY
            }
            console.info('pan update')
          })
          .onActionEnd(() => {
            this.positionX = this.offsetX
            this.positionY = this.offsetY
            this.borderStyles = BorderStyle.Solid
            console.info('pan end')
          })
      )
        .onCancel(() => {
          console.info('sequence gesture canceled')
        })
    )
  }
}

4.1 交互效果总结

  • 用户先长按卡片:

    • 长按过程中,count 会累加;
  • 长按识别完成后,才会开始识别拖动:

    • 拖动时卡片跟着移动(offsetX / offsetY 更新);
    • 边框样式变成虚线,松手恢复实线;
  • 如果中途被取消,走 onCancel

4.2 关键点解读

  1. 必须用 Sequence 模式

    GestureGroup(GestureMode.Sequence, LongPressGesture(...), PanGesture())

    想要“长按 → 再拖动”这样的链式交互,最自然就是顺序识别。

  2. 位移计算通过“起始位置 + 偏移量”完成

    this.offsetX = this.positionX + event.offsetX
    this.offsetY = this.positionY + event.offsetY
    • positionX / positionY 记录上一次拖动结束的位置;
    • event.offsetX / offsetY 是当前手势中的增量;
    • 松手时把当前 offset 写回 position,即新起点。
  3. 只在 PanGesture 的 onActionEnd 收尾

    • 因为 Sequence 模式下只有最后一个手势能触发 onActionEnd
    • 恰好我们希望拖动结束时写入最终位置、恢复边框样式。

五、经典场景:单击 + 双击共存怎么写?

这是 GestureGroup 出现频率最高的需求之一。

5.1 思路

  • TapGesture 写两个手势:

    • 一个 count: 2 表示双击;
    • 一个 count: 1 表示单击;
  • 使用 GestureGroup(GestureMode.Sequence, 双击, 单击)
  • 双击优先识别,成功后单击不会再触发。

5.2 示例代码

@Entry
@Component
struct TapGestureGroupDemo {
  @State singleCount: number = 0;
  @State doubleCount: number = 0;

  build() {
    Column() {
      Text(`单击次数:${this.singleCount}`)
        .fontSize(16)
      Text(`双击次数:${this.doubleCount}`)
        .fontSize(16)
        .margin({ bottom: 12 })

      Text('点击这个区域测试单击/双击')
        .fontSize(18)
        .padding(20)
        .backgroundColor('#EEEEEE')
        .borderRadius(12)
        .gesture(
          GestureGroup(
            GestureMode.Sequence,
            // 一定要把双击放前面!
            TapGesture({ count: 2 })
              .onAction(() => {
                this.doubleCount++;
                console.info('double tap');
              }),
            TapGesture({ count: 1 })
              .onAction(() => {
                this.singleCount++;
                console.info('single tap');
              })
          )
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

✅ 小结:

  • 双击写在前面 → 既能识别双击,又不会误触单击;
  • 用 Sequence 模式就够了,不需要 Parallel / Exclusive。

六、Parallel / Exclusive 模式实战思路示例

这里给两个思路示例,你可以按需带入自己项目。

6.1 Parallel:缩放 + 旋转同时识别

伪代码示意:

Shape()
  .width(200)
  .height(200)
  .gesture(
    GestureGroup(GestureMode.Parallel,
      PinchGesture()
        .onActionUpdate(e => {
          // 根据 e.scale 处理缩放
        }),
      RotationGesture()
        .onActionUpdate(e => {
          // 根据 e.angle 处理旋转
        })
    )
  )
  • 两个手势同时识别,互不阻塞;
  • 更适合“画布类”、“图片编辑器”等交互。

6.2 Exclusive:滑动删除 vs 点击打开二选一

思路:

  • 给同一个 Item 区域同时注册:

    • 一个 PanGesture(水平滑动触发删除);
    • 一个 TapGesture(点击进入详情);
  • GestureGroup(GestureMode.Exclusive, PanGesture, TapGesture)
  • 用户如果滑动成功,就进入删除逻辑,不再触发点击。

伪代码示意:

Row()
  .width('100%')
  .height(60)
  .gesture(
    GestureGroup(GestureMode.Exclusive,
      PanGesture({ direction: PanDirection.Horizontal })
        .onActionEnd(e => {
          // 滑到一定距离后,触发删除
        }),
      TapGesture({ count: 1 })
        .onAction(() => {
          // 打开详情页
        })
    )
  )

七、GestureGroup 使用小结 & 常见坑

最后快速帮你盘一遍重点:

  1. 基本语法

    .gesture(
      GestureGroup(GestureMode.Sequence | Parallel | Exclusive, 手势1, 手势2, ...)
        .onCancel(() => { ... })
    )
  2. mode 选型建议

    • 顺序链条(长按 → 拖动、双击优先于单击):Sequence
    • 多手势同时有效(缩放 + 旋转):Parallel
    • 多手势竞争,一个成功其他失败(滑动 vs 点击):Exclusive
  3. Tap + 双击 必须注意顺序

    • 双击手势写前面,单击写后面;
    • 否则单击会先被识别,导致双击识别不到。
  4. Sequence 模式 only 最后一个 onActionEnd 生效

    • 需要在“最后一个手势”的 onActionEnd 里做收尾逻辑;
    • 上层流程性操作,尽量放在最后一个手势里处理。
  5. onCancel 用来兜底清理状态

    • onActionEnd 不同:onCancel 是“被打断”的收尾;
    • 避免 UI 卡在“选中态 / 虚线边框 / 半透明”等中间状态。

到这里,GestureGroup 的核心思路和常见用法基本都过了一遍。建议你:

  • 先把官方的长按 + 拖动例子跑起来;
  • 再自己写一个 单击 + 双击共存 的小 Demo;
  • 然后根据项目需求,尝试用 Parallel / Exclusive 把原来复杂的 if/else 手势逻辑慢慢收敛到 GestureGroup 上。

用熟之后,你会发现:组合手势本身没那么难,难的是想清楚交互规则,而 GestureGroup 正好帮你把“规则”变成清晰的代码结构。