一、引言:手势处理 —— 构建沉浸式交互的核心能力

在鸿蒙应用开发中,手势交互系统是实现自然人机对话的关键技术。通过识别用户的点击、滑动、拖拽等手势行为,开发者能够构建符合直觉的交互体验,如图片编辑中的多点缩放、列表项的滑动删除、组件的自由拖拽等场景。本文将系统解构鸿蒙手势处理的核心机制、手势类型及工程实践技巧,帮助开发者掌握从基础手势到复杂组合手势的全流程实现方法。

二、鸿蒙手势处理基础:核心框架与手势类型

2.1 手势处理核心架构

鸿蒙手势系统基于识别器 - 事件回调双层架构实现:

  • 手势识别器:系统封装多种专用识别器
    • TapGesture:支持单击、双击和多次点击事件的识别。
      onAction(event: Callback<GestureEvent>): TapGestureHandler
      
      Tap手势识别成功回调。
    • LongPressGesture:用于触发长按手势事件,触发长按手势的最少手指数为1,最短长按时间为500毫秒。
      onAction(event: Callback<GestureEvent>): LongPressGestureHandler
      
      LongPress手势识别成功回调。
      
      onActionEnd(event: Callback<GestureEvent>): LongPressGestureHandler
      
      LongPress手势识别成功,最后一根手指抬起后触发回调。
      
      onActionCancel(event: Callback<void>): LongPressGestureHandler
      
      LongPress手势识别成功,接收到触摸取消事件触发回调。不返回手势事件信息。
      
      onActionCancel(event: Callback<GestureEvent>): LongPressGestureHandler
      
      LongPress手势识别成功,接收到触摸取消事件触发回调。与onActionCancel接口相比,此接口返回手势事件信息。
    • PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。
      onActionStart(event: Callback<GestureEvent>): PanGestureHandler
      
      Pan手势识别成功回调。
      
      onActionUpdate(event: Callback<GestureEvent>): PanGestureHandler
      
      Pan手势移动过程中回调。
      
      onActionEnd(event: Callback<GestureEvent>): PanGestureHandler
      
      Pan手势识别成功,手指抬起后触发回调。
      
      onActionCancel(event: Callback<void>): PanGestureHandler
      
      Pan手势识别成功,接收到触摸取消事件触发回调。不返回手势事件信息。
      
      onActionCancel(event: Callback<GestureEvent>): PanGestureHandler
      
      Pan手势识别成功,接收到触摸取消事件触发回调。与onActionCancel接口相比,此接口返回手势事件信息。
    • PinchGesture:用于触发捏合手势,触发捏合手势的最少手指为2指,最大为5指,最小识别距离为5vp。
      onActionStart(event: Callback<GestureEvent>): PinchGestureHandler
      
      Pinch手势识别成功回调。
      
      onActionUpdate(event: Callback<GestureEvent>): PinchGestureHandler
      
      Pinch手势移动过程中回调。
      
      onActionEnd(event: Callback<GestureEvent>): PinchGestureHandler
      
      Pinch手势识别成功,手指抬起后触发回调。
      
      onActionCancel(event: Callback<void>): PinchGestureHandler
      
      Pinch手势识别成功,接收到触摸取消事件触发回调。不返回手势事件信息。
      
      onActionCancel(event: Callback<GestureEvent>): PinchGestureHandler
      
      Pinch手势识别成功,接收到触摸取消事件触发回调。与onActionCancel接口相比,此接口返回手势事件信息。
    • RotationGesture:用于触发旋转手势事件,触发旋转手势的最少手指为2指,最大为5指,最小改变度数为1度。该手势不支持通过触控板双指旋转操作触发。
      onActionStart(event: Callback<GestureEvent>): RotationGestureHandler
      
      Rotation手势识别成功回调。
      
      onActionUpdate(event: Callback<GestureEvent>): RotationGestureHandler
      
      Rotation手势移动过程中回调。
      
      onActionEnd(event: Callback<GestureEvent>): RotationGestureHandler
      
      Rotation手势识别成功,手指抬起后触发回调。
      
      onActionCancel(event: Callback<void>): RotationGestureHandler
      
      Rotation手势识别成功,接收到触摸取消事件触发回调。不返回手势事件信息。
      
      onActionCancel(event: Callback<GestureEvent>): RotationGestureHandler
      
      Rotation手势识别成功,接收到触摸取消事件触发回调。与onActionCancel相比,此接口返回手势事件信息。
    • SwipeGesture:用于触发滑动事件,滑动速度大于100vp/s时可识别成功。

      onAction(event: Callback<GestureEvent>): SwipeGestureHandlerOptions
      
      Swipe手势识别成功回调。

