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

#### 问题现象

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

#### 背景知识

*   手势处理支持多种手势类型，包括PanGesture、SwipeGesture、PinchGesture等手势。详细参考：基础手势。
*   PanGesture：滑动手势用于触发滑动手势事件，滑动达到最小滑动距离时滑动手势识别成功，参数定义参考PanGesture。
*   手势事件冲突解决方案：在复杂的应用界面中，多个组件嵌套时同时绑定手势事件，或者同一个组件同时绑定多个手势，都有可能导致手势事件产生冲突，达不到用户的预期效果。
*   可以通过设置手势的GestureMask手势屏蔽、GesturePriority手势优先级、手势组合模式等方式来解决手势冲突问题。
*   stopPropagation：阻塞事件冒泡。详细参考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枚举说明。

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

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

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

A：PanGesture侧重于滑动距离，滑动达到最小滑动距离时滑动手势识别成功，而SwipeGesture侧重于滑动的速度，滑动速度大于速度阈值时识别成功。

#### 总结

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