前言

A hand-drawn doodle illustration on pure white pap

你刷短视频双击点赞、看图捏合放大、长按弹出菜单——这些交互背后全是手势识别。HarmonyOS7 的手势系统比我想象的要完善,单手势、组合手势、优先级控制,该有的都有。但刚上手时确实容易晕:TapGesture 和 onClick 啥区别?PanGesture 方向怎么设?手势冲突怎么办?这篇把这些问题一次讲透。

手势体系概览

HarmonyOS7 提供的手势类型,一张表看全:

手势类型核心回调典型场景
TapGesture点击onAction双击点赞、多次点击
LongPressGesture长按onAction / onActionEnd长按菜单、拖拽预备
PanGesture拖拽/平移onActionStart/Update/End图片拖拽、滑动删除
PinchGesture捏合onActionStart/Update/End图片缩放
RotationGesture旋转onActionStart/Update/End图片旋转
SwipeGesture快速滑动onAction翻页、切歌

绑定方式有三种:

  • gesture():普通绑定,子组件优先
  • priorityGesture():优先绑定,父组件抢优先
  • parallelGesture():并行绑定,父子同时响应

划重点:大部分情况用 gesture() 就够了。只有父子组件手势冲突时才需要 priorityGesture 和 parallelGesture。

点击与长按

TapGesture——双击点赞

A hand-drawn doodle illustration on pure white pap

你可能问了:不是有 onClick 吗?区别在于 onClick 只能响应单击,TapGesture 能识别多次点击:

Image($r('app.media.heart'))
  .width(40)
  .height(40)
  .gesture(
    TapGesture({ count: 2 })
      .onAction(() => {
        this.toggleLike()
      })
  )

count: 2 表示双击触发。还有个细节——绑定 TapGesture({ count: 2 }) 后,单击不会触发任何回调。如果你需要单击和双击共存,得用组合手势(后面讲)。

LongPressGesture——长按弹出操作菜单

@State isLongPressed: boolean = false

Text('长按我试试')
  .fontSize(20)
  .gesture(
    LongPressGesture({ repeat: true })
      .onAction((event: GestureEvent) => {
        if (event.repeat) {
          // 持续长按中,可以震动反馈
        } else {
          this.isLongPressed = true
        }
      })
      .onActionEnd(() => {
        this.isLongPressed = false
      })
  )

A hand-drawn doodle illustration on pure white pap

参数 repeat: true 很关键——设了之后,长按过程中会持续触发 onAction,event.repeat 为 true 表示是重复触发。适合做"长按加速"效果,比如长按数字递增。

不设 repeat 的话,只在长按达到阈值时触发一次,手指抬起触发 onActionEnd。

拖拽——PanGesture 实战图片拖拽

这是最实用的手势之一,图片拖拽、列表滑动删除都用它:

@State imageX: number = 100
@State imageY: number = 100

Image($r('app.media.photo'))
  .width(80)
  .height(80)
  .position({ x: this.imageX, y: this.imageY })
  .gesture(
    PanGesture({ direction: PanDirection.All })
      .onActionStart(() => {
        console.info('开始拖拽')
      })
      .onActionUpdate((event: GestureEvent) => {
        this.imageX += event.offsetX
        this.imageY += event.offsetY
      })
      .onActionEnd(() => {
        console.info('拖拽结束')
      })
  )

关键讲解:

  • direction: PanDirection.All——允许所有方向拖拽。也可以设 PanDirection.Horizontal 或 PanDirection.Vertical 限制单方向
  • event.offsetX / offsetY——相对上一次回调的偏移量,不是绝对坐标。所以要用 += 累加
  • 如果要限制拖拽范围,在 onActionUpdate 里加边界判断就行

捏合缩放——PinchGesture 实战图片缩放

看大图时捏合缩放,几乎成了用户本能:

@State imageScale: number = 1
@State lastScale: number = 1

Image($r('app.media.photo'))
  .width(300)
  .height(200)
  .scale({ x: this.imageScale, y: this.imageScale })
  .gesture(
    PinchGesture()
      .onActionStart(() => {
        this.lastScale = this.imageScale
      })
      .onActionUpdate((event: GestureEvent) => {
        this.imageScale = this.lastScale * event.scale
        // 限制缩放范围
        if (this.imageScale < 0.5) this.imageScale = 0.5
        if (this.imageScale > 3) this.imageScale = 3
      })
      .onActionEnd(() => {
        this.lastScale = this.imageScale
      })
  )

为什么需要 lastScale?

event.scale 是相对手势开始时的缩放比例,不是相对上一次的。比如当前 scale 是 2,用户捏合让 event.scale 变成 1.5,那实际应该是 2 * 1.5 = 3,不是直接用 1.5。所以每次手势开始时记录当前 scale,update 时用 lastScale * event.scale 算真实值。

血泪教训:一定要限制缩放范围,不然用户一使劲缩到 0,图片就"没了";放大到 10 倍,直接卡死。

手势组合——并行与串行

单手势好理解,但实际需求经常要组合。比如"双击缩放 + 捏合缩放"、“长按后拖拽”。

并行组合——GestureGroup Parallel

双击缩放 + 捏合缩放同时生效:

.gesture(
  GestureGroup(GestureMode.Parallel,
    TapGesture({ count: 2 })
      .onAction(() => {
        animateTo({ duration: 200 }, () => {
          this.imageScale = this.imageScale > 1 ? 1 : 2
        })
      }),
    PinchGesture()
      .onActionUpdate((event: GestureEvent) => {
        this.imageScale = this.lastScale * event.scale
      })
  )
)

GestureMode.Parallel 表示两个手势互不干扰,各响各的。双击触发缩放切换,捏合触发连续缩放,两不误。

串行组合——GestureMode.Sequence

长按后拖拽——必须先长按触发,再拖拽才有效:

.gesture(
  GestureGroup(GestureMode.Sequence,
    LongPressGesture({ duration: 500 })
      .onAction(() => {
        // 长按触发,给出视觉反馈
        this.itemSelected = true
      }),
    PanGesture()
      .onActionUpdate((event: GestureEvent) => {
        this.itemX += event.offsetX
        this.itemY += event.offsetY
      })
  )
)

串行模式下,手势按顺序识别。长按成功后才识别拖拽,任何一个失败则整个组合失败。这就像 Android 的长按拖拽——得先按住不动,才能拖。

还有个 GestureMode.Exclusive(互斥),多个手势谁先识别算谁的,另一个取消。日常用得不多,知道就行。

写在最后

手势识别是交互体验的基础。搞懂了 Tap、LongPress、Pan、Pinch 四个手势,90% 的交互场景都能覆盖。组合手势稍微复杂点,但理解了并行和串行的区别就不难了。

实战建议:先从单手势开始做,调通了再组合。别上来就搞三四个手势并行,出了冲突调试到崩溃。手势和动画搭配使用效果翻倍——拖拽加个 animateTo 弹回,捏合加个平滑过渡,体验直接上一个档次。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