2.2 基础手势类型与应用场景

手势类型 核心功能 典型场景 关键配置参数
点击手势 单次 / 多次点击识别 按钮交互 / 列表项选中 count(点击次数)
长按手势 长按动作检测 快捷菜单 / 多选模式 duration(最短时长 500ms)
平移手势 轨迹追踪与位移计算 组件拖拽 / 图片移动 direction(移动方向)
滑动手势 快速滑动方向识别 页面切换 / 列表删除 speed(速度阈值)
捏合手势 双指缩放比例计算 图片 / 文本缩放 fingers(最少 2 指)

三、手势处理进阶:组合手势与事件控制

3.1 组合手势高级应用

通过GestureGroup实现多手势协同,支持三种模式:

顺序模式(Sequence)

手势按注册顺序依次识别,前一手势成功后才触发下一手势

@Entry
@Component
struct SequenceGestureDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0
  @State pressCount: number = 0

  build() {
    Text('长按后拖拽')
      .fontSize(28)
      .translate({ x: this.offsetX, y: this.offsetY })
      .width(300)
      .height(250)
      .gesture(
        GestureGroup(GestureMode.Sequence,
          // 长按手势(可重复触发)
          LongPressGesture({ repeat: true })
            .onAction(() => this.pressCount++)
            .onActionEnd(() => console.log('长按结束')),
          // 平移手势(长按后触发)
          PanGesture()
            .onActionStart(() => console.log('拖拽开始'))
            .onActionUpdate((event) => {
              this.offsetX += event.offsetX
              this.offsetY += event.offsetY
            })
        )
      )
  }
}
并行模式(Parallel)

多手势同时识别,互不影响(如平移时允许点击)

@Entry
@Component
struct ParallelGestureDemo {
  @State clickCount: number = 0
  @State panOffsetX: number = 0
  @State panOffsetY: number = 0

  build() {
    Column() {
      Image($r('app.media.startIcon'))
        .width('100%')
        .height('100%')
        .gesture(
          GestureGroup(GestureMode.Parallel,
            TapGesture({ count: 1 })
              .onAction(() => this.clickCount++),
            PanGesture()
              .onActionUpdate((event) => {
                this.panOffsetX += event.offsetX
                this.panOffsetY += event.offsetY
              })
          )
        )
        .translate({ x: this.panOffsetX, y: this.panOffsetY })
    }
  }
}
互斥模式(Exclusive)

手势同时识别,任一成功即终止其他手势

@Entry
@Component
struct ExclusiveGestureDemo {
  @State clickFlag: boolean = false
  @State swipeFlag: boolean = false

  build() {
    Column() {
      if (this.clickFlag) {
        Text('点击手势识别')
      } else if (this.swipeFlag) {
        Text('左滑手势识别')
      }
    }
    .width('100%')
    .height('100%')
    .gesture(
      GestureGroup(GestureMode.Exclusive,
        TapGesture({ count: 1 })
          .onAction(() => {
            this.clickFlag = true
            this.swipeFlag = false
          }),
        SwipeGesture({ direction: SwipeDirection.Horizontal })
          .onAction(() => {
            this.swipeFlag = true
            this.clickFlag = false
          })
      )
    )
  }
}

3.2 手势优先级与事件冒泡控制

优先级手势(priorityGesture)

父组件通过priorityGesture优先捕获手势

@Entry
@Component
struct PriorityGestureDemo {
  @State parentClick: number = 0
  @State childClick: number = 0

  build() {
    Column() {
      Text('父组件' + this.parentClick)
        .width('50%')
        .height('50%')
        .backgroundColor(Color.Pink)
      Text('子组件' + this.childClick)
        .width('50%')
        .height('50%')
        .backgroundColor(Color.White)
        .gesture(
          TapGesture({ count: 1 })
            .onAction(() => {
              this.childClick++
            })
        )
    }
    .width('100%')
    .height('100%')
    .priorityGesture(
      TapGesture({ count: 1 })
        .onAction(() => {
          this.parentClick++
        })
    )
    .backgroundColor(Color.Gray)
  }
}
模拟事件冒泡

