如何处理PanGesture与其他手势冲突问题
=======================

#### 问题现象

同一组件或者父子组件上，绑定了PanGesture和其他手势，在进行手势操作时，PanGesture和其他手势发生冲突，导致无法正常完成预期的手势交互，如何处理。

#### 背景知识

*   手势处理支持多种手势类型，包括PanGesture、SwipeGesture、PinchGesture等手势。详细参考：[基础手势](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/basic-gestures)。
*   [PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-gesture-events-single-gesture#滑动手势pangesture)：滑动手势用于触发滑动手势事件，滑动达到最小滑动距离时滑动手势识别成功，参数定义参考[PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture)。
*   [手势事件冲突解决方案](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-gestures-practice)：在复杂的应用界面中，多个组件嵌套时同时绑定手势事件，或者同一个组件同时绑定多个手势，都有可能导致手势事件产生冲突，达不到用户的预期效果。
*   可以通过设置手势的GestureMask手势屏蔽、GesturePriority手势优先级、手势组合模式等方式来解决手势冲突问题。
*   stopPropagation：阻塞事件冒泡。详细参考[TouchEvent对象说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-touch#touchevent对象说明)。

#### 解决方案

在同一组件或者父子组件上绑定了多个手势或者事件，当触发条件存在重叠时，有可能导致手势冲突和误触发，可以考虑通过手势优先级、手势组合或手势判断条件等方式来解决冲突。通常进行如下检查：

1.  检查组件上是否同时绑定了多个可能冲突的手势。
2.  查看手势的触发条件和优先级配置，确认是否存在手势竞争。
3.  测试不同的手势操作，观察是否会意外触发其他手势。

场景一：当在父组件和子组件上同时绑定PanGesture时，希望响应父组件的滑动手势，忽略子组件的滑动手势。

方案：在默认情况下，当父组件和子组件使用gesture绑定同类型的手势时，子组件优先识别通过gesture绑定的手势。当父组件使用priorityGesture绑定与子组件同类型的手势时，父组件优先识别通过priorityGesture绑定的手势。

@Entry
@Component
struct GesturePriorityPage {
  @State offsetX: number \= 0;
  @State offsetY: number \= 0;
  @State swipeText: string \= '等待操作';
  @State startX: number \= 0;
  @State startY: number \= 0;

  build() {
    Column() {
      Text('手势优先级示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 });

      Stack() {
        Text('滑动手势')
          .fontSize(16)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
          .height('100%')
          .width('100%')
          .gesture(
            PanGesture()
              .onActionStart(() \=\> {
                this.startX \= this.offsetX;
                this.startY \= this.offsetY;
              })
              .onActionUpdate((event: GestureEvent) \=\> {
                this.offsetX \= this.startX \+ event.offsetX;
                this.offsetY \= this.startY \+ event.offsetY;
                this.swipeText \= \`子组件拖拽中：X=${this.offsetX.toFixed(0)}，Y=${this.offsetY.toFixed(0)}\`;
              })
              .onActionEnd(() \=\> {
                this.swipeText \= '子组件结束';
              })
          );
      }
      .width(280)
      .height(280)
      .backgroundColor('#2196F3')
      .borderRadius(16)
      .translate({ x: this.offsetX, y: this.offsetY })
      .priorityGesture(
       _ // 滑动手势，设置为高优先级_
        PanGesture()
          .onActionStart(() \=\> {
            this.startX \= this.offsetX;
            this.startY \= this.offsetY;
          })
          .onActionUpdate((event: GestureEvent) \=\> {
            this.offsetX \= this.startX \+ event.offsetX;
            this.offsetY \= this.startY \+ event.offsetY;
            this.swipeText \= \`父组件拖拽中：X=${this.offsetX.toFixed(0)}，Y=${this.offsetY.toFixed(0)}\`;
          })
          .onActionEnd(() \=\> {
            this.swipeText \= '父组件结束';
          })
      );

      Text(this.swipeText)
        .fontSize(18)
        .fontColor('#333333')
        .margin({ top: 30 })
        .padding(15)
        .backgroundColor(Color.White)
        .borderRadius(8);

      Button('重置位置')
        .margin({ top: 20 })
        .onClick(() \=\> {
          this.offsetX \= 0;
          this.offsetY \= 0;
          this.swipeText \= '等待操作';
        });
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5');
  }
}

场景二：当在组件上同时绑定PanGesture和SwipeGesture时，由于都是滑动触发，滑动时可能产生冲突。

方案：使用手势组合模式，通过LongPressGesture和PanGesture实现拖拽的效果，在长按手势识别后才能顺序识别滑动手势。然后单独绑定SwipeGesture，将快滑手势与滑动手势的触发条件进行区分。

@Entry
@Component
struct GestureCombinationPage {
  @State offsetX: number \= 0;
  @State offsetY: number \= 0;
  @State startX: number \= 0;
  @State startY: number \= 0;
  @State statusText: string \= '长按后可拖拽';
  @State swipeText: string \= '等待操作';
  @State bgColor: string \= '#2196F3';
  @State isDragging: boolean \= false;

  build() {
    Column() {
      Text('手势组合示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 });

      Stack() {
        Column() {
          Text(this.statusText)
            .fontSize(16)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center);
        };
      }
      .width(280)
      .height(280)
      .backgroundColor(this.bgColor)
      .borderRadius(16)
      .translate({ x: this.offsetX, y: this.offsetY })
   _   // 使用顺序识别组合，先长按再拖拽_
      .gesture(
        GestureGroup(GestureMode.Sequence,
          LongPressGesture({ duration: 500 })
            .onAction(() \=\> {
              this.isDragging \= true;
              this.bgColor \= '#4CAF50';
              this.statusText \= '可以拖拽了';
            }),
          PanGesture()
            .onActionStart(() \=\> {
              this.startX \= this.offsetX;
              this.startY \= this.offsetY;
            })
            .onActionUpdate((event: GestureEvent) \=\> {
              if (this.isDragging) {
                this.offsetX \= this.startX \+ event.offsetX;
                this.offsetY \= this.startY \+ event.offsetY;
                this.statusText \= \`拖拽：X=${this.offsetX.toFixed(0)}，Y=${this.offsetY.toFixed(0)}\`;
              }
            })
            .onActionEnd(() \=\> {
              this.isDragging \= false;
              this.bgColor \= '#2196F3';
              this.statusText \= '拖拽结束';
              setTimeout(() \=\> {
                this.statusText \= '长按后可拖拽';
              }, 1000);
            })
        ))
    _  // 单独添加快速滑动手势_
      .gesture(
        SwipeGesture({
          direction: SwipeDirection.All,
          speed: 100 _// 设置较高的速度阈值，避免慢速拖拽被识别为滑动_
        })
          .onAction((event: GestureEvent) \=\> {
            let direction \= '';
            if (event.angle \> -45 && event.angle <\= 45) {
              direction \= '向右快速滑动';
            } else if (event.angle \> 45 && event.angle <\= 135) {
              direction \= '向下快速滑动';
            } else if (event.angle \> 135 || event.angle <\= -135) {
              direction \= '向左快速滑动';
            } else {
              direction \= '向上快速滑动';
            }
            this.swipeText \= direction;
          }),
        GestureMask.Normal
      );

      Text(this.swipeText)
        .fontSize(18)
        .fontColor('#333333')
        .margin({ top: 30 })
        .padding(15)
        .backgroundColor(Color.White)
        .borderRadius(8);

      Text('操作说明：')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .alignSelf(ItemAlign.Start)
        .margin({ left: 20, top: 40, bottom: 10 });

      Text('1. 长按0.5秒后可以拖拽移动\\n2\. 快速左右滑动触发快速滑动')
        .fontSize(14)
        .fontColor('#666666')
        .lineHeight(24)
        .padding(15)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .width('90%');

      Button('重置')
        .margin({ top: 20 })
        .onClick(() \=\> {
          this.getUIContext()?.animateTo({ duration: 300 }, () \=\> {
            this.offsetX \= 0;
            this.offsetY \= 0;
          });
          this.statusText \= '长按后可拖拽';
          this.isDragging \= false;
        });
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5');
  }
}

场景三：当业务逻辑中不支持使用手势组合时，直接绑定PanGesture和SwipeGesture会产生冲突，但又希望实现快滑手势的效果。

方案：可以考虑在PanGesture中根据参数变化自定义相关的逻辑。

@Entry
@Component
struct GestureJudgementPage {
  @State offsetX: number \= 0;
  @State offsetY: number \= 0;
  @State statusText: string \= '拖拽或滑动';
  @State startX: number \= 0;
  @State startY: number \= 0;
  @State startTime: number \= 0;

  _// 判断是否为快速滑动_
  isSwipeGesture(distance: number, duration: number): boolean {
    const speed \= distance / duration; _//_ _像素每毫秒_
    return speed \> 0.5; _// 速度阈值_
  }

  build() {
    Column() {
      Text('自定义手势处理逻辑')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 });

      Stack() {
        Text(this.statusText)
          .fontSize(16)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center);
      }
      .width(280)
      .height(280)
      .backgroundColor('#673AB7')
      .borderRadius(16)
      .translate({ x: this.offsetX, y: this.offsetY })
      .gesture(
        PanGesture()
          .onActionStart(() \=\> {
            this.startX \= this.offsetX;
            this.startY \= this.offsetY;
            this.startTime \= Date.now();
            this.statusText \= '开始触摸';
          })
          .onActionUpdate((event: GestureEvent) \=\> {
            this.offsetX \= this.startX \+ event.offsetX;
            this.offsetY \= this.startY \+ event.offsetY;
            this.statusText \= \`移动中：${event.offsetX.toFixed(0)}，${event.offsetY.toFixed(0)}\`;
          })
          .onActionEnd((event: GestureEvent) \=\> {
            const duration \= Date.now() \- this.startTime;
            const distance \= Math.sqrt(
              event.offsetX \* event.offsetX +
                event.offsetY \* event.offsetY
            );

          _  // 根据速度判断是滑动还是拖拽_
            if (this.isSwipeGesture(distance, duration)) {
             _ // 快速滑动，执行滑动操作_
              this.statusText \= '快速滑动触发';
              const angle \= Math.atan2(event.offsetY, event.offsetX) \* 180 / Math.PI;

              if (Math.abs(angle) < 45) {
             _   // 向右滑动，回到原位_
                this.getUIContext()?.animateTo({ duration: 300 }, () \=\> {
                  this.offsetX \= this.startX;
                  this.offsetY \= this.startY;
                });
                this.statusText \= '右滑取消';
              } else if (Math.abs(angle) \> 135) {
               _ // 向左滑动，归档_
                this.getUIContext()?.animateTo({ duration: 300 }, () \=\> {
                  this.offsetX \= this.startX \- 400;
                  this.offsetY \= this.startY;
                });
                this.statusText \= '左滑删除';
              }
            } else {
             _ // 慢速移动，保持拖拽位置_
              this.statusText \= '拖拽完成';
            }
          })
      );

      Text('操作说明：')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .alignSelf(ItemAlign.Start)
        .margin({ left: 20, top: 40, bottom: 10 });

      Text('1. 慢速移动会保持拖拽位置\\n2\. 快速向右滑动取消操作\\n3\. 快速向左滑动删除内容\\n4\. 系统自动识别手势类型')
        .fontSize(14)
        .fontColor('#666666')
        .lineHeight(24)
        .padding(15)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .width('90%');

      Button('重置位置')
        .margin({ top: 20 })
        .onClick(() \=\> {
          this.getUIContext()?.animateTo({ duration: 300 }, () \=\> {
            this.offsetX \= 0;
            this.offsetY \= 0;
          });
          this.statusText \= '拖拽或滑动';
        });
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5');
  }
}

场景四：PanGesture绑定在父组件上，子组件绑定了onTouch事件，如何在子组件响应onTouch的时候，阻止父组件响应PanGesture手势。

方案：可以考虑在onTouch事件中调用stopPropagation()，阻止事件向上冒泡。

import { JSON } from '@kit.ArkTS';

@Entry
@Component
struct StopPropagationPage {
  build() {
    Row() {
      Column() {
        Text('This is Text')
          .fontSize(50)
          .backgroundColor('#f1f2f3')
          .onTouch((event: TouchEvent) \=\> {
            if (event.type \=== TouchType.Down) {
         _     // 消耗事件，阻止向上冒泡_
              if (event.stopPropagation) {
                event.stopPropagation();
              }
              console.info('Stop: PanGesture action');
            }
          });

      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .gesture(PanGesture().onActionStart((event: GestureEvent) \=\> {
        console.info('Start：PanGesture action');
        console.info(\`Start：envnt=${JSON.stringify(event)}\`);
      }));
    };
  }
}

#### 常见FAQ

Q：父子组件都有手势时如何处理？

A：可以使用GestureMask.IgnoreInternal忽略内部子组件的手势，或使用事件冒泡机制，让子组件先响应手势。详细参考[GestureMask枚举说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-gesture-common#gesturemask枚举说明)。

Q：如何实现双指手势和单指手势的配合？

A：可以使用PinchGesture和PanGesture的并行组合，通过手势识别器自动区分单指和双指操作。

Q：PanGesture和SwipeGesture都可以滑动，这两个滑动有什么区别？

A：PanGesture侧重于滑动距离，滑动达到最小滑动距离时滑动手势识别成功，而[SwipeGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-swipegesture)侧重于滑动的速度，滑动速度大于速度阈值时识别成功。

#### 总结

手势冲突是复杂交互场景中的常见问题，需要通过手势优先级、手势组合、手势判断条件等方式来解决。应该根据具体的业务场景选择合适的解决方案，确保用户能够流畅地完成各种手势操作。同时，要充分测试各种手势组合，避免出现手势死锁或误触发的情况。