HarmonyOS 鸿蒙Next中通过细粒度手势控制解决 Swiper 与 Scroll 内嵌时的手势冲突问题

HarmonyOS 鸿蒙Next中通过细粒度手势控制解决 Swiper 与 Scroll 内嵌时的手势冲突问题 在开发信息流、商品详情、个人主页等一些相对复杂的页面时,我们开发经常使用 Swiper 轮播项中嵌入可纵向滚动的内容(如评论区、商品参数)来。但有时候Swiper 会抢占所有滑动手势,导致内部 List 无法滚动,这时候就很影响用户体验了。那么,如何解决呢?

6 回复

【背景知识】

在HarmonyOS开发中,触摸事件(onTouch事件)是用户与设备交互的基础,也是所有手势事件组成的基础,手势均由触摸事件组成。当组件被父布局(Stack、Row等任何容器组件)封装住时,对于父、子组件设置的响应手势极容易发生冲突。

【解决方案】

首先添加触摸监听,记录起点与移动量,然后根据滑动方向动态开关 Swiper。

@Entry
@Component
struct SwiperScrollConflictSolution {
  private swiperController: SwiperController = new SwiperController();
  @State swiperEnabled: boolean = true; // 控制 Swiper 是否响应滑动
  private touchStartX: number = 0;
  private touchStartY: number = 0;
  @State array: string[] = ['1', '2', '3', '4', '5', '6'];

  // 判断是否为横向主导滑动
  private isHorizontalMove(offsetX: number, offsetY: number): boolean {
    const absX = Math.abs(offsetX);
    const absY = Math.abs(offsetY);
    // 横向位移明显大于纵向(阈值可调)
    return absX > absY && absX > 10; // 10px 防抖
  }

  build() {
    Column() {
      Text('Swiper + List 手势冲突解决方案')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 });

      // 外层 Swiper
      Swiper(this.swiperController) {
        ForEach([1, 2, 3], (index: number) => {
          Column() {
            Text(`轮播页 ${index}`)
              .fontSize(20)
              .fontColor(Color.White)
              .padding(10)
              .backgroundColor('#4A90E2')
              .width('100%');

            // 内嵌可滚动列表
            List({ space: 10 }) {
              ForEach(this.array, (item: number) => {
                ListItem() {
                  Text(`列表项 ${item} - 页面 ${index}`)
                    .height(60)
                    .fontSize(16)
                    .textAlign(TextAlign.Center)
                    .backgroundColor('#F0F0F0')
                    .borderRadius(8);
                };
              }, (item: number) => item.toString());
            }
            .width('100%')
            .height(400);
          }
          .width('100%')
          .height(500)
          // 关键:在此 Column 上监听触摸
          .onTouch((event: TouchEvent) => {
            switch (event.type) {
              case TouchType.Down:
                this.touchStartX = event.touches[0].screenX;
                this.touchStartY = event.touches[0].screenY;
                break;
              case TouchType.Move:
                if (this.touchStartX !== 0 && this.touchStartY !== 0) {
                  const offsetX = event.touches[0].screenX - this.touchStartX;
                  const offsetY = event.touches[0].screenY - this.touchStartY;
                  // 动态启用/禁用 Swiper
                  this.swiperEnabled = this.isHorizontalMove(offsetX, offsetY);
                }
                break;
              case TouchType.Up:
              case TouchType.Cancel:
                // 重置状态
                this.touchStartX = 0;
                this.touchStartY = 0;
                this.swiperEnabled = true; // 恢复默认可滑动
                break;
            }
          });
        }, (index: number) => index.toString());
      }
      .enabled(this.swiperEnabled) // 核心控制点
      .loop(true)
      .duration(300)
      .width('100%')
      .height(500)
      .margin({ bottom: 20 });

      Text('操作提示:\n• 水平滑动 → 切换轮播页\n• 垂直滑动 → 滚动列表内容')
        .fontSize(14)
        .fontColor(Color.Gray)
        .textAlign(TextAlign.Center)
        .width('90%');
    }
    .width('100%')
    .height('100%')
    .padding(10);
  }
}

更多关于HarmonyOS 鸿蒙Next中通过细粒度手势控制解决 Swiper 与 Scroll 内嵌时的手势冲突问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


设计思路

通过HarmonyOS开发文档我们可以看到,其为我们提供了细粒度的手势控制能力。我们通过以下策略解决冲突:

1、监听触摸事件(onTouch),记录起始点与移动轨迹; 2、计算滑动角度,判断主方向是横向还是纵向; 3、动态设置 Swiper 的 enabled 属性:仅当判定为横向滑动时启用轮播; 4、利用 gesturePriority 明确手势优先级。

实现步骤

分为4步来实现:

步骤 1:创建基础 UI 结构(Swiper 内嵌可滚动内容) 步骤 2:添加触摸监听,记录起点与移动量 步骤 3:根据滑动方向动态开关 Swiper 步骤 4:优化体验(防抖、边界处理)

