在 ArkUI 中,PanGesture(滑动/拖拽)和 PinchGesture(捏合缩放)是实现复杂交互的核心手势。它们的坐标计算逻辑各有特点。

一、 PanGesture 滑动坐标计算

PanGesture 的核心在于增量叠加。在 onActionUpdate 回调中,系统提供的是相对于手势起始点的偏移量(event.offsetX / event.offsetY),单位为 vp。

核心计算逻辑:
  1. 记录初始状态:在 onActionStart 或首次触发时,记录组件当前的绝对位置(positionXpositionY)。
  2. 实时叠加偏移:在 onActionUpdate 中,将当前偏移量与初始位置相加,得到组件的新位置。
  3. 更新基准位置:在 onActionEnd 中,将组件最终的位置保存为下一次滑动的初始位置。
实战代码:
@Entry
@Component
struct PanGestureDemo {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  // 记录手指按下时组件的绝对位置
  @State positionX: number = 0;
  @State positionY: number = 0;

  build() {
    Column() {
      Text('拖拽我')
        .width(100)
        .height(100)
        .backgroundColor('#4A90E2')
        .borderRadius(10)
        .translate({ x: this.offsetX, y: this.offsetY }) // 使用 translate 移动组件
        .gesture(
          PanGesture()
            .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;
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

二、 PinchGesture 缩放坐标计算

PinchGesture 的坐标计算比滑动复杂,它不仅涉及缩放比例(Scale)的累积,还涉及缩放中心点(Center)的坐标转换。

1. 缩放比例(Scale)的累积运算

event.scale 是相对于本次捏合起始点的比例(起始时为 1.0)。为了实现连续缩放,必须引入一个基准值(savedScale):

  • 当前总缩放 = 历史基准缩放 × 本次相对缩放 (event.scale)
2. 缩放中心点(Center)的坐标转换

event.pinchCenterX/Y 的单位是 vp,原点在组件左上角。而 .scale({ centerX, centerY }) 属性接收的是归一化值(0.0 ~ 1.0)

  • 转换公式centerX = pinchCenterX / 组件宽度centerY = pinchCenterY / 组件高度
实战代码:
@Entry
@Component
struct PinchGestureDemo {
  @State currentScale: number = 1.0;
  @State savedScale: number = 1.0; // 记录历史缩放基准
  @State centerX: number = 0.5;
  @State centerY: number = 0.5;

  build() {
    Column() {
      Text('双指捏合缩放')
        .width(200)
        .height(200)
        .backgroundColor('#4A90E2')
        .borderRadius(10)
        // 【核心】:应用缩放比例和动态中心点
        .scale({ x: this.currentScale, y: this.currentScale, centerX: this.centerX, centerY: this.centerY })
        .gesture(
          PinchGesture({ fingers: 2 })
            .onActionUpdate((event: GestureEvent | undefined) => {
              if (event) {
                // 1. 累积计算缩放比例
                this.currentScale = this.savedScale * event.scale;

                // 2. 坐标转换:将 vp 坐标转换为 0~1 的归一化值
                // 假设组件宽高为 200
                this.centerX = event.pinchCenterX / 200;
                this.centerY = event.pinchCenterY / 200;
              }
            })
            .onActionEnd(() => {
              // 捏合结束,将当前比例保存为下一次的基准值
              this.savedScale = this.currentScale;
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

三、 进阶优化建议

  1. 边界限制:在 onActionEnd 中,建议对 currentScale 或 offsetX/Y 进行边界检查(如限制缩放范围在 0.5 ~ 3.0 之间),防止组件缩放过小或飞出屏幕。
  2. 丝滑回弹动画:在 onActionEnd 中,如果检测到缩放或位移超出了边界,可以使用 animateTo 配合 Curve.Friction(摩擦力曲线)实现丝滑的回弹效果。
  3. 性能优化:高频触发的 onActionUpdate 中,尽量避免复杂的 DOM 查询或重计算,仅更新 @State 变量,让框架自动进行 UI 刷新。
1、 动态缩放中心点(PinchGesture 进阶)

在实际的图片查看器或地图应用中,缩放中心点必须跟随用户的双指中心实时变化,而不是固定在组件中心。

核心计算逻辑

  1. 获取双指中心坐标(event.pinchCenterX/Y,单位 vp)。
  2. 将 vp 坐标转换为组件内部的相对比例(0.0 ~ 1.0)。
  3. 动态更新 .scale() 的 centerX 和 centerY 属性。
@Entry
@Component
struct DynamicPinchDemo {
  @State currentScale: number = 1.0;
  @State savedScale: number = 1.0;
  @State centerX: number = 0.5; // 默认中心
  @State centerY: number = 0.5;
  
  // 组件的固定尺寸(实际开发中可通过 onAreaChange 动态获取)
  private componentSize: number = 200;

  build() {
    Column() {
      Text('️')
        .fontSize(80)
        .width(this.componentSize)
        .height(this.componentSize)
        .backgroundColor('#E3F2FD')
        .borderRadius(16)
        // 【核心】动态绑定缩放中心点
        .scale({ 
          x: this.currentScale, 
          y: this.currentScale, 
          centerX: this.centerX, 
          centerY: this.centerY 
        })
        .gesture(
          PinchGesture({ fingers: 2 })
            .onActionUpdate((event: GestureEvent | undefined) => {
              if (event) {
                // 1. 累积缩放比例
                this.currentScale = Math.max(0.5, Math.min(3.0, 
                  this.savedScale * event.scale
                ));
                
                // 2. 动态计算并更新缩放中心(vp 转归一化值)
                this.centerX = event.pinchCenterX / this.componentSize;
                this.centerY = event.pinchCenterY / this.componentSize;
              }
            })
            .onActionEnd(() => {
              this.savedScale = this.currentScale;
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
2、 实时监测多指触控信息(fingerInfos)

在高级交互中,可能需要根据参与手势的手指数量来切换行为(例如:单指拖拽,双指缩放)。PanGesture 支持通过 event.fingerInfos 实时获取有效触点信息。

@State fingerCount: number = 0;

.gesture(
  PanGesture()
    .onActionStart((event: GestureEvent | undefined) => {
      // 记录初始触点数量
      this.fingerCount = event?.fingerInfos?.length || 0;
    })
    .onActionUpdate((event: GestureEvent | undefined) => {
      if (event) {
        // 实时更新触点数量
        this.fingerCount = event.fingerInfos?.length || 0;
        
        // 根据手指数量执行不同逻辑
        if (this.fingerCount === 1) {
          // 单指拖拽逻辑
        } else if (this.fingerCount >= 2) {
          // 双指特殊交互逻辑
        }
      }
    })
    .onActionEnd(() => {
      this.fingerCount = 0;
    })
)

3、 手势冲突解决(PanGesture vs SwipeGesture)

在列表或 Swiper 组件中嵌套可拖拽元素时,PanGesture 极易与系统的滑动手势冲突。

解决方案

  1. 增大触发阈值:通过 distance 参数提高 Pan 手势的触发门槛,避免轻微滑动被误判为拖拽。
  2. 限定滑动方向:使用 PanDirection 限制拖拽方向,与列表的滑动方向正交。
.gesture(
  PanGesture({ 
    direction: PanDirection.Horizontal, // 仅允许水平拖拽
    distance: 10 // 滑动超过 10vp 才触发,避免与垂直列表冲突
  })
    .onActionUpdate((event: GestureEvent | undefined) => {
      // 拖拽逻辑
    })
)

4、 性能优化与丝滑体验

高频的 onActionUpdate 回调是性能瓶颈的重灾区,以下优化至关重要:

  1. 细粒度状态更新:使用 @ObservedV2 和 @Trace 装饰器替代传统的 @State,仅追踪发生变化的坐标属性,减少不必要的 UI 重绘。
  2. 硬件加速动画:在 onActionEnd 中触发回弹或吸附动画时,务必使用 animateTo 并设置较短的 duration(如 200ms),框架会自动启用硬件加速渲染。
  3. 避免重计算:在 onActionUpdate 中仅更新 @State 变量,严禁进行 DOM 查询、复杂数学运算或网络请求。
// 丝滑回弹示例
.onActionEnd(() => {
  this.positionX = this.offsetX;
  this.positionY = this.offsetY;
  
  // 边界检查与回弹
  if (this.offsetX < -100) {
    animateTo({ duration: 200, curve: Curve.Friction }, () => {
      this.offsetX = -100;
    });
  }
})
Logo

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

更多推荐