文档管理中心

手势事件冲突常见问题

滚动容器嵌套滚动容器事件冲突

  1. Scroll组件嵌套List组件滑动事件冲突。

    Scroll组件嵌套List组件,子组件List组件的滑动手势优先级高于父组件Scroll的滑动手势,所以当List列表滚动时,不会响应Scroll组件的滚动事件,List不会和Scroll一起滚动。如果需要List和Scroll组件同步滚动可以使用nestedScroll属性来解决,设置向前向后两个方向上的嵌套滚动模式,实现与父组件的滚动联动。

    使用nestedScroll属性设置List组件的嵌套滚动方式,NestedScrollMode设置成SELF_FIRST时,List组件滚动到页面边缘后,父组件继续滚动。NestedScrollMode设置为PARENT_FIRST时,父组件先滚动,滚动至边缘后通知List组件继续滚动。示例代码如下。

    收起
    自动换行
    深色代码主题
    复制
    1. @Entry
    2. @Component
    3. struct GesturesConflictScene1 {
    4. build() {
    5. Scroll() {
    6. Column() {
    7. Column()
    8. .height('30%')
    9. .width('100%')
    10. .backgroundColor(Color.Blue)
    11. List() {
    12. ForEach([1, 2, 3, 4, 5, 6], (item: string) => {
    13. ListItem() {
    14. Text(item.toString())
    15. .height(300)
    16. .fontSize(50)
    17. .fontWeight(FontWeight.Bold)
    18. }
    19. }, (item: number) => item.toString())
    20. }
    21. .edgeEffect(EdgeEffect.None)
    22. .nestedScroll({
    23. scrollForward: NestedScrollMode.PARENT_FIRST,
    24. scrollBackward: NestedScrollMode.SELF_FIRST
    25. })
    26. .height('100%')
    27. .width('100%')
    28. }
    29. }
    30. .height('100%')
    31. .width('100%')
    32. }
    33. }
  2. List、Scroller等滚动容器嵌套Web组件,滑动事件冲突。

    比如List组件嵌套Web组件,当Web加载的网页中也包含滚动视图的时候,这时候上下滚动Web组件,不能和List列表整体一起滑动。这是因为Web的滑动事件和List组件的冲突,如果想让Web随List一起整体滚动,解决方案和前面的例子一样,给Web组件添加nestedScroll属性。

    收起
    自动换行
    深色代码主题
    复制
    1. Web(
    2. // ...
    3. )
    4. .nestedScroll({
    5. scrollForward: NestedScrollMode.PARENT_FIRST,
    6. scrollBackward: NestedScrollMode.SELF_FIRST
    7. })

    具体实现可以参考:Web组件嵌套滚动

使用组合手势同时绑定多个同类型手势冲突

例如给组件同时设置单击和双击的点击手势TapGesture,按如下方式设置会发现双击手势失效,这是因为在互斥识别的组合手势中,手势会按声明的顺序进行识别,若有一个手势识别成功,则结束手势识别。因为单击手势放在了前面,所以当双击的时候会优先识别了单击手势,单击成功后后面的双击回调就不会执行了。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct GesturesConflictScene2 {
  4. @State count1: number = 0;
  5. @State count2: number = 0;
  6. build() {
  7. Column() {
  8. Text('Exclusive gesture\n' + 'Click count is:' + this.count1 + '\nDouble click count is:' + this.count2 + '\n')
  9. .fontSize(28)
  10. }
  11. .height(200)
  12. .width('100%')
  13. .gesture(
  14. GestureGroup(GestureMode.Exclusive,
  15. TapGesture({ count: 1 })
  16. .onAction(() => {
  17. this.count1++;
  18. }),
  19. TapGesture({ count: 2 })
  20. .onAction(() => {
  21. this.count2++;
  22. })
  23. )
  24. )
  25. }
  26. }

可以设置手势为并行识别来解决,设置对应的GestureMode为Parallel。

