1# 手势拦截 2 3手势拦截主要用于确保手势按需执行,有效解决手势冲突问题。典型应用场景包括:嵌套滚动、通过过滤组件响应手势的范围来优化交互体验。手势拦截主要采用[手势触发控制](#手势触发控制)和[手势响应控制](#手势响应控制)两种方式实现。 4 5## 手势触发控制 6 7手势触发控制是指在系统判定阈值已满足的条件下,应用可自行判断是否应拦截手势,使手势操作失败。 8 9**图1** 手势触发控制流程图 10 11 12 13手势触发控制涉及以下接口。 14 15| **接口** | **说明** | 16| ------- | -------------- | 17|[onGestureJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-customize-judge.md#ongesturejudgebegin)|用于手势拦截,是通用事件。在手势满足系统触发阈值场景下,回调给应用判断是否拦截手势。| 18|[onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin)|用于手势拦截、获取手势识别器和设置手势识别器开闭状态。是onGestureJudgeBegin接口的能力扩展,可以代替onGestureJudgeBegin接口。<br>获取手势识别器时,会获取一次交互中手势响应链上的所有手势识别器,以及当前即将触发成功的手势识别器,此时可以设置手势的激活状态。| 19 20以下示例中,Image和Stack两个组件位于同一区域。长按Stack组件的上半部分可触发挂载在Stack组件上的长按手势,长按Stack组件的下半部分则会响应Image组件的拖拽操作。 21 22**图2** 示例图 23 24 25 261. Image组件设置拖拽。 27 28 ```ts 29 Image($r('sys.media.ohos_app_icon')) 30 .draggable(true) 31 .onDragStart(()=>{ 32 promptAction.showToast({ message: "Drag 下半区蓝色区域,Image响应" }); 33 }) 34 .width('200vp').height('200vp') 35 ``` 36 372. Stack组件设置手势。 38 39 ```ts 40 Stack() {} 41 .width('200vp') 42 .height('200vp') 43 .hitTestBehavior(HitTestMode.Transparent) 44 .gesture(GestureGroup(GestureMode.Parallel, 45 LongPressGesture() 46 .onAction((event: GestureEvent) => { 47 promptAction.showToast({ message: "LongPressGesture 长按上半区 红色区域,红色区域响应" }); 48 }) 49 .tag("longpress") 50 )) 51 ``` 52 533. Stack组件设置拦截。 54 55 ```ts 56 .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => { 57 // 如果是长按类型手势,判断点击的位置是否在上半区 58 if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) { 59 if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) { 60 return GestureJudgeResult.CONTINUE; 61 } else { 62 return GestureJudgeResult.REJECT; 63 } 64 } 65 return GestureJudgeResult.CONTINUE; 66 }) 67 ``` 68 694. 代码完整示例。 70 71 ```ts 72 import { PromptAction } from '@kit.ArkUI'; 73 74 @Entry 75 @Component 76 struct Index { 77 scroller: Scroller = new Scroller(); 78 promptAction: PromptAction = this.getUIContext().getPromptAction(); 79 80 build() { 81 Scroll(this.scroller) { 82 Column({ space: 8 }) { 83 Text("包括上下两层组件,上层组件绑定长按手势,下层组件绑定拖拽。其中上层组件下半区域绑定手势拦截,使该区域响应下层拖拽手势。").width('100%').fontSize(20).fontColor('0xffdd00') 84 Stack({ alignContent: Alignment.Center }) { 85 Column() { 86 // 模拟上半区和下半区 87 Stack().width('200vp').height('100vp').backgroundColor(Color.Red) 88 Stack().width('200vp').height('100vp').backgroundColor(Color.Blue) 89 }.width('200vp').height('200vp') 90 // Stack的下半区是绑定了拖动手势的图像区域。 91 Image($r('sys.media.ohos_app_icon')) 92 .draggable(true) 93 .onDragStart(()=>{ 94 this.promptAction.showToast({ message: "Drag 下半区蓝色区域,Image响应" }); 95 }) 96 .width('200vp').height('200vp') 97 // Stack的上半区是绑定了长按手势的浮动区域。 98 Stack() { 99 } 100 .width('200vp') 101 .height('200vp') 102 .hitTestBehavior(HitTestMode.Transparent) 103 .gesture(GestureGroup(GestureMode.Parallel, 104 LongPressGesture() 105 .onAction((event: GestureEvent) => { 106 this.promptAction.showToast({ message: "LongPressGesture 长按上半区 红色区域,红色区域响应" }); 107 }) 108 .tag("longpress") 109 )) 110 .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => { 111 // 如果是长按类型手势,判断点击的位置是否在上半区 112 if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) { 113 if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) { 114 return GestureJudgeResult.CONTINUE; 115 } else { 116 return GestureJudgeResult.REJECT; 117 } 118 } 119 return GestureJudgeResult.CONTINUE; 120 }) 121 }.width('100%') 122 }.width('100%') 123 } 124 } 125 } 126 ``` 127 128## 手势响应控制 129 130手势响应控制指的是手势已经成功识别,但是开发者仍然可以通过调用API接口控制手势回调是否能够响应。 131 132**图3** 手势响应控制流程图 133 134 135手势响应控制的前提是手势识别成功,如果手势不成功则不会产生手势回调响应。 136 1371. 业务手势作业流:指真正触发UI变化的业务手势,比如使页面滚动的PanGesture,触发点击的TapGesture等。 138 1392. 监听手势作业流:指在监听手势运行的过程中,应根据上下文的业务状态变化动态控制手势识别器的开闭,例如判断组件嵌套滚动过程中是否已滑至边缘。这一监听事件可借助一个使用[并行手势绑定方式](arkts-gesture-events-binding.md#parallelgesture并行手势绑定方法)的PanGesture实现,或者采用Touch事件来完成。 140 1413. 设置手势并行:此步骤并非必需,典型场景是在嵌套滚动中,设置外部组件的滚动手势与内部的滚动手势并行。 142 1434. 动态开闭手势:指通过手势识别器的setEnable方法,控制手势是否响应用户回调。 144 145手势响应控制涉及以下接口。 146 147| **接口** | **说明** | 148| ------- | -------------- | 149|[shouldBuiltInRecognizerParallelWith](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#shouldbuiltinrecognizerparallelwith)|用于设置系统组件内置手势与其他手势并行。| 150|[onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin)|用于手势拦截,获取手势识别器,初始化手势识别器开闭状态。| 151|[parallelGesture](arkts-gesture-events-binding.md#parallelgesture并行手势绑定方法)|可使开发者定义的手势,与比他优先级高的手势并行。| 152 153以下示例是两个Scroll组件的嵌套滚动场景,使用手势控制的api去控制外部组件和内部组件的嵌套滚动联动。 154 1551. 使用shouldBuiltInRecognizerParallelWith接口设置外部Scroll组件的PanGesture手势与内部Scroll组件的PanGesture手势并行。 156 157 ```ts 158 .shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => { 159 for (let i = 0; i < others.length; i++) { 160 let target = others[i].getEventTargetInfo(); 161 if (target.getId() == "inner" && others[i].isBuiltIn() && others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // 找到将要组成并行手势的识别器 162 this.currentRecognizer = current; // 保存当前组件的识别器 163 this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器 164 return others[i]; // 返回和当前手势将要组成并行手势的识别器 165 } 166 } 167 return undefined; 168 }) 169 ``` 170 1712. 使用onGestureRecognizerJudgeBegin接口获取到Scroll组件的PanGesture手势识别器,同时根据内外Scroll组件的边界条件,设置内外手势的开闭状态。 172 173 ```ts 174 .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { // 在识别器即将要成功时,根据当前组件状态,设置识别器使能状态 175 let target = current.getEventTargetInfo(); 176 if (target.getId() == "outer" && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { 177 for (let i = 0; i < others.length; i++) { 178 let target = others[i].getEventTargetInfo() as ScrollableTargetInfo; 179 if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // 找到响应链上对应并行的识别器 180 let panEvent = event as PanGestureEvent; 181 this.childRecognizer.setEnabled(true); 182 this.currentRecognizer.setEnabled(false); 183 if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态 184 if (panEvent && panEvent.offsetY < 0) { 185 this.childRecognizer.setEnabled(false); 186 this.currentRecognizer.setEnabled(true); 187 } 188 } else if (target.isBegin()) { 189 if (panEvent.offsetY > 0) { 190 this.childRecognizer.setEnabled(false); 191 this.currentRecognizer.setEnabled(true); 192 } 193 } 194 } 195 } 196 } 197 return GestureJudgeResult.CONTINUE; 198 }) 199 ``` 200 2013. 设置监听手势,监听Scroll组件状态,动态调整手势开闭状态,控制手势回调是否触发,从而控制Scroll是否滚动。 202 203 ```ts 204 .parallelGesture( // 绑定一个Pan手势作为动态控制器 205 PanGesture() 206 .onActionUpdate((event: GestureEvent)=>{ 207 if (this.childRecognizer.getState() != GestureRecognizerState.SUCCESSFUL || this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制 208 return; 209 } 210 let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; 211 let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo; 212 if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) { 213 this.childRecognizer.setEnabled(true); 214 this.currentRecognizer.setEnabled(false); 215 if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态 216 if ((event.offsetY - this.lastOffset) < 0) { 217 this.childRecognizer.setEnabled(false); 218 if (currentTarget.isEnd()) { 219 this.currentRecognizer.setEnabled(false); 220 } else { 221 this.currentRecognizer.setEnabled(true); 222 } 223 } 224 } else if (target.isBegin()) { 225 if ((event.offsetY - this.lastOffset) > 0) { 226 this.childRecognizer.setEnabled(false); 227 if (currentTarget.isBegin()) { 228 this.currentRecognizer.setEnabled(false); 229 } else { 230 this.currentRecognizer.setEnabled(true); 231 } 232 } 233 } 234 } 235 this.lastOffset = event.offsetY 236 }) 237 ) 238 ``` 239 2404. 代码完整示例。 241 242 ```ts 243 // xxx.ets 244 @Entry 245 @Component 246 struct FatherControlChild { 247 scroller: Scroller = new Scroller(); 248 scroller2: Scroller = new Scroller(); 249 private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 250 private childRecognizer: GestureRecognizer = new GestureRecognizer(); 251 private currentRecognizer: GestureRecognizer = new GestureRecognizer(); 252 private lastOffset: number = 0; 253 254 build() { 255 Stack({ alignContent: Alignment.TopStart }) { 256 Scroll(this.scroller) { // 外部滚动容器 257 Column() { 258 Text("Scroll Area") 259 .width('90%') 260 .height(150) 261 .backgroundColor(0xFFFFFF) 262 .borderRadius(15) 263 .fontSize(16) 264 .textAlign(TextAlign.Center) 265 .margin({ top: 10 }) 266 Scroll(this.scroller2) { // 内部滚动容器 267 Column() { 268 Text("Scroll Area2") 269 .width('90%') 270 .height(150) 271 .backgroundColor(0xFFFFFF) 272 .borderRadius(15) 273 .fontSize(16) 274 .textAlign(TextAlign.Center) 275 .margin({ top: 10 }) 276 Column() { 277 ForEach(this.arr, (item: number) => { 278 Text(item.toString()) 279 .width('90%') 280 .height(150) 281 .backgroundColor(0xFFFFFF) 282 .borderRadius(15) 283 .fontSize(16) 284 .textAlign(TextAlign.Center) 285 .margin({ top: 10 }) 286 }, (item: string) => item) 287 }.width('100%') 288 } 289 } 290 .id("inner") 291 .width('100%') 292 .height(800) 293 }.width('100%') 294 } 295 .id("outer") 296 .height(600) 297 .scrollable(ScrollDirection.Vertical) // 滚动方向纵向 298 .scrollBar(BarState.On) // 滚动条常驻显示 299 .scrollBarColor(Color.Gray) // 滚动条颜色 300 .scrollBarWidth(10) // 滚动条宽度 301 .edgeEffect(EdgeEffect.None) 302 .shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => { 303 for (let i = 0; i < others.length; i++) { 304 let target = others[i].getEventTargetInfo(); 305 if (target.getId() == "inner" && others[i].isBuiltIn() && others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // 找到将要组成并行手势的识别器 306 this.currentRecognizer = current; // 保存当前组件的识别器 307 this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器 308 return others[i]; // 返回和当前手势将要组成并行手势的识别器 309 } 310 } 311 return undefined; 312 }) 313 .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { // 在识别器即将要成功时,根据当前组件状态,设置识别器使能状态 314 let target = current.getEventTargetInfo(); 315 if (target.getId() == "outer" && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { 316 for (let i = 0; i < others.length; i++) { 317 let target = others[i].getEventTargetInfo() as ScrollableTargetInfo; 318 if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // 找到响应链上对应并行的识别器 319 let panEvent = event as PanGestureEvent; 320 this.childRecognizer.setEnabled(true); 321 this.currentRecognizer.setEnabled(false); 322 if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态 323 if (panEvent && panEvent.offsetY < 0) { 324 this.childRecognizer.setEnabled(false); 325 this.currentRecognizer.setEnabled(true); 326 } 327 } else if (target.isBegin()) { 328 if (panEvent.offsetY > 0) { 329 this.childRecognizer.setEnabled(false); 330 this.currentRecognizer.setEnabled(true); 331 } 332 } 333 } 334 } 335 } 336 return GestureJudgeResult.CONTINUE; 337 }) 338 .parallelGesture( // 绑定一个Pan手势作为动态控制器 339 PanGesture() 340 .onActionUpdate((event: GestureEvent)=>{ 341 if (this.childRecognizer.getState() != GestureRecognizerState.SUCCESSFUL || this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制 342 return; 343 } 344 let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; 345 let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo; 346 if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) { 347 this.childRecognizer.setEnabled(true); 348 this.currentRecognizer.setEnabled(false); 349 if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态 350 if ((event.offsetY - this.lastOffset) < 0) { 351 this.childRecognizer.setEnabled(false); 352 if (currentTarget.isEnd()) { 353 this.currentRecognizer.setEnabled(false); 354 } else { 355 this.currentRecognizer.setEnabled(true); 356 } 357 } 358 } else if (target.isBegin()) { 359 if ((event.offsetY - this.lastOffset) > 0) { 360 this.childRecognizer.setEnabled(false) 361 if (currentTarget.isBegin()) { 362 this.currentRecognizer.setEnabled(false); 363 } else { 364 this.currentRecognizer.setEnabled(true); 365 } 366 } 367 } 368 } 369 this.lastOffset = event.offsetY; 370 }) 371 ) 372 }.width('100%').height('100%').backgroundColor(0xDCDCDC) 373 } 374 } 375 ```