通过手动调用父组件逻辑实现类似冒泡效果

@Entry
@Component
struct BubbleGestureDemo {
  @State itemClick: number = 0
  @State listClick: number = 0

  handleItemClick() {
    this.itemClick++
    this.handleListClick() // 手动触发父逻辑
  }

  handleListClick() {
    this.listClick++
  }

  build() {
    Column() {
      Text('父' + this.listClick)
        .width('50%')
        .height('30%')
        .backgroundColor(Color.Pink)
      Text('子' + this.itemClick)
        .width('50%')
        .height('30%')
        .backgroundColor(Color.White)
        .gesture(
          TapGesture({ count: 1 })
            .onAction(() => this.handleItemClick())
        )
    }
    .width('100%')
    .height('100%')
    .gesture(
      TapGesture({ count: 1 })
        .onAction(() => this.handleListClick())
    )
    .backgroundColor(Color.Gray)

  }
}

四、实战案例:典型手势交互场景实现

4.1 图片贴纸自由拖拽(平移手势应用)

@Entry
@Component
struct StickerDragApp {
  @State stickerX: number = 50 // 初始X坐标
  @State stickerY: number = 50 // 初始Y坐标

  build() {
    Column() {
      // 可拖拽贴纸
      Image($r("app.media.startIcon"))
        .width(80)
        .height(80)
        .translate({ x: this.stickerX, y: this.stickerY })
        .gesture(
          PanGesture()
            .onActionUpdate((event) => {
              // 累加位移并限制边界
              this.stickerX = Math.max(
                0,
                Math.min(event.offsetX, 300)
              )
              this.stickerY = Math.max(
                0,
                Math.min(event.offsetY, 500)
              )
            })
        )
    }
    .backgroundColor("#FFE4B5")
    .width("100%")
    .height("100%")
  }
}

4.2 列表项左滑删除(滑动 + 阻尼效果)

@Entry
@Component
struct SwipeDeleteApp {
  @State messages: string[] = ['通知1', '通知2', '通知3', '通知4']
  @State itemOffsets: number[] = [] // 改为数组存储偏移量
  @State isDeleting: boolean[] = [] // 改为数组存储删除状态

  aboutToAppear() {
    // 初始化状态数组
    this.itemOffsets = new Array(this.messages.length).fill(0)
    this.isDeleting = new Array(this.messages.length).fill(false)
  }

  // 处理滑动更新
  handleSwipe(index: number, event: GestureEvent) {
    // 限制偏移量范围,避免过度滑动
    this.itemOffsets[index] = Math.max(-200, Math.min(0, event.offsetX))
  }

  // 处理滑动结束
  handleSwipeEnd(index: number, event: GestureEvent) {
    // 向左滑动超过80px时执行删除(降低触发门槛)
    if (this.itemOffsets[index] < -80) {
      this.startDeleteAnimation(index)
    } else {
      this.resetPosition(index)
    }
  }

  // 启动删除动画
  startDeleteAnimation(index: number) {
    this.isDeleting[index] = true
    setTimeout(() => {
      // 删除元素并更新状态数组
      this.messages.splice(index, 1)
      this.itemOffsets.splice(index, 1)
      this.isDeleting.splice(index, 1)
    }, 300)
  }

  // 复位动画
  resetPosition(index: number) {
    animateTo({
      duration: 300,
      curve: Curve.EaseOut
    }, () => {
      this.itemOffsets[index] = 0
    })
  }

