请添加图片描述
请添加图片描述

一、引言

手势交互是移动端应用最核心的用户输入方式。HarmonyOS NEXT 的 ArkUI 框架提供了完整的手势系统(Gesture),支持以声明式方式为组件绑定各种手势识别与响应逻辑。

三种基础手势:

手势 类名 触发条件 典型场景
点击 TapGesture 轻触后抬起 按钮、双击点赞
长按 LongPressGesture 按住超过指定时长 弹出菜单
拖拽 PanGesture 按住并平移 滑动列表、拖拽排序

二、核心原理

2.1 手势绑定方式

方式 方法 说明
普通手势 .gesture() 绑定一个手势,后绑定的覆盖先绑定的
多手势 多次 .gesture() 绑定多个,并行识别
优先手势 .priorityGesture() 优先识别,可阻断父组件手势
并行手势 .parallelGesture() 并行识别,不阻断父组件

2.2 手势生命周期

onActionStart(手势开始识别)
    ↓
onActionUpdate(连续手势持续触发,如 PanGesture)
    ↓
onActionEnd(正常结束)/ onActionCancel(被打断取消)

2.3 GestureEvent 关键属性

event.fingerList[0].localX/Y  // 触摸点相对组件的坐标
event.offsetX/Y               // 偏移增量(PanGesture)
event.timestamp               // 时间戳

三、环境

MyApplication/
└── entry/src/main/
    ├── ets/pages/GestureDemo.ets
    └── resources/base/profile/main_pages.json

四、6 个实战场景

4.1 TapGesture 点击手势

count 参数控制点击次数:1=单击,2=双击。

@Component
struct TapGestureDemo {
  @State tapCount: number = 0;
  @State lastTapX: number = 0;
  @State lastTapY: number = 0;

  build() {
    Column() {
      // 单击区域
      Column() {
        Text('👆').fontSize(48)
        Text('点击此处').fontSize(18)
        Text('次数: ' + this.tapCount)
        Text('坐标: (' + this.lastTapX + ', ' + this.lastTapY + ')')
      }
      .width('100%').padding(24)
      .backgroundColor('rgba(255,255,255,0.08)').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .gesture(
        TapGesture({ count: 1 })
          .onAction((event: GestureEvent) => {
            this.tapCount++;
            this.lastTapX = Math.round(event.fingerList[0].localX);
            this.lastTapY = Math.round(event.fingerList[0].localY);
          })
      )

      // 双击区域
      Column() { Text('👆👆 双击此处').fontSize(15) }
        .padding(16).borderRadius(12)
        .backgroundColor('rgba(255,215,0,0.1)')
        .alignItems(HorizontalAlign.Center)
        .gesture(
          TapGesture({ count: 2 })
            .onAction(() => { this.tapCount += 2; })
        )
    }
  }
}

要点

  • count:1 单击,手指抬起立即触发 onAction
  • count:2 双击,系统等待第二次点击,超时未发生则不触发
  • event.fingerList[0].localX/Y 获取相对组件的点击坐标(vp)

4.2 LongPressGesture 长按手势

duration 控制触发长按的最小时长(ms)。

@Component
struct LongPressGestureDemo {
  @State isLongPressed: boolean = false;
  @State pressDuration: number = 0;
  private readonly minPress: number = 500;

  build() {
    Column() {
      Column() {
        Text(this.isLongPressed ? '🟢 已触发!' : '🔴 按住我 500ms')
          .fontSize(16)
          .fontColor(this.isLongPressed ? '#4CAF50' : Color.White)
        Text('时长: ' + this.pressDuration + 'ms')
      }
      .padding(24).borderRadius(16)
      .backgroundColor(this.isLongPressed
        ? 'rgba(76,175,80,0.15)' : 'rgba(255,255,255,0.08)')
      .alignItems(HorizontalAlign.Center)
      .gesture(
        LongPressGesture({ fingers: 1, repeat: false, duration: this.minPress })
          .onAction((event: GestureEvent) => {
            this.isLongPressed = true;
            this.pressDuration = event.timestamp;
          })
          .onActionEnd(() => { this.isLongPressed = false; })
      )
    }
  }
}

要点

  • onAction 在按住达到 duration 时触发
  • onActionEnd 在手指抬起时触发,适合重置状态
  • repeat: true 可让长按每隔 duration 重复触发

4.3 PanGesture 拖拽手势

direction 控制方向,distance 控制最小触发距离(防误触)。

@Component
struct PanGestureDemo {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State boxColor: string = '#5C8AFF';
  @State dragStatus: string = '等待拖拽';

