《相机焦距缩放》二、捏合手势使用指南
HarmonyOS PinchGesture 捏合手势使用指南
本指南系统讲解 HarmonyOS ArkUI 中
PinchGesture捏合手势的使用方法,从基础概念到实战示例,帮助开发者快速掌握双指缩放手势的实现。
效果
一、概述
PinchGesture是 ArkUI 提供的基础手势之一,用于识别双指(或多指)捏合手势。典型应用场景包括:
- 图片/地图的缩放
- 相机焦距缩放控制
- 文档/网页的放大缩小
- 游戏界面的视角缩放
核心特性
| 特性 | 说明 |
|---|---|
| 最少手指 | 2 指 |
| 最多手指 | 5 指 |
| 最小识别距离 | 5vp |
| 鼠标/键盘支持 | Ctrl + 鼠标滚轮(在支持设备上) |
| 起始版本 | API Version 7 |
二、基本语法
2.1 构造函数
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 属性
interfaceGestureEvent{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 最简单的捏合手势
@ComponentV2struct SimplePinchExample{@LocalscaleValue: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 带起始和结束回调
@ComponentV2struct PinchWithCallbacks{@LocalscaleValue:number=1;@LocalstatusText: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 开始:
@ComponentV2struct AccumulativeZoom{@LocalcurrentScale:number=1;// 当前显示的缩放比例@LocalbaseScale: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 限制缩放范围
@ComponentV2struct BoundedZoom{@LocalcurrentScale:number=1;@LocalbaseScale:number=1;privateminScale:number=0.5;privatemaxScale: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)=>{letnewScale=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 图片缩放查看器
@ComponentV2struct ImageZoomViewer{@LocalimageScale:number=1;@LocalbaseScale: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)=>{letnewScale=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 结合拖拽手势实现平移+缩放
import{gestureModifier}from'@kit.ArkUI';@ComponentV2struct PanAndZoom{@LocalimageScale:number=1;@LocalbaseScale:number=1;@LocaloffsetX:number=0;@LocaloffsetY:number=0;@LocalstartOffsetX:number=0;@LocalstartOffsetY: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与相机焦距控制结合:
// 假设 photoSession 已初始化letzoomRatioRange:number[]=[1.0,10.0];// 从 getZoomRatioRange() 获取letcurrentZoom:number=1.0;letbaseZoom:number=1.0;XComponent({type:XComponentType.SURFACE,controller:xComponentController}).gesture(PinchGesture({fingers:2}).onActionUpdate((event:GestureEvent)=>{// 计算目标缩放值lettargetZoom=baseZoom*event.scale;// 限制在相机支持范围内if(targetZoom>zoomRatioRange[1]){targetZoom=zoomRatioRange[1];}elseif(targetZoom<zoomRatioRange[0]){targetZoom=zoomRatioRange[0];}currentZoom=targetZoom;// 设置相机焦距photoSession.setZoomRatio(targetZoom);}).onActionEnd(()=>{// 保存当前焦距作为下次手势的基准baseZoom=photoSession.getZoomRatio();}))六、手势组合
6.1 串行组合手势(GestureGroup - Sequence)
// 先捏合再旋转.gesture(GestureGroup(GestureMode.Sequence,PinchGesture(),RotationGesture()))6.2 并行组合手势(GestureGroup - Parallel)
// 同时支持捏合和拖拽.gesture(GestureGroup(GestureMode.Parallel,PinchGesture(),PanGesture()))6.3 互斥组合手势(GestureGroup - Exclusive)
// 优先识别捏合,其次识别点击.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中保存当前值,下次手势开始时作为基准:
.onActionUpdate((event)=>{this.currentScale=this.baseScale*event.scale;}).onActionEnd(()=>{this.baseScale=this.currentScale;// 保存为下次基准})7.3 如何限制缩放范围?
在onActionUpdate中使用Math.max()和Math.min()进行边界限制:
letclampedScale=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()可与其他手势共存