收起
自动换行
深色代码主题
复制
  1. .gesture(
  2. GestureGroup(GestureMode.Parallel,
  3. TapGesture({ count: 2 })
  4. .onAction(() => {
  5. this.count2++;
  6. }),
  7. TapGesture({ count: 1 })
  8. .onAction(() => {
  9. this.count1++;
  10. })
  11. )
  12. )

系统手势和自定义手势之间冲突

对于一般同类型的手势,系统手势优先于自定义手势执行,可以通过priorityGesture或者parallelGesture的方式来绑定自定义手势,例如下面这个示例。

图片长按手势响应失败或冲突,在Image控件上添加长按手势后,长按图片无法响应对应方法,而是图片放大的动画,示例代码如下。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct GesturesConflictScene3 {
  4. @State message: string = 'Hello World';
  5. build() {
  6. Row() {
  7. Column() {
  8. Text(this.message)
  9. .fontSize(50)
  10. .fontWeight(FontWeight.Bold)
  11. Image($r('app.media.startIcon'))
  12. .margin({ top: 100 })
  13. .width(360)
  14. .height(360)
  15. .gesture(
  16. LongPressGesture({ repeat: true })
  17. .onAction((event: GestureEvent) => {
  18. })
  19. .onActionEnd(() => {
  20. try {
  21. this.getUIContext().getPromptAction().showToast({ message: 'Long Press' });
  22. } catch (err) {
  23. let error = err as BusinessError;
  24. hilog.error(0x0000, 'testTag', `showToast err, code: ${error.code}, mesage: ${error.message}`);
  25. }
  26. })
  27. )
  28. }
  29. .width('100%')
  30. }
  31. .height('100%')
  32. }
  33. }

这是因为Image组件内置的长按动画和用户自定义的长按手势LongPressGesture冲突了。可以使用priorityGesture绑定手势的方式替代gesture的方式,这样就会只响应自定义手势LongPressGesture了。如果需要两者都执行可以使用parallelGesture的绑定方式。

收起
自动换行
深色代码主题
复制
  1. .priorityGesture(
  2. LongPressGesture({ repeat: true })
  3. .onAction((event: GestureEvent) => {
  4. })
  5. .onActionEnd(() => {
  6. try {
  7. this.getUIContext().getPromptAction().showToast({ message: 'Long Press' });
  8. } catch (err) {
  9. let error = err as BusinessError;
  10. hilog.error(0x0000, 'testTag', `showToast err, code: ${error.code}, mesage: ${error.message}`);
  11. }
  12. })
  13. )

手势事件透传

和触摸事件一样,手势事件也可以通过hitTestBehavior属性来进行透传,例如下面这个示例,上层的Column组件设置hitTestBehavior属性为HitTestMode.None后,可以将滑动手势SwipeGesture透传给被覆盖的Column组件。HitTestMode.None:自身不接收事件,但不会阻塞兄弟组件和子组件继续做触摸测试。

收起
自动换行
深色代码主题
复制
  1. import { BusinessError } from '@kit.BasicServicesKit';
  2. import { hilog } from '@kit.PerformanceAnalysisKit';
  3. @Entry
  4. @Component
  5. struct GesturesConflictScene4 {
  6. build() {
  7. Stack() {
  8. Column()
  9. .width('100%')
  10. .height('100%')
  11. .backgroundColor(Color.Black)
  12. .gesture(
  13. SwipeGesture({ direction: SwipeDirection.Horizontal })
  14. .onAction((event) => {
  15. if (event) {
  16. try {
  17. this.getUIContext().getPromptAction().showToast({ message: 'SwipeGesture' });
  18. } catch (err) {
  19. let error = err as BusinessError;
  20. hilog.error(0x0000, 'testTag', `showToast err, code: ${error.code}, mesage: ${error.message}`);
  21. }
  22. }
  23. })
  24. )
  25. Column()
  26. .width(300)
  27. .height(100)
  28. .backgroundColor(Color.Red)
  29. .hitTestBehavior(HitTestMode.None)
  30. }
  31. .width(300)
  32. .height(300)
  33. }
  34. }

多点触控场景下手势冲突