  build() {
    Column() {
      Stack() {
        Column() { Text('⬡').fontSize(28) }
          .width(64).height(64)
          .backgroundColor(this.boxColor).borderRadius(14)
          .shadow({ radius: 8, color: 'rgba(0,0,0,0.3)' })
          .gesture(
            PanGesture({ fingers: 1, direction: PanDirection.All, distance: 5 })
              .onActionStart(() => {
                this.dragStatus = '🔄 拖拽中';
                this.boxColor = randomColor();
              })
              .onActionUpdate((event: GestureEvent) => {
                // event.offsetX/Y 是本次增量,需累加
                this.offsetX += event.offsetX;
                this.offsetY += event.offsetY;
              })
              .onActionEnd(() => { this.dragStatus = '✅ 结束'; })
              .onActionCancel(() => { this.dragStatus = '❌ 取消'; })
          )
          .translate({ x: this.offsetX, y: this.offsetY })
      }
      .width('100%').height(200).borderRadius(16).clip(true)

      Text(this.dragStatus).fontSize(13).margin({ top: 4 })
      Button('重置').onClick(() => { this.offsetX = 0; this.offsetY = 0; })
    }
  }
}
function randomColor(): string {
  return `hsl(${Math.floor(Math.random() * 360)}, 75%, 55%)`;
}
回调 时机 用途
onActionStart 手指移动达到 distance 初始化拖拽状态
onActionUpdate 手指持续移动 更新位置(event.offsetX/Y 为增量)
onActionEnd 手指抬起 保存结果
onActionCancel 手势被更高优先级打断 清理状态

要点event.offsetX 是相对于上一次回调的增量,需用 this.offsetX += event.offsetX 累加,再配合 .translate() 应用偏移。


4.4 多手势组合

同一组件可绑定多个手势——多次调用 .gesture()

.gesture(
  TapGesture({ count: 1 })
    .onAction(() => { /* 点击响应 */ })
)
.gesture(
  LongPressGesture({ fingers: 1, repeat: false, duration: 400 })
    .onAction(() => { /* 长按响应 */ })
)

多手势识别规则:

  • TapGesture + LongPressGesture → 并行,点击触发 Tap,长按触发 LongPress
  • TapGesture + PanGesture → 并行,轻触触发 Tap,滑动触发 Pan
  • LongPressGesture + PanGesture → 互斥,达到 duration 后不再响应 Pan

4.5 手势参数对比

三个并排卡片展示单击/双击/长按的参数差异。

Row({ space: 10 }) {
  // 单击 count:1
  Column() { Text('👆 单击我') }
    .backgroundColor('rgba(21,101,192,0.3)').borderRadius(12)
    .gesture(TapGesture({ count: 1 }).onAction(() => { /* 单击 */ }))

  // 双击 count:2
  Column() { Text('👆👆 双击我') }
    .backgroundColor('rgba(233,30,99,0.3)').borderRadius(12)
    .gesture(TapGesture({ count: 2 }).onAction(() => { /* 双击 */ }))

  // 长按 800ms
  Column() { Text('⏱️ 长按我') }
    .backgroundColor('rgba(255,152,0,0.3)').borderRadius(12)
    .gesture(LongPressGesture({ duration: 800 }).onAction(() => { /* 长按 */ }))
}
手势 关键参数 示例
TapGesture count 1(单击)/ 2(双击)
LongPressGesture duration 500ms / 800ms
PanGesture direction, distance All / 5vp

4.6 边界限制拖拽

通过 Math.max/Math.min 约束偏移量,结合 animateTo 实现松手回弹。

@Component
struct BoundedPanGestureDemo {
  @State panX: number = 0;
  @State panY: number = 0;
  private readonly maxOffset: number = (200 - 50) / 2; // (容器 - 方块) / 2

  build() {
    Stack() {
      Stack() {
        Column() { Text('⬡').fontSize(28) }
          .width(50).height(50).backgroundColor('#FFD700').borderRadius(12)
          .translate({ x: this.panX, y: this.panY })
          .gesture(
            PanGesture({ fingers: 1, direction: PanDirection.All, distance: 3 })
              .onActionUpdate((event: GestureEvent) => {
                let newX = this.panX + event.offsetX;
                let newY = this.panY + event.offsetY;
                // 限制在 [-maxOffset, maxOffset] 范围内
                newX = Math.max(-this.maxOffset, Math.min(this.maxOffset, newX));
                newY = Math.max(-this.maxOffset, Math.min(this.maxOffset, newY));
                this.panX = newX;
                this.panY = newY;
              })
              .onActionEnd(() => {
                // 弹性回中
                animateTo({ duration: 200, curve: Curve.Friction }, () => {
                  this.panX = 0;
                  this.panY = 0;
                });
              })
          )
      }
      .width(200).height(200).borderRadius(16)
      .backgroundColor('rgba(156,39,176,0.2)').clip(true)
    }
  }
}

