HarmonyOS PinchGesture 捏合手势使用指南
本指南系统讲解 HarmonyOS ArkUI 中
PinchGesture捏合手势的使用方法,从基础概念到实战示例,帮助开发者快速掌握双指缩放手势的实现。
效果

一、概述
PinchGesture 是 ArkUI 提供的基础手势之一,用于识别双指(或多指)捏合手势。典型应用场景包括:
- 图片/地图的缩放
- 相机焦距缩放控制
- 文档/网页的放大缩小
- 游戏界面的视角缩放
核心特性
| 特性 | 说明 |
|---|---|
| 最少手指 | 2 指 |
| 最多手指 | 5 指 |
| 最小识别距离 | 5vp |
| 鼠标/键盘支持 | Ctrl + 鼠标滚轮(在支持设备上) |
| 起始版本 | API Version 7 |
二、基本语法
2.1 构造函数
typescript
PinchGesture(value?: { fingers?: number; distance?: number })
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fingers |
number |
2 |
触发手势所需的最少手指数量(2~5) |
distance |
number |
5 |
最小识别距离,单位 vp |
2.2 回调事件
PinchGesture 提供三个回调:
| 回调 | 触发时机 | 参数 |
|---|---|---|
onActionStart |
手势识别成功时 | event: GestureEvent |
onActionUpdate |
手势状态持续更新时(手指移动) | event: GestureEvent |
onActionEnd |
手势结束时(手指抬起) | event: GestureEvent |
2.3 GestureEvent 属性
typescript
interface GestureEvent {
scale: number; // 捏合缩放比例(相对于手势开始时的比例)
centerX: number; // 双指中心点 X 坐标
centerY: number; // 双指中心点 Y 坐标
offsetX: number; // X 方向偏移量
offsetY: number; // Y 方向偏移量
}
关键属性 scale 说明:
- 手势开始时
scale = 1.0 - 双指张开(放大)时
scale > 1.0 - 双指捏合(缩小)时
scale < 1.0 - 该值是相对于手势开始时的累积比例,不是增量
三、基础用法
3.1 最简单的捏合手势
typescript
@ComponentV2
struct SimplePinchExample {
@Local scaleValue: number = 1;
build() {
Column() {
Text(`缩放比例: ${this.scaleValue.toFixed(2)}x`)
.fontSize(20)
.margin({ bottom: 20 })
Box()
.width(200)
.height(200)
.backgroundColor('#4FC08D')
.borderRadius(16)
.scale({ x: this.scaleValue, y: this.scaleValue })
.gesture(
PinchGesture()
.onActionUpdate((event: GestureEvent) => {
this.scaleValue = event.scale;
})
.onActionEnd(() => {
// 手势结束后保留当前缩放值
console.info('捏合结束, 最终比例: ' + this.scaleValue.toFixed(2));
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
3.2 带起始和结束回调
typescript
@ComponentV2
struct PinchWithCallbacks {
@Local scaleValue: number = 1;
@Local statusText: string = '等待手势...';
build() {
Column({ space: 20 }) {
Text(this.statusText)
.fontSize(16)
.fontColor('#666')
Text(`当前缩放: ${this.scaleValue.toFixed(2)}x`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Box()
.width(150)
.height(150)
.backgroundColor('#3B82F6')
.borderRadius(12)
.scale({ x: this.scaleValue, y: this.scaleValue })
.gesture(
PinchGesture({ fingers: 2 })
.onActionStart(() => {
this.statusText = '手势开始 - 正在缩放';
})
.onActionUpdate((event: GestureEvent) => {
this.scaleValue = event.scale;
})
.onActionEnd(() => {
this.statusText = '手势结束';
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
四、进阶用法
4.1 累积缩放(基于初始值)
实际应用中,通常需要在每次捏合时基于当前已缩放的比例继续缩放,而非每次从 1.0 开始:
typescript
@ComponentV2
struct AccumulativeZoom {
@Local currentScale: number = 1; // 当前显示的缩放比例
@Local baseScale: number = 1; // 手势开始时的基准比例
build() {
Column() {
Text(`缩放: ${this.currentScale.toFixed(2)}x`)
.fontSize(20)
.margin({ bottom: 20 })
Box()
.width(200)
.height(200)
.backgroundColor('#8B5CF6')
.borderRadius(16)
.scale({ x: this.currentScale, y: this.currentScale })
.gesture(
PinchGesture()
.onActionUpdate((event: GestureEvent) => {
// 当前缩放 = 基准值 × 手势比例
this.currentScale = this.baseScale * event.scale;
})
.onActionEnd(() => {
// 手势结束后,将当前值保存为下次的基准值
this.baseScale = this.currentScale;
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
4.2 限制缩放范围
typescript
@ComponentV2
struct BoundedZoom {
@Local currentScale: number = 1;
@Local baseScale: number = 1;
private minScale: number = 0.5;
private maxScale: number = 5.0;
build() {
Column() {
Text(`缩放: ${this.currentScale.toFixed(2)}x`)
.fontSize(20)
.margin({ bottom: 20 })
Box()
.width(200)
.height(200)
.backgroundColor('#EF4444')
.borderRadius(16)
.scale({ x: this.currentScale, y: this.currentScale })
.gesture(
PinchGesture()
.onActionUpdate((event: GestureEvent) => {
let newScale = this.baseScale * event.scale;
// 限制在 [minScale, maxScale] 范围内
newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
this.currentScale = newScale;
})
.onActionEnd(() => {
this.baseScale = this.currentScale;
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
4.3 图片缩放查看器
typescript
@ComponentV2
struct ImageZoomViewer {
@Local imageScale: number = 1;
@Local baseScale: number = 1;
build() {
Stack() {
Image($r('app.media.sample_image'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Contain)
.scale({ x: this.imageScale, y: this.imageScale })
.gesture(
PinchGesture()
.onActionUpdate((event: GestureEvent) => {
let newScale = this.baseScale * event.scale;
newScale = Math.max(1.0, Math.min(5.0, newScale));
this.imageScale = newScale;
})
.onActionEnd(() => {
this.baseScale = this.imageScale;
})
)
// 缩放比例提示
Text(`${this.imageScale.toFixed(1)}x`)
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('rgba(0,0,0,0.5)')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(20)
.position({ x: '50%', y: '90%' })
.translate({ x: -30 })
}
.width('100%')
.height('100%')
}
}
4.4 结合拖拽手势实现平移+缩放
typescript
import { gestureModifier } from '@kit.ArkUI';
@ComponentV2
struct PanAndZoom {
@Local imageScale: number = 1;
@Local baseScale: number = 1;
@Local offsetX: number = 0;
@Local offsetY: number = 0;
@Local startOffsetX: number = 0;
@Local startOffsetY: number = 0;
build() {
Column() {
Image($r('app.media.sample_image'))
.width('100%')
.height(400)
.objectFit(ImageFit.Contain)
.scale({ x: this.imageScale, y: this.imageScale })
.translate({ x: this.offsetX, y: this.offsetY })
.gesture(
PinchGesture()
.onActionUpdate((event: GestureEvent) => {
this.imageScale = Math.max(0.5,
Math.min(5.0, this.baseScale * event.scale));
})
.onActionEnd(() => {
this.baseScale = this.imageScale;
})
)
.parallelGesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
this.offsetX = this.startOffsetX + event.offsetX;
this.offsetY = this.startOffsetY + event.offsetY;
})
.onActionEnd(() => {
this.startOffsetX = this.offsetX;
this.startOffsetY = this.offsetY;
})
)
}
}
}
五、在相机焦距缩放中的应用
以下示例展示如何将 PinchGesture 与相机焦距控制结合:
typescript
// 假设 photoSession 已初始化
let zoomRatioRange: number[] = [1.0, 10.0]; // 从 getZoomRatioRange() 获取
let currentZoom: number = 1.0;
let baseZoom: number = 1.0;
XComponent({
type: XComponentType.SURFACE,
controller: xComponentController
})
.gesture(
PinchGesture({ fingers: 2 })
.onActionUpdate((event: GestureEvent) => {
// 计算目标缩放值
let targetZoom = baseZoom * event.scale;
// 限制在相机支持范围内
if (targetZoom > zoomRatioRange[1]) {
targetZoom = zoomRatioRange[1];
} else if (targetZoom < zoomRatioRange[0]) {
targetZoom = zoomRatioRange[0];
}
currentZoom = targetZoom;
// 设置相机焦距
photoSession.setZoomRatio(targetZoom);
})
.onActionEnd(() => {
// 保存当前焦距作为下次手势的基准
baseZoom = photoSession.getZoomRatio();
})
)
六、手势组合
6.1 串行组合手势(GestureGroup - Sequence)
typescript
// 先捏合再旋转
.gesture(
GestureGroup(GestureMode.Sequence,
PinchGesture(),
RotationGesture()
)
)
6.2 并行组合手势(GestureGroup - Parallel)
typescript
// 同时支持捏合和拖拽
.gesture(
GestureGroup(GestureMode.Parallel,
PinchGesture(),
PanGesture()
)
)
6.3 互斥组合手势(GestureGroup - Exclusive)
typescript
// 优先识别捏合,其次识别点击
.gesture(
GestureGroup(GestureMode.Exclusive,
PinchGesture(),
TapGesture()
)
)
七、常见问题与注意事项
7.1 scale 是累积值还是增量?
event.scale 是相对于手势开始时的累积比例,不是每次回调的增量。
手势开始: scale = 1.0
手指张开: scale = 1.2 (表示比开始时放大了 20%)
继续张开: scale = 1.5 (表示比开始时放大了 50%)
手指捏合: scale = 0.8 (表示比开始时缩小了 20%)
7.2 如何实现连续累积缩放?
需要在 onActionEnd 中保存当前值,下次手势开始时作为基准:
typescript
.onActionUpdate((event) => {
this.currentScale = this.baseScale * event.scale;
})
.onActionEnd(() => {
this.baseScale = this.currentScale; // 保存为下次基准
})
7.3 如何限制缩放范围?
在 onActionUpdate 中使用 Math.max() 和 Math.min() 进行边界限制:
typescript
let clampedScale = Math.max(minScale, Math.min(maxScale, computedScale));
7.4 PinchGesture 与 Scroll 冲突
当组件在 Scroll 容器中时,PinchGesture 可能与滚动冲突。解决方案:
- 使用
.hitTestBehavior(HitTestMode.Block)阻止事件穿透 - 使用
parallelGesture()替代gesture()使手势并行识别 - 在缩放时临时禁用滚动
7.5 多指手势的手指数量
fingers 参数指定的是最少手指数量,不是固定手指数量。设置为 2 时,2~5 指均可触发。
八、API 速查表
| API | 说明 |
|---|---|
PinchGesture({ fingers?, distance? }) |
创建捏合手势 |
.onActionStart(callback) |
手势识别成功回调 |
.onActionUpdate(callback) |
手势持续更新回调 |
.onActionEnd(callback) |
手势结束回调 |
event.scale |
缩放比例(累积值) |
event.centerX / centerY |
双指中心点坐标 |
GestureGroup(GestureMode.Parallel, ...) |
并行组合手势 |
.parallelGesture(gesture) |
组件并行手势(不阻止默认行为) |
九、总结
PinchGesture 的使用核心流程:
创建 PinchGesture → 绑定到组件 .gesture()
→ onActionUpdate 中处理缩放逻辑(注意累积缩放和范围限制)
→ onActionEnd 中保存基准值
关键要点:
event.scale是累积比例,需要配合基准值实现连续缩放- 始终对缩放范围进行限制,避免超出合理值
- 在相机场景中,缩放范围由
getZoomRatioRange()决定 - 结合
parallelGesture()可与其他手势共存