• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Gesture Conflict Handling
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @jiangtao92-->
5<!--Designer: @piggyguy-->
6<!--Tester: @songyanhong-->
7<!--Adviser: @HelloCrease-->
8
9Gesture conflicts occur when multiple gesture recognizers compete for recognition on the same component or overlapping areas, resulting in unexpected behavior. Common conflict scenarios include:
10- Multiple gestures on the same component (for example, both tap and long-press gestures on a button)
11- Gesture recognizers of the same type on parent and child components
12- Conflicts between system default gestures and custom gestures (for example, conflict between the scroll gesture and the click gesture of a child component)
13
14Effective conflict resolution involves gesture intervention. Beyond controlling component response regions and hit test modes, there are three primary approaches: [Custom Gesture Judgment](#custom-gesture-judgment), [Parallel Gesture Dynamic Control](#parallel-gesture-dynamic-control), and [Gesture Recognition Prevention](#gesture-recognition-prevention).
15
16## Custom Gesture Judgment
17
18Custom gesture judgment enables applications to override system recognition decisions. When system thresholds are met, the application can determine whether to intercept the gesture (causing its recognition to fail) and prioritize other gestures instead.
19
20**Figure 1** Custom gesture judgment process
21
22![gesture_judge](figures/gesture_judge.png)
23
24Custom gesture judgment involves the following APIs.
25
26| API| Description|
27| ------- | -------------- |
28|[onGestureJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-customize-judge.md#ongesturejudgebegin)|Used for gesture interception as a universal event. Called when the gesture meets system trigger thresholds, allowing the application to determine whether to intercept the gesture.|
29|[onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin)|Called to implement gesture judgment, obtain gesture recognizers, and set their enabled state. It is an extension of **onGestureJudgeBegin** and can serve as its substitute.<br>When obtaining gesture recognizers, this API obtains all gesture recognizers in the response chain of the current interaction, as well as the recognizer about to be triggered successfully, allowing the enabled state of the gesture to be set.|
30
31In the following example, the **Image** and **Stack** components are located in the same area. Long-pressing the upper half of the **Stack** component triggers the long-press gesture bound to the **Stack** component, while long-pressing the lower half of the **Stack** component triggers the drag operation of the **Image** component.
32
33**Figure 2** Example
34
35![gesture_judge_image_response_region](figures/gesture_judge_image_response_region.png)
36
371. Configure drag settings for the **Image** component.
38
39   ```ts
40   Image($r('sys.media.ohos_app_icon'))
41     .draggable(true)
42     .onDragStart(()=>{
43       promptAction.showToast({ message: "Drag the lower blue area. The Image component responds." });
44     })
45     .width('200vp').height('200vp')
46   ```
47
482. Set up gestures for the **Stack** component.
49
50   ```ts
51   Stack() {}
52     .width('200vp')
53     .height('200vp')
54     .hitTestBehavior(HitTestMode.Transparent)
55     .gesture(GestureGroup(GestureMode.Parallel,
56       LongPressGesture()
57         .onAction((event: GestureEvent) => {
58           promptAction.showToast({ message: "Long-press the upper red area. The red area responds." });
59         })
60         .tag("longpress")
61     ))
62   ```
63
643. Set up gesture judgment for the **Stack** component.
65
66   ```ts
67   .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {
68     // If it is a long press gesture, determine whether the touch position is in the upper half area.
69     if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) {
70       if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) {
71         return GestureJudgeResult.CONTINUE;
72       } else {
73         return GestureJudgeResult.REJECT;
74       }
75     }
76     return GestureJudgeResult.CONTINUE;
77   })
78   ```
79
804. Below is the complete code example.
81
82   ```ts
83   import { PromptAction } from '@kit.ArkUI';
84
85   @Entry
86   @Component
87   struct Index {
88     scroller: Scroller = new Scroller();
89     promptAction: PromptAction = this.getUIContext().getPromptAction();
90
91     build() {
92       Scroll(this.scroller) {
93         Column({ space: 8 }) {
94           Text("There are two layers of components, the upper layer component bound to a long press gesture, and the lower layer component bound to a drag. The lower half of the upper layer component is bound to gesture judgment, making this area respond to the drag gesture of the lower layer.").width('100%').fontSize(20).fontColor('0xffdd00')
95           Stack({ alignContent: Alignment.Center }) {
96             Column() {
97               // Simulate the upper and lower half areas.
98               Stack().width('200vp').height('100vp').backgroundColor(Color.Red)
99               Stack().width('200vp').height('100vp').backgroundColor(Color.Blue)
100             }.width('200vp').height('200vp')
101             // The lower half of the Stack component is the image area bound to the drag gesture.
102             Image($r('sys.media.ohos_app_icon'))
103               .draggable(true)
104               .onDragStart(()=>{
105                 this.promptAction.showToast({ message: "Drag the lower blue area. The Image component responds." });
106               })
107               .width('200vp').height('200vp')
108             // The upper half of the Stack component is the floating area bound to the long press gesture.
109             Stack() {
110             }
111             .width('200vp')
112             .height('200vp')
113             .hitTestBehavior(HitTestMode.Transparent)
114             .gesture(GestureGroup(GestureMode.Parallel,
115               LongPressGesture()
116                 .onAction((event: GestureEvent) => {
117                   this.promptAction.showToast({ message: "Long-press the upper red area. The red area responds." });
118                 })
119                 .tag("longpress")
120             ))
121             .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {
122               // If it is a long press gesture, determine whether the touch position is in the upper half area.
123               if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) {
124                 if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) {
125                   return GestureJudgeResult.CONTINUE;
126                 } else {
127                   return GestureJudgeResult.REJECT;
128                 }
129               }
130               return GestureJudgeResult.CONTINUE;
131             })
132           }.width('100%')
133         }.width('100%')
134       }
135     }
136   }
137   ```
138
139## Parallel Gesture Dynamic Control
140
141Parallel gesture dynamic control allows you to manage whether a gesture callback should be executed, even after the gesture has been successfully recognized.
142
143**Figure 3** Parallel gesture dynamic control process
144
145![gesture_judge_controller](figures/gesture_judge_controller.png)
146Parallel gesture dynamic control is based on the successful recognition of a gesture. If the gesture is not recognized, no callback response will be triggered.
147
1481. Service gesture workflow: This refers to gestures that directly cause changes in the UI, such as **PanGesture** for scrolling pages or **TapGesture** for clicks.
149
1502. Gesture listening workflow: This involves dynamically controlling the start and stop of gesture recognizers based on the current service state. For example, during nested scrolling, the listener can determine whether the component has reached the edge. This can be achieved using **PanGesture** with the [parallel gesture binding method](arkts-gesture-events-binding.md#parallelgesture-parallel-gesture-binding-method) or by using touch events.
151
1523. Parallel gesture configuration: This step is optional. A typical use case is to set the scroll gesture of an outer component to be parallel with the scroll gesture of an inner component during nested scrolling.
153
1544. Dynamic gesture control: This involves controlling whether gestures respond to user callbacks by using the **setEnable** API of gesture recognizers.
155
156Parallel gesture dynamic control involves the following APIs.
157
158| API| Description|
159| ------- | -------------- |
160|[shouldBuiltInRecognizerParallelWith](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#shouldbuiltinrecognizerparallelwith)|Used to set the built-in gestures to be parallel with other gestures.|
161|[onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin)|Called to implement gesture judgment, obtain gesture recognizers, and initialize and their enabled state.|
162|[parallelGesture](arkts-gesture-events-binding.md#parallelgesture-parallel-gesture-binding-method)|Allows custom gestures to be parallel with gestures of higher priority.|
163
164The following example demonstrates a nested scrolling scenario with two **Scroll** components, using gesture control APIs to manage the nested scrolling linkage between the outer and inner components.
165
1661. Use the **shouldBuiltInRecognizerParallelWith** API to set the **PanGesture** of the outer **Scroll** component to be parallel with the **PanGesture** of the inner **Scroll** component.
167
168   ```ts
169   .shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => {
170     for (let i = 0; i < others.length; i++) {
171       let target = others[i].getEventTargetInfo();
172       if (target.getId() == "inner" && others[i].isBuiltIn() && others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // Identify the recognizer to work in parallel.
173         this.currentRecognizer = current; // Save the recognizer of the current component.
174         this.childRecognizer = others[i]; // Save the recognizer to work in parallel.
175         return others[i]; // Return the recognizer to work in parallel with the current one.
176       }
177     }
178     return undefined;
179   })
180   ```
181
1822. Use the **onGestureRecognizerJudgeBegin** API to obtain the pan gesture recognizer of the **Scroll** component and to set the enabled state of the inner and outer gesture recognizers based on the boundary conditions of the **Scroll** components.
183
184   ```ts
185   .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { // When gesture recognition is about to be successful, set the recognizer's enabled state based on the current component state.
186     let target = current.getEventTargetInfo();
187     if (target && target.getId() == "outer" && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) {
188       for (let i = 0; i < others.length; i++) {
189         let target = others[i].getEventTargetInfo() as ScrollableTargetInfo;
190         if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // Identify the recognizer to work in parallel on the response chain.
191           let panEvent = event as PanGestureEvent;
192           this.childRecognizer.setEnabled(true);
193           this.currentRecognizer.setEnabled(false);
194           if (target.isEnd()) { // Dynamically control the recognizer's enabled state based on the current component state and direction of movement.
195             if (panEvent && panEvent.offsetY < 0) {
196               this.childRecognizer.setEnabled(false);
197               this.currentRecognizer.setEnabled(true);
198             }
199           } else if (target.isBegin()) {
200             if (panEvent.offsetY > 0) {
201               this.childRecognizer.setEnabled(false);
202               this.currentRecognizer.setEnabled(true);
203             }
204           }
205         }
206       }
207     }
208     return GestureJudgeResult.CONTINUE;
209   })
210   ```
211
2123. Set up a gesture listener to listen for the state changes of the **Scroll** component, dynamically adjust the enabled state of the gesture recognizers, and controls whether gesture callbacks are triggered, allowing you to manage scrolling.
213
214   ```ts
215   .parallelGesture ( // Bind a PanGesture as a dynamic controller.
216     PanGesture()
217       .onActionUpdate((event: GestureEvent)=>{
218         if (this.childRecognizer.getState() != GestureRecognizerState.SUCCESSFUL || this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // If neither recognizer is in the SUCCESSFUL state, no control is applied.
219           return;
220         }
221         let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo;
222         let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo;
223         if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) {
224           this.childRecognizer.setEnabled(true);
225           this.currentRecognizer.setEnabled(false);
226           if (target.isEnd()) { // Adjust the enabled state of the gesture recognizers based on the current component state during movement.
227             if ((event.offsetY - this.lastOffset) < 0) {
228               this.childRecognizer.setEnabled(false);
229               if (currentTarget.isEnd()) {
230                 this.currentRecognizer.setEnabled(false);
231               } else {
232                 this.currentRecognizer.setEnabled(true);
233               }
234             }
235           } else if (target.isBegin()) {
236             if ((event.offsetY - this.lastOffset) > 0) {
237               this.childRecognizer.setEnabled(false);
238               if (currentTarget.isBegin()) {
239                 this.currentRecognizer.setEnabled(false);
240               } else {
241                 this.currentRecognizer.setEnabled(true);
242               }
243             }
244           }
245         }
246         this.lastOffset = event.offsetY
247     })
248   )
249   ```
250
2514. Below is the complete code example.
252
253   ```ts
254   // xxx.ets
255   @Entry
256   @Component
257   struct FatherControlChild {
258     scroller: Scroller = new Scroller();
259     scroller2: Scroller = new Scroller();
260     private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
261     private childRecognizer: GestureRecognizer = new GestureRecognizer();
262     private currentRecognizer: GestureRecognizer = new GestureRecognizer();
263     private lastOffset: number = 0;
264
265     build() {
266       Stack({ alignContent: Alignment.TopStart }) {
267         Scroll(this.scroller) { // Outer scroll container.
268           Column() {
269             Text("Scroll Area")
270               .width('90%')
271               .height(150)
272               .backgroundColor(0xFFFFFF)
273               .borderRadius(15)
274               .fontSize(16)
275               .textAlign(TextAlign.Center)
276               .margin({ top: 10 })
277             Scroll(this.scroller2) { // Inner scroll container.
278               Column() {
279                 Text("Scroll Area2")
280                   .width('90%')
281                   .height(150)
282                   .backgroundColor(0xFFFFFF)
283                   .borderRadius(15)
284                   .fontSize(16)
285                   .textAlign(TextAlign.Center)
286                   .margin({ top: 10 })
287                 Column() {
288                   ForEach(this.arr, (item: number) => {
289                     Text(item.toString())
290                       .width('90%')
291                       .height(150)
292                       .backgroundColor(0xFFFFFF)
293                       .borderRadius(15)
294                       .fontSize(16)
295                       .textAlign(TextAlign.Center)
296                       .margin({ top: 10 })
297                   }, (item: string) => item)
298                 }.width('100%')
299               }
300             }
301             .id("inner")
302             .width('100%')
303             .height(800)
304           }.width('100%')
305         }
306         .id("outer")
307         .height(600)
308         .scrollable(ScrollDirection.Vertical) // The scrollbar scrolls in the vertical direction.
309         .scrollBar(BarState.On) // The scrollbar is always displayed.
310         .scrollBarColor(Color.Gray) // The scrollbar color is gray.
311         .scrollBarWidth(10) // The scrollbar width is 10.
312         .edgeEffect(EdgeEffect.None)
313         .shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => {
314           for (let i = 0; i < others.length; i++) {
315             let target = others[i].getEventTargetInfo();
316             if (target.getId() == "inner" && others[i].isBuiltIn() &&
317               others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // Identify the recognizer that to be bound to parallelGesture.
318               this.currentRecognizer = current; // Save the recognizer of the current component.
319               this.childRecognizer = others[i]; // Save the recognizer to work in parallel.
320               return others[i]; // Return the recognizer to work in parallel with the current one.
321             }
322           }
323           return undefined;
324         })
325         .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer,
326           others: Array<GestureRecognizer>) => { // When the implementation is about to succeed, set the recognizer enabling state based on the current component state.
327           let target = current.getEventTargetInfo();
328           if (target && target.getId() == "outer" && current.isBuiltIn() &&
329             current.getType() == GestureControl.GestureType.PAN_GESTURE) {
330             for (let i = 0; i < others.length; i++) {
331               let target = others[i].getEventTargetInfo() as ScrollableTargetInfo;
332               if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // Identify the recognizer to work in parallel on the response chain.
333                 let panEvent = event as PanGestureEvent;
334                 this.childRecognizer.setEnabled(true);
335                 this.currentRecognizer.setEnabled(false);
336                 if (target.isEnd()) { // Dynamically control the recognizer's enabled state based on the current component state and direction of movement.
337                   if (panEvent && panEvent.offsetY < 0) {
338                     this.childRecognizer.setEnabled(false);
339                     this.currentRecognizer.setEnabled(true);
340                   }
341                 } else if (target.isBegin()) {
342                   if (panEvent.offsetY > 0) {
343                     this.childRecognizer.setEnabled(false);
344                     this.currentRecognizer.setEnabled(true);
345                   }
346                 }
347               }
348             }
349           }
350           return GestureJudgeResult.CONTINUE;
351         })
352         .parallelGesture ( // Bind a PanGesture as a dynamic controller.
353           PanGesture()
354             .onActionUpdate((event: GestureEvent) => {
355               if (this.childRecognizer?.getState() != GestureRecognizerState.SUCCESSFUL ||
356                 this.currentRecognizer?.getState() != GestureRecognizerState.SUCCESSFUL) { // If the recognizer is not in the SUCCESSFUL state, no control is applied.
357                 return;
358               }
359               let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo;
360               let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo;
361               if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) {
362                 this.childRecognizer.setEnabled(true);
363                 this.currentRecognizer.setEnabled(false);
364                 if (target.isEnd()) { // Adjust the enabled state of the gesture recognizers based on the current component state during movement.
365                   if ((event.offsetY - this.lastOffset) < 0) {
366                     this.childRecognizer.setEnabled(false);
367                     if (currentTarget.isEnd()) {
368                       this.currentRecognizer.setEnabled(false);
369                     } else {
370                       this.currentRecognizer.setEnabled(true);
371                     }
372                   }
373                 } else if (target.isBegin()) {
374                   if ((event.offsetY - this.lastOffset) > 0) {
375                     this.childRecognizer.setEnabled(false)
376                     if (currentTarget.isBegin()) {
377                       this.currentRecognizer.setEnabled(false);
378                     } else {
379                       this.currentRecognizer.setEnabled(true);
380                     }
381                   }
382                 }
383               }
384               this.lastOffset = event.offsetY;
385             })
386         )
387       }.width('100%').height('100%').backgroundColor(0xDCDCDC)
388     }
389   }
390   ```
391
392## Gesture Recognition Prevention
393
394Gesture recognition is based on the response chain results of [hit testing](./arkts-interaction-basic-principles.md#hit-testing). Therefore, it is efficient to dynamically intervene in gesture processing by controlling the participation status of gesture recognizers in the response chain when the user presses down.
395
396This is achieved using the [onTouchTestDone](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ontouchtestdone20) API:
397
398After hit testing is completed, the system returns all gesture recognizer objects through this API. Applications can filter recognizers by type, component ID, or associated component, and proactively disable specific recognizers by calling the [preventBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#preventbegin20) API.
399
400Disabling by gesture type:
401
402```typescript
403  .onTouchTestDone((event, recognizers) => {
404    for (let i = 0; i < recognizers.length; i++) {
405      let recognizer = recognizers[i];
406      // Disable all pan gestures based on type.
407      if (recognizer.getType() == GestureControl.GestureType.PAN_GESTURE) {
408        recognizer.preventBegin();
409      }
410    }
411  })
412```
413
414Disabling by associated component:
415
416Components must be pre-configured with a component ID through the universal attribute [id](../reference/apis-arkui/arkui-ts/ts-universal-attributes-component-id.md#id).
417
418```typescript
419  .onTouchTestDone((event, recognizers) => {
420    for (let i = 0; i < recognizers.length; i++) {
421      let recognizer = recognizers[i];
422      // Disable all gestures on the component with ID "myID."
423      if (recognizer.getEventTargetInfo().getId() == "myID") {
424        recognizer.preventBegin();
425      }
426    }
427  })
428```
429
430Disabling system built-in gestures:
431
432```typescript
433  .onTouchTestDone((event, recognizers) => {
434    for (let i = 0; i < recognizers.length; i++) {
435      let recognizer = recognizers[i];
436      //  Disable all system built-in gestures.
437      if (recognizer.isBuiltIn()) {
438        recognizer.preventBegin();
439      }
440    }
441  })
442```
443
444Combine the preceding conditions based on specific scenarios.
445
446> **NOTE**
447>
448> The system executes **onTouchTestDone** callbacks on nodes from innermost to outermost.
449
450In the NDK, the corresponding APIs for **onTouchTestDone** and **preventBegin** are **OH_ArkUI_SetTouchTestDoneCallback** and **OH_ArkUI_PreventGestureRecognizerBegin**, respectively. Their usage and functionality are consistent with the ArkTS APIs.
451
452The following example illustrates a simplified video player UI interaction.
453
454The parent container (**video_layer**) has multiple bound gestures:
455- Tap: controls the playback (pause/play).
456- Double-tap: switches between full-screen and non-full-screen modes.
457- Long press: fast-forwards.
458- Vertical swipe: adjusts brightness.
459- Horizontal swipe: seeks playback.
460
461The inner **Slider** component (**progress_layer**) does not have a long-press gesture bound to it. This causes the parent container's fast-forward gesture to be triggered when the user long-presses the **Slider** component, which is unexpected behavior.
462
463Solution: Register an **onTouchTestDone** callback on the **Slider** component. Use this callback to disable gesture recognizers not belonging to the **Slider** component, thereby resolving the conflict.
464
465The following shows the complete sample code:
466
467```typescript
468@Entry
469@ComponentV2
470struct Index {
471  @Local progress: number = 496000 // Initial progress, in seconds.
472  @Local total: number = 27490000   // Total duration, in seconds.
473  @Local currentWidth: string = '100%'
474  @Local currentHeight: string = '100%'
475  private currentPosX: number = 0
476  private currentPosY: number = 0
477  private currentFullScreenState: boolean = true
478  private normalPlayTimer: number = -1;
479  private isPlaying: boolean = true;
480  private fastForwardTimer: number = -1;
481
482  aboutToAppear(): void {
483    // Start a periodic timer to refresh the progress every second.
484    this.startNormalPlayTimer()
485  }
486
487  startNormalPlayTimer(): void {
488    if (this.normalPlayTimer != -1) {
489      this.stopNormalPlayTimer()
490    }
491    this.normalPlayTimer = setInterval(() => {
492      this.progress = this.progress + 1000
493    }, 1000)
494  }
495
496  stopNormalPlayTimer(): void {
497    if (this.normalPlayTimer == -1) {
498      return
499    }
500    clearInterval(this.normalPlayTimer)
501    this.normalPlayTimer = -1
502  }
503
504  startFastForwardTimer(): void {
505    if (this.fastForwardTimer != -1) {
506      this.stopFastForwardTimer()
507    }
508    this.fastForwardTimer = setInterval(() => {
509      this.progress = this.progress + 100000
510    }, 100)
511  }
512
513  stopFastForwardTimer(): void {
514    if (this.fastForwardTimer == -1) {
515      return
516    }
517    clearInterval(this.fastForwardTimer)
518    this.fastForwardTimer = -1
519  }
520
521  showMessage(message: string): void {
522    this.getUIContext().getPromptAction().showToast({ message: message, alignment: Alignment.Center })
523  }
524
525  resetPosInfo(): void {
526    this.currentPosX = 0
527    this.currentPosY = 0
528  }
529
530  toggleFullScreenState(): void {
531    this.currentFullScreenState = !this.currentFullScreenState
532    if (this.currentFullScreenState) {
533      this.currentWidth = '100%'
534      this.currentHeight = '100%'
535    } else {
536      this.currentWidth = '100%'
537      this.currentHeight = '50%'
538    }
539    this.showMessage(this.currentFullScreenState ? 'Full-screen playback' : 'Exit full-screen')
540  }
541
542  togglePlayAndPause(): void {
543    this.isPlaying = !this.isPlaying
544    if (!this.isPlaying) {
545      this.stopNormalPlayTimer()
546    } else {
547      // Restart the timer.
548      this.startNormalPlayTimer()
549    }
550    this.showMessage(this.isPlaying ? 'Pause playback' : 'Resume playback')
551  }
552
553  doFastForward(start: boolean): void {
554    if (!start) { // Stop fast-forwarding and resume normal playback.
555      this.stopFastForwardTimer()
556      this.startNormalPlayTimer()
557      this.showMessage('Cancel fast-forwarding')
558      return
559    }
560
561    this.stopNormalPlayTimer()
562    this.startFastForwardTimer()
563    this.showMessage('Start fast-forwarding')
564  }
565
566  updateBrightness(start: boolean, event: BaseGestureEvent): void {
567    let newY = event.fingerList[0].localY
568    if (start) {
569      this.currentPosY = newY
570      this.showMessage('Start adjusting brightness')
571      return
572    }
573    let offsetY = newY - this.currentPosY;
574    if (offsetY > 10) {
575      this.showMessage((offsetY > 0) ? 'Decrease brightness' : 'Increase brightness')
576    }
577    this.currentPosY = newY
578  }
579
580  updateProgress(start: boolean, event: BaseGestureEvent): void {
581    let newX = event.fingerList[0].localX
582    if (start) {
583      this.currentPosX = newX
584      this.showMessage('Start adjusting progress')
585      return
586    }
587    let offsetX = newX - this.currentPosX;
588    this.progress = Math.floor(this.progress + offsetX * 10000)
589    this.currentPosX = newX
590  }
591
592  build() {
593    Stack({ alignContent: Alignment.Center }) {
594      Column() {
595        Column() {
596          Text("Playback progress: " + this.progress)
597        }
598          .width("100%").height("90%")
599        Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
600          Slider({
601            value: this.progress,
602            min: 0,
603            max: this.total,
604            style: SliderStyle.OutSet
605          })
606            .onChange((value: number, mode: SliderChangeMode) => {
607              this.progress = value
608            })
609            .id("progress_layer")
610            .onTouchTestDone((event, allRecognizers: Array<GestureRecognizer>) => {
611              for (let i = 0; i < allRecognizers.length; i++) {
612                let recognizer = allRecognizers[i];
613                let inspectorInfo = recognizer.getEventTargetInfo().getId();
614                if (inspectorInfo !== "progress_layer") {
615                  // When the user interacts with the progress bar area, disable all gestures not belonging to progress_layer.
616                  recognizer.preventBegin();
617                }
618              }
619            })
620            .margin({ left: 5 })
621            .trackColor(Color.Red)
622            .blockColor(Color.Yellow)
623            .selectedColor(Color.Orange)
624            .trackThickness(2)
625            .flexShrink(1)
626            .flexGrow(1)
627        }
628        .flexGrow(1)
629        .flexShrink(1)
630        .id('id_progress_view')
631      }
632    }
633    .id('video_layer')
634    .backgroundColor('#E0E0E0')
635    .gesture(
636      GestureGroup(GestureMode.Exclusive,
637        PanGesture({ direction: PanDirection.Vertical, distance: 10 })
638          .tag('pan_for_brightness_control')
639          .onActionStart((event) => {
640            this.updateBrightness(true, event)
641          })
642          .onActionUpdate((event) => {
643            this.updateBrightness(false, event)
644          }),
645        PanGesture({ direction: PanDirection.Horizontal, distance: 10 })
646          .tag('pan_for_play_progress_control')
647          .onActionStart((event) => {
648            this.updateProgress(true, event)
649          })
650          .onActionUpdate((event) => {
651            this.updateProgress(false, event)
652          }),
653
654        LongPressGesture()
655          .tag('long_press_for_fast_forward_control')
656          .onAction(() => {
657            this.doFastForward(true) // Start fast-forwarding.
658          })
659          .onActionEnd(() => {
660            this.doFastForward(false) // Stop fast-forwarding.
661          })
662          .onActionCancel(() => {
663            this.doFastForward(false)
664          }),
665
666        TapGesture({ count: 2 })
667          .tag('double_tap_on_video')
668          .onAction(() => {
669            this.toggleFullScreenState()
670          }),
671
672        TapGesture()
673          .tag('single_tap_on_video')
674          .onAction(() => {
675            this.togglePlayAndPause()
676          })
677      )
678    )
679    .width(this.currentWidth)
680    .height(this.currentHeight)
681  }
682}
683```
684<!--no_check-->