前置知识: HarmonyOS

手势与交互

3 minIntermediate2026/6/14

手势识别与触摸事件

概述

手势与交互是移动应用用户体验的核心。HarmonyOS 提供了丰富的手势识别能力,包括点击、长按、拖动、缩放、旋转等基础手势,以及组合手势和自定义手势。通过手势系统,你可以让用户以自然的方式与应用交互,而不局限于按钮点击。

为什么需要手势系统?触屏设备上,手势是最自然的交互方式。用户习惯通过滑动浏览内容、双指缩放片、长按呼出菜单。如果你的应用只支持点击,用户体验会大打折扣。HarmonyOS 的手势系统让你可以轻松实现这些交互模式。

基础概念

TapGesture:点击手势,支持单击、双击和多击识别。

LongPressGesture:长按手势,用户按住一段时间后触发。

PanGesture:拖动手势,用户按住并移动手指时触发,常用于滑动和拖拽。

PinchGesture:捏合手势,双指缩放,用于片缩放等场景。

RotationGesture:旋转手势,双指旋转,用于片旋转等场景。

GestureGroup:组合手势,将多个手势组合在一起,支持串行、并行和互斥模式。

快速上手

最简单的手势示例:

@Entry
@Component
struct GestureDemo {
  @State message: string = '试试各种手势'

  build() {
    Column() {
      Text(this.message)
        .fontSize(24)

      // 点击手势
      Text('点击我')
        .fontSize(20)
        .padding(20)
        .backgroundColor('#f0f0f0')
        .gesture(
          TapGesture()
            .onAction(() => {
              this.message = '你点击了!'
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

详细用法

点击手势

@Component
struct TapGestureDemo {
  @State clickCount: number = 0
  @State doubleClickMsg: string = ''

  build() {
    Column() {
      // 单击手势
      Text('单击计数: ' + this.clickCount)
        .fontSize(20)
        .padding(20)
        .backgroundColor('#e0e0e0')
        .gesture(
          TapGesture({ count: 1 }) // count: 1 表示单击
            .onAction(() => {
              this.clickCount++
            })
        )

      // 双击手势
      Text(this.doubleClickMsg || '双击我')
        .fontSize(20)
        .padding(20)
        .backgroundColor('#d0d0d0')
        .gesture(
          TapGesture({ count: 2 }) // count: 2 表示双击
            .onAction(() => {
              this.doubleClickMsg = '双击成功!'
            })
        )
    }
  }
}

长按手势

@Component
struct LongPressDemo {
  @State pressMsg: string = '长按我'

  build() {
    Column() {
      Text(this.pressMsg)
        .fontSize(20)
        .padding(20)
        .backgroundColor('#c0c0c0')
        .gesture(
          LongPressGesture({ repeat: true }) // repeat: 重复触发
            .onAction((event: GestureEvent) => {
              this.pressMsg = `长按中... 重复次数: ${event.repeatCount}`
            })
            .onActionEnd(() => {
              this.pressMsg = '长按结束'
            })
        )
    }
  }
}

拖动手势

@Component
struct PanGestureDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Column() {
      Row() {
        Text('拖动我')
          .fontSize(18)
          .fontColor(Color.White)
      }
      .width(100)
      .height(100)
      .backgroundColor(Color.Blue)
      .translate({ x: this.offsetX, y: this.offsetY }) // 使用 translate 实现移动
      .gesture(
        PanGesture()
          .onActionStart(() => {
            console.info('开始拖动')
          })
          .onActionUpdate((event: GestureEvent) => {
            // 累加偏移量
            this.offsetX += event.offsetX
            this.offsetY += event.offsetY
          })
          .onActionEnd(() => {
            console.info('拖动结束')
          })
      )

      Button('重置位置').onClick(() => {
        this.offsetX = 0
        this.offsetY = 0
      })
    }
    .width('100%')
    .height('100%')
  }
}

缩放手势

@Component
struct PinchGestureDemo {
  @State scale: number = 1.0

  build() {
    Column() {
      Image($r('app.media.test_image'))
        .width(300)
        .height(300)
        .objectFit(ImageFit.Contain)
        .scale({ x: this.scale, y: this.scale }) // 根据缩放比例调整大小
        .gesture(
          PinchGesture()
            .onActionStart(() => {
              console.info('开始缩放')
            })
            .onActionUpdate((event: GestureEvent) => {
              // event.scale 是相对于初始状态的缩放比
              this.scale = Math.max(0.5, Math.min(3.0, this.scale * event.scale))
            })
        )

      Text(`缩放比例: ${this.scale.toFixed(2)}`)
        .fontSize(16)
        .margin({ top: 12 })

      Button('重置').onClick(() => {
        this.scale = 1.0
      })
    }
  }
}

旋转手势

@Component
struct RotationGestureDemo {
  @State angle: number = 0

  build() {
    Column() {
      Image($r('app.media.test_image'))
        .width(200)
        .height(200)
        .objectFit(ImageFit.Contain)
        .rotate({ angle: this.angle }) // 根据角度旋转
        .gesture(
          RotationGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.angle += event.angle
            })
        )

      Text(`旋转角度: ${this.angle.toFixed(0)} 度`)
        .fontSize(16)
        .margin({ top: 12 })

      Button('重置').onClick(() => {
        this.angle = 0
      })
    }
  }
}

组合手势

@Component
struct GestureGroupDemo {
  @State msg: string = '试试不同手势'

  build() {
    Column() {
      Text(this.msg)
        .fontSize(20)
        .padding(30)
        .backgroundColor('#e8e8e8')
        .gesture(
          // 互斥模式:只响应第一个识别成功的手势
          GestureGroup(GestureMode.Exclusive,
            TapGesture({ count: 2 })
              .onAction(() => {
                this.msg = '双击'
              }),
            LongPressGesture()
              .onAction(() => {
                this.msg = '长按'
              }),
            PanGesture()
              .onAction(() => {
                this.msg = '拖动'
              })
          )
        )
    }
  }
}

组合手势的三种模式:

// 1. 互斥模式(Exclusive):只响应第一个识别成功的手势
GestureGroup(GestureMode.Exclusive, tapGesture, longPressGesture);

// 2. 并行模式(Parallel):所有手势同时识别
GestureGroup(GestureMode.Parallel, pinchGesture, rotationGesture);

// 3. 串行模式(Sequential):按顺序识别,前一个完成后才开始下一个
GestureGroup(GestureMode.Sequential, longPressGesture, panGesture);
// 先长按,长按成功后才能拖动

触摸事件

除了手势,还可以直接处理触摸事件

@Component
struct TouchEventDemo {
  @State touchMsg: string = '触摸此区域'
  @State x: number = 0
  @State y: number = 0

  build() {
    Column() {
      Text(this.touchMsg)
        .fontSize(16)
      Text(`坐标: (${this.x.toFixed(0)}, ${this.y.toFixed(0)})`)
        .fontSize(14)
        .fontColor('#666666')
    }
    .width(300)
    .height(200)
    .backgroundColor('#f0f0f0')
    .justifyContent(FlexAlign.Center)
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down:
          this.touchMsg = '手指按下'
          break
        case TouchType.Move:
          this.touchMsg = '手指移动'
          break
        case TouchType.Up:
          this.touchMsg = '手指抬起'
          break
      }
      this.x = event.touches[0].x
      this.y = event.touches[0].y
    })
  }
}