完整实现

// entry/src/main/ets/pages/Index.ets
import { SwiperController } from '@ohos/arkui';

@Entry
@Component
struct SwiperScrollConflictSolution {
  private swiperController: SwiperController = new SwiperController();
  @State swiperEnabled: boolean = true; // 控制 Swiper 是否响应滑动
  private touchStartX: number = 0;
  private touchStartY: number = 0;

  // 判断是否为横向主导滑动
  private isHorizontalMove(offsetX: number, offsetY: number): boolean {
    const absX = Math.abs(offsetX);
    const absY = Math.abs(offsetY);
    // 横向位移明显大于纵向(阈值可调)
    return absX > absY && absX > 10; // 10px 防抖
  }

  build() {
    Column() {
      Text('Swiper + List 手势冲突解决方案')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 })

      // 外层 Swiper
      Swiper(this.swiperController) {
        ForEach([1, 2, 3], (index: number) => {
          Column() {
            Text(`轮播页 ${index}`)
              .fontSize(20)
              .fontColor(Color.White)
              .padding(10)
              .backgroundColor('#4A90E2')
              .width('100%')

            // 内嵌可滚动列表
            List({ space: 10 }) {
              ForEach(Array.from({ length: 20 }, (_, i) => i + 1), (item: number) => {
                ListItem() {
                  Text(`列表项 ${item} - 页面 ${index}`)
                    .height(60)
                    .fontSize(16)
                    .textAlign(TextAlign.Center)
                    .backgroundColor('#F0F0F0')
                    .borderRadius(8)
                }
              }, (item: number) => item.toString())
            }
            .layoutPattern(ListLayoutPattern.LINEAR)
            .width('100%')
            .height(400)
          }
          .width('100%')
          .height(500)
          // 关键:在此 Column 上监听触摸
          .onTouch((event: TouchEvent) => {
            switch (event.type) {
              case TouchType.Down:
                this.touchStartX = event.touches[0].screenX;
                this.touchStartY = event.touches[0].screenY;
                break;
              case TouchType.Move:
                if (this.touchStartX !== 0 && this.touchStartY !== 0) {
                  const offsetX = event.touches[0].screenX - this.touchStartX;
                  const offsetY = event.touches[0].screenY - this.touchStartY;
                  // 动态启用/禁用 Swiper
                  this.swiperEnabled = this.isHorizontalMove(offsetX, offsetY);
                }
                break;
              case TouchType.Up:
              case TouchType.Cancel:
                // 重置状态
                this.touchStartX = 0;
                this.touchStartY = 0;
                this.swiperEnabled = true; // 恢复默认可滑动
                break;
            }
          })
        }, (index: number) => index.toString())
      }
      .enabled(this.swiperEnabled) // 核心控制点
      .loop(true)
      .duration(300)
      .width('100%')
      .height(500)
      .margin({ bottom: 20 })

      Text('操作提示:\n• 水平滑动 → 切换轮播页\n• 垂直滑动 → 滚动列表内容')
        .fontSize(14)
        .fontColor(Color.Gray)
        .textAlign(TextAlign.Center)
        .width('90%')
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

通过  touch-action  CSS 属性限制 Swiper 手势

在HarmonyOS鸿蒙Next中,通过细粒度手势控制可解决Swiper与Scroll嵌套时的手势冲突。使用PanGesture和PinchGesture等独立手势识别器,分别绑定到Swiper和Scroll组件。通过设置不同的手势响应优先级和方向约束,如限制Swiper响应水平滑动、Scroll响应垂直滑动,实现手势隔离。利用onGestureJudgeBegin回调动态判断手势归属,避免同时触发。该方法直接通过ArkTS声明式UI配置,无需依赖底层事件处理。

在HarmonyOS Next中,可以通过细粒度手势控制解决Swiper与Scroll嵌套时的手势冲突。具体实现如下:

  1. 使用GestureGroupPanGesture分别定义水平和垂直方向的手势识别器
  2. 通过GestureMask设置手势互斥关系,避免同时响应
  3. 在Swiper组件中设置.gesture()修饰符,指定水平滑动手势
  4. 在Scroll组件中设置.gesture()修饰符,指定垂直滑动手势

示例代码:

// 定义手势
const horizontalPan = new PanGesture(PanDirection.Horizontal)
const verticalPan = new PanGesture(PanDirection.Vertical)

// 手势组配置
GestureGroup.create()
  .onMask(GestureMask.IgnoreSelf)

// 组件使用
Swiper()
  .gesture(horizontalPan)
  
Scroll()
  .gesture(verticalPan)

这种方式可以精确控制手势分发,水平滑动由Swiper处理,垂直滑动由Scroll处理,有效解决手势冲突问题。

回到顶部