  build() {
    Column() {
      List() {
        ForEach(this.messages, (item: string, index) => {
          ListItem() {
            Row() {
              Text(item)
                .flexGrow(1)
                .padding(16)

              if (this.isDeleting[index]) {
                Text('删除中...')
                  .fontColor(Color.Red)
                  .padding(16)
              } else {
                Button('删除')
                  .backgroundColor(Color.Red)
                  .width(80)
                  .onClick(() => this.startDeleteAnimation(index))
              }
            }
            .width('100%')
          }
          .gesture(
            PanGesture({ direction: PanDirection.All })
              .onActionUpdate((event: GestureEvent) => this.handleSwipe(index, event))
              .onActionEnd((event: GestureEvent) => this.handleSwipeEnd(index, event))
          )
          .offset({ x: this.itemOffsets[index] })
        }, (_item: string, index) => index.toString())
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }
}

五、工程实践最佳指南

5.1 性能优化策略

手势事件轻量化处理
@Entry
@Component
struct OptimizedGestureApp {
  @State offsetX: number = 0
  @State offsetY: number = 0
  private lastOffsetX: number = 0 // 记录上次偏移量
  private lastOffsetY: number = 0

  // 使用帧动画优化高频更新
  handlePanUpdate(event: GestureEvent) {
    // 计算增量偏移(带0.8阻尼系数)
    const deltaX = (event.offsetX - this.lastOffsetX) * 0.8
    const deltaY = (event.offsetY - this.lastOffsetY) * 0.8
    // 累加偏移量并四舍五入到整数
    this.offsetX += Math.round(deltaX)
    this.offsetY += Math.round(deltaY)
    // 更新最后记录位置
    this.lastOffsetX = event.offsetX
    this.lastOffsetY = event.offsetY
  }

  build() {
    Column() {
      // 可拖动矩形
      Column()
        .width(200)
        .height(200)
        .backgroundColor(Color.Blue)
        .gesture(
          PanGesture()
            .onActionStart(() => {
              // 手势开始时重置记录点
              this.lastOffsetX = 0
              this.lastOffsetY = 0
            })
            .onActionUpdate((event: GestureEvent) => {
              this.handlePanUpdate(event)
            })
        )
        .translate({ x: this.offsetX, y: this.offsetY })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

手势冲突解决方案
@Entry
@Component
struct ConflictResolutionApp {
  @State scaleXY: number = 1
  @State panX: number = 0
  @State panY: number = 0

  build() {
    Column() {
      Image($r("app.media.startIcon"))
        .width(30)
        .height(30)
        .scale({ x: this.scaleXY, y: this.scaleXY })
        .translate({ x: this.panX, y: this.panY })
        .gesture(
          GestureGroup(GestureMode.Parallel, // 并行模式避免冲突
            PinchGesture()
              .onActionUpdate((event) => {
                this.scaleXY = Math.max(0.5, Math.min(2, this.scaleXY * event.scale))
              }),
            PanGesture()
              .onActionUpdate((event) => {
                this.panX += event.offsetX
                this.panY += event.offsetY
              })
          )
        )
    }
    .width('100%')
    .height('100%')
  }
}

5.2 兼容性与调试方案

API 分级适配
// 兼容不同API版本的手势逻辑
#if (API >= 9)
// API 9+新特性
let advancedGesture = PanGesture({
  direction: PanDirection.All,
  minDistance: 10
})
  .onActionUpdate((event) => {
    // 新回调参数处理
  })
#else
// 旧版本兼容逻辑
let legacyGesture = PanGesture()
  .onActionUpdate((event) => {
    // 旧回调处理
  })
#endif
日志调试技巧
@Entry
@Component
struct GestureDebugApp {
  @State count: number = 0
  build() {
    Button('测试手势')
      .width(200)
      .height(80)
      .backgroundColor(Color.Green)
      .gesture(
        TapGesture({ count: 2 })  // 双击手势
          .onAction((event) => {
            this.count++
            console.log(`双击事件: 
              坐标(x,y): (${event.offsetX}, ${event.offsetY})
              点击次数: ${this.count}
              时间戳: ${event.timestamp}`)
          })
      )
  }
}

六、总结:构建全场景手势交互体系

鸿蒙手势处理系统通过标准化接口与灵活组合机制,提供了从基础交互到复杂手势的完整解决方案。开发者需重点掌握:

  1. 基础手势:点击 / 长按 / 平移 / 滑动 / 捏合的核心参数配置
  2. 组合手势:顺序 / 并行 / 互斥模式的应用场景
  3. 事件控制:优先级管理与冒泡机制的工程实现
  4. 性能优化:轻量化处理与冲突解决方案

建议从基础案例入手,逐步尝试复杂交互场景,结合官方模拟器的手势调试工具(如多点触控模拟)验证效果。随着鸿蒙生态的演进,手势处理将与 AI 交互、多设备协同深度融合,成为全场景应用的核心竞争力。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