边界限制公式Math.max(-max, Math.min(max, newValue)) 将值限制在 [-max, max] 内。

回弹动画animateTo({ duration: 200, curve: Curve.Friction }, () => { this.panX = 0; })Curve.Friction 摩擦力曲线模拟真实减速回弹。


五、主页面整合

@Entry
@Component
struct GestureDemo {
  build() {
    Column() {
      Row() { Text('👆 Gesture 基础手势').fontSize(20) }
        .width('100%').height(56).backgroundColor('rgba(0,0,0,0.3)')

      Scroll() {
        Column() {
          TapGestureDemo()
          LongPressGestureDemo()
          PanGestureDemo()
          GestureGroupDemo()
          GestureParamsDemo()
          BoundedPanGestureDemo()

          Column() {
            Text('📖 要点总结').fontSize(16).fontColor('#FFD700')

            Text('1. TapGesture:count 控制次数(1=单击/2=双击),onAction 获取坐标。')

            Text('2. LongPressGesture:duration 控制时长,onAction 触发长按,onActionEnd 处理抬起。')

            Text('3. PanGesture:direction/ distance 控制方向与灵敏度。onActionUpdate 获取偏移增量。')

            Text('4. 多手势:多次 .gesture() 并行绑定。')

            Text('5. 回调:onActionStart / onActionUpdate / onActionEnd / onActionCancel。')

            Text('6. 边界:onActionUpdate 中用 Math.max/min 约束偏移,animateTo 实现回弹。')
          }
          .width('100%').padding(20)
          .backgroundColor('rgba(0,0,0,0.25)').borderRadius(16)
        }.width('100%').padding(16)
      }.layoutWeight(1)
    }.width('100%').height('100%')
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [['#1a1a2e', 0], ['#16213e', 0.5], ['#0f3460', 1]]
    })
  }
}

六、进阶技巧

6.1 手势冲突

父子组件都绑定了手势时,子组件手势优先响应。可通过以下方式调整:

// 父组件抢优先权
.parentGesture(PanGesture().onActionUpdate(() => {}))
// 子组件声明并行
.parallelGesture(TapGesture().onAction(() => {}))

6.2 手势与动画组合

.onActionEnd(() => {
  if (Math.abs(this.offsetX) > 100) {
    animateTo({ duration: 300 }, () => { this.offsetX = 300; });
  } else {
    animateTo({ duration: 200, curve: Curve.Friction }, () => { this.offsetX = 0; });
  }
})

6.3 视觉反馈建议

状态 反馈
点击按下 缩放 0.95 + 背景加深
长按触发 背景变色
拖拽中 放大 + 阴影增强

七、常见问题

Q1:双击时单击的 onAction 会触发吗?
A:不会。系统等待约 300ms 确认是否二次点击,双击只触发 count:2 回调。

Q2:如何同时支持点击和长按?
A:多次 .gesture() 分别绑定 TapGesture 和 LongPressGesture,两者并行识别。

Q3:PanGesture 的 distance 设多大?
A:推荐 5~10vp。太小易误触,太大响应迟钝。

Q4:event.offsetX 和 translate 的关系?
A:offsetX 是增量,需累加后用 translate({ x: 累计值 }) 应用。

Q5:怎么松手回弹?
A:onActionEnd 中调用 animateTo({ duration:200, curve:Curve.Friction }, () => { 归零 })


八、总结

场景 技术 交互
1 TapGesture 单击/双击 + 坐标获取
2 LongPressGesture 时长控制
3 PanGesture 四回调完整演示
4 多手势组合(点击+长按)
5 单击/双击/长按参数对比
6 边界限制 + 松手回弹

核心要点:

TapGesture({count})         → onAction → 点击坐标
LongPressGesture({duration}) → onAction / onActionEnd → 长按
PanGesture({direction,dist}) → 四回调 → 拖拽
多次 .gesture()             → 多手势共存
Math.max/min               → 边界限制
animateTo                  → 松手回弹

掌握这三种基础手势的绑定与回调,是构建流畅、自然交互体验的第一步。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