常见场景

可拖动的浮动按钮

@Component
struct DraggableButton {
  @State posX: number = 300
  @State posY: number = 500

  build() {
    Stack() {
      // 页面内容
      Text('页面内容区域')
        .fontSize(24)

      // 可拖动的浮动按钮
      Button('+')
        .width(56)
        .height(56)
        .fontSize(28)
        .borderRadius(28)
        .position({ x: this.posX, y: this.posY })
        .gesture(
          PanGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.posX += event.offsetX
              this.posY += event.offsetY
            })
        )
    }
    .width('100%')
    .height('100%')
  }
}

片查看器(缩放+旋转)

@Component
struct ImageViewer {
  @State scale: number = 1.0
  @State angle: number = 0
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Stack() {
      Image($r('app.media.photo'))
        .objectFit(ImageFit.Contain)
        .scale({ x: this.scale, y: this.scale })
        .rotate({ angle: this.angle })
        .translate({ x: this.offsetX, y: this.offsetY })
        .gesture(
          GestureGroup(GestureMode.Parallel,
            // 缩放
            PinchGesture()
              .onActionUpdate((event: GestureEvent) => {
                this.scale = Math.max(0.5, Math.min(5.0, this.scale * event.scale))
              }),
            // 旋转
            RotationGesture()
              .onActionUpdate((event: GestureEvent) => {
                this.angle += event.angle
              }),
            // 拖动
            PanGesture()
              .onActionUpdate((event: GestureEvent) => {
                this.offsetX += event.offsetX
                this.offsetY += event.offsetY
              })
          )
        )

      // 重置按钮
      Button('重置')
        .position({ x: 16, y: 16 })
        .onClick(() => {
          this.scale = 1.0
          this.angle = 0
          this.offsetX = 0
          this.offsetY = 0
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }
}

注意事项

手势冲突:当父子组件都绑定了手势时,默认由子组件消费手势事件。如果需要父组件处理,可以使用 .priorityGesture() 代替 .gesture()

手势与滚动的冲突:在可滚动容器(List、Scroll)中使用 PanGesture 可能会与滚动冲突。使用 .parallelGesture() 可以让两者同时工作。

性能考虑:手势回调中的计算应尽量轻量,避免在 onActionUpdate 中执行耗时操作,因为它在手指移动时会被高频调用。

手势识别距离:PanGesture 默认需要移动一定距离才会触发,可以通过 distance 参数调整。

进阶用法

自定义手势识别

@Component
struct CustomGestureDemo {
  @State msg: string = ''

  build() {
    Column() {
      Text(this.msg).fontSize(20)

      // 使用原始触摸事件实现自定义手势
      Column() {
        Text('自定义手势区域')
      }
      .width(200)
      .height(200)
      .backgroundColor('#e0e0e0')
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          // 记录按下位置和时间
          console.info(`按下位置: (${event.touches[0].x}, ${event.touches[0].y})`)
        } else if (event.type === TouchType.Up) {
          // 计算滑动方向和速度
          console.info('手指抬起')
        }
      })
    }
  }
}

手势与动画配合

@Component
struct GestureAnimationDemo {
  @State scale: number = 1.0
  @State brightness: number = 1.0

  build() {
    Column() {
      Image($r('app.media.photo'))
        .width(300)
        .height(300)
        .objectFit(ImageFit.Cover)
        .scale({ x: this.scale, y: this.scale })
        .brightness(this.brightness)
        .gesture(
          PinchGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.scale = Math.max(0.5, Math.min(3.0, this.scale * event.scale))
            })
        )
        .animation({
          duration: 200,
          curve: Curve.EaseOut
        })
    }
  }
}