• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 手势拦截
2
3手势拦截主要用于确保手势按需执行,有效解决手势冲突问题。典型应用场景包括:嵌套滚动、通过过滤组件响应手势的范围来优化交互体验。手势拦截主要采用[手势触发控制](#手势触发控制)和[手势响应控制](#手势响应控制)两种方式实现。
4
5## 手势触发控制
6
7手势触发控制是指在系统判定阈值已满足的条件下,应用可自行判断是否应拦截手势,使手势操作失败。
8
9**图1** 手势触发控制流程图
10
11![gesture_judge](figures/gesture_judge.png)
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![gesture_judge_image_response_region](figures/gesture_judge_image_response_region.png)
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![gesture_judge_controller](figures/gesture_judge_controller.png)
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   ```