当一个页面中有多个组件可以响应手势事件,在多个手指触控的情况下,多个组件可能会同时响应手势事件,从而导致业务异常。ArkUI提供了手势独占的属性monopolizeEvents,设置需要单独响应事件的组件的monopolizeEvents属性为true,可以解决这一问题。

例如下面这个示例,给按钮Button1设置了.monopolizeEvents(true)之后,当手指首先触摸在Button1之后,在手指离开之前,其它组件的手势和事件都不会触发。

收起
自动换行
深色代码主题
复制
  1. import { hilog } from '@kit.PerformanceAnalysisKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. @Entry
  4. @Component
  5. struct GesturesConflictScene5 {
  6. @State message: string = 'Hello World';
  7. build() {
  8. Column() {
  9. Row({ space: 20 }) {
  10. Button('Button1')
  11. .width(100)
  12. .height(40)
  13. .monopolizeEvents(true)
  14. Button('Button2')
  15. .width(200)
  16. .height(50)
  17. .onClick(() => {
  18. try {
  19. this.getUIContext().getPromptAction().showToast({ message: 'GesturesConflictScene5 Button2 click' });
  20. } catch (err) {
  21. let error = err as BusinessError;
  22. hilog.error(0x0000, 'testTag', `showToast err, code: ${error.code}, mesage: ${error.message}`);
  23. }
  24. })
  25. }
  26. .margin(20)
  27. Text(this.message)
  28. .margin(15)
  29. }
  30. .width('100%')
  31. .gesture(
  32. TapGesture({ count: 1 })
  33. .onAction(() => {
  34. console.info('GesturesConflictScene5 TapGesture onAction.');
  35. }),
  36. )
  37. }
  38. }

动态控制自定义手势是否响应

在手势识别期间,开发者决定是否响应手势,例如下面的示例代码,通过onGestureJudgeBegin回调方法在手势识别期间进行判定,当手势为GestureType.DRAG的时候,不响应该手势,所以会使定义的onDragStart事件失效。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct GesturesConflictScene6 {
  4. @State message: string = 'Hello World';
  5. build() {
  6. Column()
  7. .width('100%')
  8. .height(200)
  9. .backgroundColor(Color.Brown)
  10. .onDragStart(() => {
  11. console.info('GesturesConflictScene6 Drag start.');
  12. })
  13. .gesture(
  14. TapGesture({ count: 1 })
  15. .tag('tap1')
  16. .onAction(() => {
  17. console.info('GesturesConflictScene6 TapGesture onAction.');
  18. }),
  19. )
  20. .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {
  21. if (gestureInfo.type === GestureControl.GestureType.LONG_PRESS_GESTURE) {
  22. let longPressEvent = event as LongPressGestureEvent;
  23. console.info('GesturesConflictScene6: ' + longPressEvent.repeat);
  24. }
  25. if (gestureInfo.type === GestureControl.GestureType.DRAG) {
  26. return GestureJudgeResult.REJECT;
  27. } else if (gestureInfo.tag === 'tap1' && event.pressure > 10) {
  28. return GestureJudgeResult.CONTINUE
  29. }
  30. return GestureJudgeResult.CONTINUE;
  31. })
  32. }
  33. }

父组件如何管理子组件手势

