文章目录
- 每个手势都要有明确反馈
- 拖拽和捏合要注意收口
- 手势冲突别靠猜
- 完整代码
- 写在最后
手势交互最容易写成“能触发就行”。但真到页面里,点击、双击、长按、滑动、拖拽、捏合放在一起,重点就不是 API 名字,而是每个区域到底响应什么、状态怎么反馈。
这个页面把几种常见手势分开放在不同卡片里,学习成本比较低。你可以先跑起来感受交互,再回头看每个gesture()里面绑定了什么状态。
每个手势都要有明确反馈
单击和双击用计数器反馈,长按用背景色和文案反馈,滑动用方向文字反馈,拖拽用偏移值反馈,捏合用缩放比例反馈。这样的示例才有参考价值,因为它不是只证明“事件触发了”,而是告诉用户当前操作产生了什么结果。
页面里的状态也围绕反馈来设计:tapCount、doubleTapCount、longPressActive、swipeDir、panDeltaX、panDeltaY、pinchScale都不是摆设,它们直接绑定到 UI 上。
拖拽和捏合要注意收口
拖拽区域里,PanGesture在onActionUpdate里更新偏移,松手后用animateTo回到中心。捏合也是一样,更新时限制缩放范围,结束后恢复到 1。
这个“收口”很重要。没有收口的手势很容易把元素拖到奇怪位置,或者把缩放比例留在不可控状态。写交互时,开始、更新、结束三个阶段都要想清楚。
手势冲突别靠猜
实际项目里,不建议把所有手势堆在同一个组件上。比如列表项里既要点击进入详情,又要长按打开菜单,还要左右滑删除,就必须明确触发区域和手势优先级。这个案例把每种手势拆开讲,正好适合先理解单个手势的行为。
完整代码
下面是整理后的完整代码,入口组件名已经换成和当前场景匹配的名字,放进自己的页面目录后可以按项目路由规则接入。
// 手势处理:点击、长按、拖拽interfaceDragItem{id:numberlabel:stringcolor:stringbgColor:stringx:numbery:number}@Entry@Componentstruct GestureInteractionPage{@StatetapCount:number=0@StatedoubleTapCount:number=0@StatelongPressActive:boolean=false@StatelongPressMsg:string='长按此处'@StatepanDeltaX:number=0@StatepanDeltaY:number=0@StatepanVelocityX:number=0@StateballColor:string='#2563EB'@StateswipeDir:string='等待滑动'@StatepinchScale:number=1@StaterotationAngle:number=0privatecolorList:string[]=['#2563EB','#EF4444','#059669','#D97706','#7C3AED','#EC4899']privatecolorIdx:number=0build(){Scroll(){Column({space:0}){// 标题Text('手势处理').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#111827').width('100%').padding({left:20,right:20,top:20,bottom:4})Text('Tap / LongPress / Pan / Swipe / Pinch').fontSize(13).fontColor('#6B7280').width('100%').padding({left:20,right:20,bottom:16})// 一、点击与双击Column({space:12}){Text('一、点击 / 双击手势').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Row({space:12}){// 单击Column({space:8}){Text('👆').fontSize(32).width(80).height(80).textAlign(TextAlign.Center).backgroundColor('#EFF6FF').borderRadius(16).gesture(TapGesture({count:1}).onAction(()=>{this.tapCount++}))Text('单击 ×'+this.tapCount.toString()).fontSize(12).fontColor('#2563EB')}.alignItems(HorizontalAlign.Center)// 双击Column({space:8}){Text('👆👆').fontSize(24).width(80).height(80).textAlign(TextAlign.Center).backgroundColor('#D1FAE5').borderRadius(16).gesture(TapGesture({count:2}).onAction(()=>{this.doubleTapCount++}))Text('双击 ×'+this.doubleTapCount.toString()).fontSize(12).fontColor('#059669')}.alignItems(HorizontalAlign.Center)// 变色(点击)Column({space:8}){Text('🎨').fontSize(32).width(80).height(80).textAlign(TextAlign.Center).backgroundColor(this.ballColor).borderRadius(40).gesture(TapGesture({count:1}).onAction(()=>{this.colorIdx=(this.colorIdx+1)%this.colorList.lengththis.ballColor=this.colorList[this.colorIdx]}))Text('点击变色').fontSize(12).fontColor('#6B7280')}.alignItems(HorizontalAlign.Center)}.width('100%').justifyContent(FlexAlign.SpaceAround).padding(14).backgroundColor('#F9FAFB').borderRadius(10)}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:16})// 二、长按手势Column({space:12}){Text('二、长按手势 LongPress').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:8}){Stack({alignContent:Alignment.Center}){Row().width('100%').height(72).borderRadius(10).backgroundColor(this.longPressActive?'#FEF3C7':'#F9FAFB').borderWidth(this.longPressActive?2:1).borderColor(this.longPressActive?'#D97706':'#E5E7EB').gesture(LongPressGesture({duration:500}).onAction((event:GestureEvent)=>{this.longPressActive=truethis.longPressMsg='长按触发!松开手指'}).onActionEnd(()=>{this.longPressActive=falsethis.longPressMsg='已松开,长按 500ms 触发'}))Text(this.longPressMsg).fontSize(14).fontColor(this.longPressActive?'#D97706':'#9CA3AF').fontWeight(this.longPressActive?FontWeight.Bold:FontWeight.Normal)}.width('100%')}}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:16})// 三、滑动手势Column({space:12}){Text('三、滑动方向 Swipe').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:8}){Stack({alignContent:Alignment.Center}){Row().width('100%').height(100).borderRadius(10).backgroundColor('#F5F3FF').borderWidth(1).borderColor('#DDD6FE').gesture(SwipeGesture({direction:SwipeDirection.All}).onAction((event:GestureEvent)=>{constangle=event.angleif(angle>-45&&angle<45){this.swipeDir='→ 向右滑动'}elseif(angle>135||angle<-135){this.swipeDir='← 向左滑动'}elseif(angle>45&&angle<135){this.swipeDir='↓ 向下滑动'}else{this.swipeDir='↑ 向上滑动'}}))Column({space:4}){Text('👉 在此区域内滑动').fontSize(13).fontColor('#9CA3AF')Text(this.swipeDir).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#7C3AED')}.alignItems(HorizontalAlign.Center)}.width('100%')}}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:16})// 四、拖拽手势(位移)Column({space:12}){Text('四、拖拽手势 Pan').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:8}){Stack({alignContent:Alignment.Center}){Row().width('100%').height(140).borderRadius(10).backgroundColor('#F3F4F6')Text('🟣').fontSize(32).translate({x:this.panDeltaX,y:this.panDeltaY}).gesture(PanGesture({direction:PanDirection.All}).onActionStart(()=>{}).onActionUpdate((event:GestureEvent)=>{this.panDeltaX=event.offsetXthis.panDeltaY=event.offsetY}).onActionEnd(()=>{animateTo({duration:300,curve:Curve.EaseOut},()=>{this.panDeltaX=0this.panDeltaY=0})}))}.width('100%')Text(`偏移:X=${this.panDeltaX.toFixed(0)}Y=${this.panDeltaY.toFixed(0)}松手自动回中`).fontSize(12).fontColor('#9CA3AF')}}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:16})// 五、捏合缩放Column({space:12}){Text('五、捏合缩放 Pinch').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:8}){Stack({alignContent:Alignment.Center}){Row().width('100%').height(140).borderRadius(10).backgroundColor('#ECFDF5')Text('🌍').fontSize(48).scale({x:this.pinchScale,y:this.pinchScale}).gesture(PinchGesture().onActionUpdate((event:GestureEvent)=>{this.pinchScale=Math.max(0.3,Math.min(3,event.scale))}).onActionEnd(()=>{animateTo({duration:300,curve:Curve.EaseOut},()=>{this.pinchScale=1})}))}.width('100%')Text(`缩放比:${this.pinchScale.toFixed(2)}双指捏合/展开,松手复原`).fontSize(12).fontColor('#9CA3AF')}}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:24})}}.width('100%').height('100%').backgroundColor('#FFFFFF')}}写在最后
这个例子不复杂,但很适合反复看。很多 ArkUI 页面写不顺,其实不是组件不会用,而是状态、结构和反馈没有放在一起设计。