父子组件嵌套滚动发生手势冲突,父组件有机制可以干预子组件的手势响应。下面例子介绍了如何使用手势拦截增强,在外层Scroll组件的shouldBuiltInRecognizerParallelWithonGestureRecognizerJudgeBegin回调中,动态控制内外层Scroll手势事件的滚动。

  1. 首先在父组件Scroll的shouldBuiltInRecognizerParallelWith方法中收集需做并行处理的手势。下面示例代码中收集到了子组件的手势识别器childRecognizer,使其和父组件的手势识别器currentRecognizer并行处理。

  2. 调用onGestureRecognizerJudgeBegin方法,判断滚动组件是否滑动到顶部或者底部,做业务逻辑处理,通过动态控制手势识别器是否可用,来决定并行处理器的childRecognizer和currentRecognizer是否可用。

    收起
    自动换行
    深色代码主题
    复制
    1. @Entry
    2. @Component
    3. struct GesturesConflictScene7 {
    4. scroller: Scroller = new Scroller();
    5. scroller2: Scroller = new Scroller();
    6. private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    7. private childRecognizer: GestureRecognizer = new GestureRecognizer();
    8. private currentRecognizer: GestureRecognizer = new GestureRecognizer();
    9. build() {
    10. Stack({ alignContent: Alignment.TopStart }) {
    11. Scroll(this.scroller) {
    12. Column() {
    13. Text('Scroll Area')
    14. .width('100%')
    15. .height(150)
    16. .backgroundColor(0xFFFFFF)
    17. .borderRadius(15)
    18. .fontSize(16)
    19. .textAlign(TextAlign.Center)
    20. .margin({ top: 10 })
    21. Scroll(this.scroller2) {
    22. Column() {
    23. Text('Scroll Area2')
    24. .width('100%')
    25. .height(150)
    26. .backgroundColor(0xFFFFFF)
    27. .borderRadius(15)
    28. .fontSize(16)
    29. .textAlign(TextAlign.Center)
    30. .margin({ top: 10 })
    31. Column() {
    32. ForEach(this.arr, (item: number) => {
    33. Text(item.toString())
    34. .width('100%')
    35. .height(200)
    36. .backgroundColor(0xFFFFFF)
    37. .borderRadius(15)
    38. .fontSize(20)
    39. .textAlign(TextAlign.Center)
    40. .margin({ top: 10 })
    41. }, (item: string) => item)
    42. }
    43. .width('100%')
    44. }
    45. }
    46. .id('innerScroll')
    47. .scrollBar(BarState.Off)
    48. .width('100%')
    49. .height(800)
    50. }.width('100%')
    51. }
    52. .id('outerScroll')
    53. .height(600)
    54. .scrollBar(BarState.Off)
    55. .shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => {
    56. for (let i = 0; i < others.length; i++) {
    57. let target = others[i].getEventTargetInfo();
    58. if (target) {
    59. if (target.getId() === 'innerScroll' && others[i].isBuiltIn() &&
    60. others[i].getType() === GestureControl.GestureType.PAN_GESTURE) {
    61. this.currentRecognizer = current;
    62. this.childRecognizer = others[i];
    63. return others[i];
    64. }
    65. }
    66. }
    67. return undefined;
    68. })
    69. .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer,
    70. others: Array<GestureRecognizer>) => {
    71. if (current) {
    72. let target = current.getEventTargetInfo();
    73. if (target) {
    74. if (target.getId() === 'outerScroll' && current.isBuiltIn() &&
    75. current.getType() === GestureControl.GestureType.PAN_GESTURE) {
    76. if (others) {
    77. for (let i = 0; i < others.length; i++) {
    78. let target = others[i].getEventTargetInfo() as ScrollableTargetInfo;
    79. if (target instanceof ScrollableTargetInfo && target.getId() == 'innerScroll') {
    80. let panEvent = event as PanGestureEvent;
    81. if (target.isEnd()) {
    82. if (panEvent && panEvent.offsetY < 0) {
    83. this.childRecognizer.setEnabled(false)
    84. this.currentRecognizer.setEnabled(true)
    85. } else {
    86. this.childRecognizer.setEnabled(true)
    87. this.currentRecognizer.setEnabled(false)
    88. }
    89. } else if (target.isBegin()) {
    90. if (panEvent.offsetY > 0) {
    91. this.childRecognizer.setEnabled(false)
    92. this.currentRecognizer.setEnabled(true)
    93. } else {
    94. this.childRecognizer.setEnabled(true)
    95. this.currentRecognizer.setEnabled(false)
    96. }
    97. } else {
    98. this.childRecognizer.setEnabled(true)
    99. this.currentRecognizer.setEnabled(false)
    100. }
    101. }
    102. }
    103. }
    104. }
    105. }
    106. }
    107. return GestureJudgeResult.CONTINUE;
    108. })
    109. }
    110. .width('100%')
    111. .height('100%')
    112. .backgroundColor(0xF1F3F5)
    113. .padding(12)
    114. }
    115. }
在 指南 中进行搜索
请输入您想要搜索的关键词