HarmonyOS鸿蒙Next中swiper的nestedScroll无法实现父组件优先怎么手动实现

HarmonyOS鸿蒙Next中swiper的nestedScroll无法实现父组件优先怎么手动实现 swiper的nestedScroll中的SwiperNestedScrollMode类型没有PARENT_FIRST这个属性
无法实现父组件优先
问题描述:scroll里嵌套swiper 当swiper划到最后一页出现scroll底部的column时。往下滑动则无法触发scroll的滚动 仍优先滚动swiper
目标效果 先滚动底部column column消失后再滚动swiper里的内容

@Entry
@Component
struct ScrollSwiperFooter {
  private readonly footerHeight: number = 300;
  private scroller: Scroller = new Scroller();
  private swiperCtl: SwiperController = new SwiperController();

  build() {
    Scroll(this.scroller) {
      Column() {
          Swiper(this.swiperCtl) {
            ForEach([0, 1, 2], (i:number) => {
              Text('Page ' + i)
                .fontSize(30)
                .backgroundColor(i % 2 ? '#FFF' : '#EEE')
                .width('100%')
                .height('100%')
            })
          }
          .vertical(true)
          .loop(false)
          .indicator(false)
          .effectMode(EdgeEffect.None)
          .height('100%')
          .nestedScroll(SwiperNestedScrollMode.SELF_FIRST)

        // 3. 300 vp 底部区域
        Column(){
          Text('300 vp footer').fontSize(24)
        }
          .backgroundColor('#ffcd1d38')
          .width('100%')
          .height(this.footerHeight)

      }
    }
    .scrollBar(BarState.Off)
    .height('100%')
  }
}

更多关于HarmonyOS鸿蒙Next中swiper的nestedScroll无法实现父组件优先怎么手动实现的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复
import { router } from '@kit.ArkUI';

@Entry
@Component
struct ScrollSwiperFooter {
  private readonly footerHeight: number = 300;
  private scroller: Scroller = new Scroller();
  private swiperCtl: SwiperController = new SwiperController();
  @State swiperList: number[] = [0, 1, 2]
  @State enableSwiper: boolean = true
  @State currentIndex: number = 0
  @State lastTouchDown: number = 0
  @State yOffset: number = -1

  build() {
    Scroll(this.scroller) {
      Column() {
        Swiper(this.swiperCtl) {
          ForEach(this.swiperList, (i: number) => {
            Text('Page ' + i)
              .fontSize(30)
              .backgroundColor(i % 2 ? '#FFF' : '#EEE')
              .width('100%')
              .height('100%')
          })
        }
        .vertical(true)
        .loop(false)
        .indicator(false)
        .effectMode(EdgeEffect.None)
        .height('100%')
        .nestedScroll(SwiperNestedScrollMode.SELF_FIRST)
        .onChange((index: number) => {
          this.currentIndex = index
        })
        .onTouch((event: TouchEvent) => {
          const touchInfo = event.touches[0]; // 获取触摸点信息
          if (!event) {
            return;
          }
          switch (event.type) {
            case TouchType.Down: // 手指按下
              this.lastTouchDown = touchInfo.y; // 记录按下位置
              break;
            case TouchType.Move: // 手指移动
              const move = touchInfo.y - this.lastTouchDown
              // move<0 从下往上
              if (move < 0) {
                this.enableSwiper = true
              } else {
                this.enableSwiper = false
              }
              break;
            case TouchType.Up: // 手指抬起
              break;
          }
        })
        .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {
          // column未滑下去,拦截swiper手势
          if (this.currentIndex == this.swiperList.length - 1) {
            if (this.scroller.currentOffset().yOffset <= 0) {
              if (this.enableSwiper) {
                this.enableSwiper = false
                return GestureJudgeResult.REJECT;
              }
              return GestureJudgeResult.CONTINUE;
            }
            return GestureJudgeResult.REJECT;
          }
          if (this.scroller.currentOffset().yOffset > 0) {
            return GestureJudgeResult.REJECT;
          }
          ;
          return GestureJudgeResult.CONTINUE;
        })

        // 3. 300 vp 底部区域
        Column() {
          Text('300 vp footer').fontSize(24)
        }
        .backgroundColor('#ffcd1d38')
        .width('100%')
        .height(this.footerHeight)
      }
    }
    .scrollBar(BarState.Off)
    .height('100%')
    .onDidScroll(() => {
      this.yOffset = this.scroller.currentOffset().yOffset
    })
    .onScrollStop(() => {
      if (this.currentIndex === this.swiperList.length - 1) {
        if (this.yOffset > 100) {
          // 跳转
          router.push({ url: 'pages/ExitTransitionPage' })
        }
        this.yOffset = 0
        this.scroller.scrollTo({
          xOffset: 0, yOffset: 0, animation: {
            duration: 100,
            curve: Curve.Linear
          }
        })
      }
    })
  }
}

更多关于HarmonyOS鸿蒙Next中swiper的nestedScroll无法实现父组件优先怎么手动实现的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


可以在外层Scroll上设置hitTestBehavior控制内层Swiper组件是否响应滑动,设置Block类型表示Scroll自身响应触摸测试,阻塞子节点Swiper响应触摸事件,设置为Transparent类型可以让自身和子节点均响应触摸测试,再通过onScrollEdge获取Scroll是否滑动到底部。

具体示例可以参考:

@Entry
@Component
struct Index {
  private readonly footerHeight: number = 300;
  private scroller: Scroller = new Scroller();
  private swiperCtl: SwiperController = new SwiperController();
  @State ScrollFlag: boolean = false;
  @State side: number = -1;

  build() {
    Scroll(this.scroller) {
      Column() {
        Swiper(this.swiperCtl) {
          ForEach([0, 1, 2], (i: number) => {
            Text('Page ' + i)
              .fontSize(30)
              .backgroundColor(i % 2 ? '#FFF' : '#EEE')
              .width('100%')
              .height('100%')
          })
        }
        .vertical(true)
        .loop(false)
        .indicator(false)
        .effectMode(EdgeEffect.None)
        .height('100%')
        .nestedScroll(SwiperNestedScrollMode.SELF_FIRST)

        // 3. 300 vp 底部区域
        Column() {
          Text('300 vp footer').fontSize(24)
        }
        .backgroundColor('#ffcd1d38')
        .width('100%')
        .height(this.footerHeight)
      }
    }
    .onScrollEdge((side: Edge) => {
      if (side === 2) {
        this.ScrollFlag = true;
      } else if (side === 0) {
        this.ScrollFlag = false;
      }
      this.side = side;
    })
    .hitTestBehavior(this.ScrollFlag ? HitTestMode.Transparent : HitTestMode.Block)
    .onDidScroll((xOffset: number, yOffset: number, scrollState: ScrollState) => {
      if (this.side === 2 && yOffset <= 0) {
        this.ScrollFlag = true;
      }
    })
    .scrollBar(BarState.Off)
    .height('100%')
  }
}

你需要实现「底部 Column 优先滚动消失,再触发 Swiper 滚动」的嵌套滚动效果,由于鸿蒙SwipernestedScroll没有PARENT_FIRST选项,需通过手动拦截触摸事件 + 滚动状态判断 + 事件传递控制来实现,以下是完整可落地的解决方案:

一、核心实现思路

  1. 拦截触摸事件:给包含Swiper和底部Column的父容器添加触摸事件监听,捕获滑动起始 / 移动 / 结束状态;
  2. 判断关键状态
    • 底部Column是否完全显示(父Scroll是否在顶部,未滚动过);
    • 滑动方向(手指向下滑 = 页面向上滚,需优先隐藏Column;手指向上滑 = 页面向下滚,需优先显示Column);
    • Scroll的滚动距离是否已覆盖Column高度(Column是否完全消失);
  3. 控制事件传递
    • Column未完全消失且滑动方向为「向下滑(隐藏Column)」时,阻止事件传递给Swiper,让父Scroll处理滚动;
    • Column完全消失后,放行事件给Swiper,让Swiper处理自身滚动;
  4. 辅助滚动控制:利用ScrollerscrollBy方法手动控制父Scroll滚动,确保Column平滑消失 / 显示。

二、完整可复用代码

@Entry
@Component
struct ScrollSwiperFooter {
  private readonly footerHeight: number = 300; // 底部Column高度
  private scroller: Scroller = new Scroller(); // 父Scroll的滚动控制器
  private swiperCtl: SwiperController = new SwiperController(); // Swiper控制器
  @State startY: number = 0; // 触摸起始Y坐标
  @State isFooterHidden: boolean = false; // 底部Column是否已完全隐藏
  private readonly touchThreshold: number = 5; // 滑动阈值(忽略微小滑动,避免误触)

  build() {
    Scroll(this.scroller) {
      // 核心:给Swiper+footer的父Column添加触摸事件,拦截滑动逻辑
      Column() {
        // 1. 垂直Swiper
        Swiper(this.swiperCtl) {
          ForEach([0, 1, 2], (i: number) => {
            Text('Page ' + i)
              .fontSize(30)
              .backgroundColor(i % 2 ? '#FFF' : '#EEE')
              .width('100%')
              .height('100%')
          })
        }
        .vertical(true)
        .loop(false)
        .indicator(false)
        .effectMode(EdgeEffect.None)
        .height('100%')
        .nestedScroll(SwiperNestedScrollMode.NONE) // 关闭系统嵌套滚动,手动控制
        .enabled(!this.isFooterHidden ? false : true) // Column未隐藏时,禁用Swiper滚动

        // 2. 底部300vp区域
        Column() {
          Text('300 vp footer').fontSize(24)
        }
        .backgroundColor('#ffcd1d38')
        .width('100%')
        .height(this.footerHeight)
      }
      // 关键:拦截触摸事件,实现自定义嵌套滚动逻辑
      .onTouch((event: TouchEvent) => {
        switch (event.type) {
          case TouchType.DOWN:
            // 触摸按下:记录起始Y坐标,更新Column隐藏状态
            this.startY = event.touches[0].y;
            this.updateFooterHiddenState();
            break;
          case TouchType.MOVE:
            // 触摸移动:计算滑动距离,判断是否优先滚动父Scroll
            const currentY = event.touches[0].y;
            const deltaY = currentY - this.startY; // 滑动差值:<0=手指向下滑(页面向上滚),>0=手指向上滑(页面向下滚)

            // 过滤微小滑动,避免抖动
            if (Math.abs(deltaY) < this.touchThreshold) {
              break;
            }

            // 场景1:Column未隐藏,且手指向下滑(需优先隐藏Column)
            if (!this.isFooterHidden && deltaY < 0) {
              // 计算需要滚动的距离(不超过footerHeight)
              const scrollDistance = Math.min(Math.abs(deltaY), this.footerHeight - this.scroller.currentOffset().yOffset);
              // 手动控制父Scroll向上滚动(隐藏Column)
              this.scroller.scrollBy({ xOffset: 0, yOffset: scrollDistance, animation: true });
              // 更新起始Y坐标(保证滑动连续性)
              this.startY = currentY;
              // 阻止事件传递给Swiper,避免Swiper触发滚动
              event.stopPropagation();
              event.preventDefault();
            }
            // 场景2:Column已隐藏,或手指向上滑(放行事件给Swiper/父Scroll)
            else {
              // 若手指向上滑,且父Scroll未回到顶部(Column未完全显示),优先让父Scroll滚动显示Column
              if (deltaY > 0 && this.scroller.currentOffset().yOffset > 0) {
                const scrollDistance = Math.min(deltaY, this.scroller.currentOffset().yOffset);
                this.scroller.scrollBy({ xOffset: 0, yOffset: -scrollDistance, animation: true });
                this.startY = currentY;
                event.stopPropagation();
                event.preventDefault();
              }
              // 否则,放行事件给Swiper,让Swiper处理自身滚动
            }
            break;
          case TouchType.UP:
          case TouchType.CANCEL:
            // 触摸结束:更新最终Column隐藏状态
            this.updateFooterHiddenState();
            break;
        }
        // 返回true表示已处理事件,避免重复触发
        return true;
      })
    }
    .scrollBar(BarState.Off)
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  /**
   * 更新底部Column是否完全隐藏的状态(通过父Scroll的滚动偏移判断)
   */
  private updateFooterHiddenState() {
    const currentScrollY = this.scroller.currentOffset().yOffset;
    this.isFooterHidden = currentScrollY >= this.footerHeight;
    // 同步启用/禁用Swiper(Column隐藏后才允许Swiper滚动)
    this.swiperCtl.setEnabled(this.isFooterHidden);
  }
}

三、关键优化点解释

  1. 关闭系统嵌套滚动:将SwipernestedScroll设为SwiperNestedScrollMode.NONE,避免系统默认的SELF_FIRST干扰手动滚动逻辑,完全由自定义触摸事件控制。
  2. 触摸事件拦截与过滤
    • 通过onTouch监听父Column的触摸事件,捕获DOWN/MOVE/UP/CANCEL四个阶段;
    • 设置touchThreshold(5vp)过滤微小滑动,避免页面抖动和误触。
  3. 滚动状态判断与手动控制
    • 利用scroller.currentOffset().yOffset获取父Scroll的当前滚动距离,判断Column是否完全隐藏(滚动距离≥footerHeight即隐藏);
    • 手指向下滑(deltaY < 0)且Column未隐藏时,调用scroller.scrollBy手动控制父Scroll滚动,隐藏Column,并通过event.stopPropagation()阻止事件传递给Swiper
    • 手指向上滑(deltaY > 0)且Column未完全显示时,优先让父Scroll滚动显示Column,再放行事件给Swiper
  4. Swiper 启用 / 禁用同步:通过isFooterHidden状态同步启用 / 禁用SwiperswiperCtl.setEnabled()/.enabled()),确保Column未隐藏时,Swiper无法触发滚动,避免冲突。

四、效果验证与边界处理

  1. 正常场景
    • 页面初始状态(Column完全显示),手指向下滑,优先滚动父ScrollColumn逐渐消失,直到完全隐藏;
    • Column完全隐藏后,继续向下滑,触发Swiper的垂直滚动,切换Swiper页面;
    • 手指向上滑,优先滚动父ScrollColumn逐渐显示,直到完全显示,再向上滑触发Swiper的反向滚动。
  2. 边界场景
    • 滑动距离不足footerHeight时,Column部分隐藏,再次滑动可继续隐藏,保证滑动连续性;
    • 微小滑动不触发滚动,避免页面抖动;
    • SwiperColumn未隐藏时被禁用,不会出现 “同时滚动” 的冲突问题。

五、总结

  1. 核心是手动拦截触摸事件,替代系统嵌套滚动逻辑,通过滚动偏移判断Column状态,控制事件传递优先级;
  2. 关键 API:Scroller.currentOffset()(获取滚动偏移)、Scroller.scrollBy()(手动控制滚动)、TouchEvent.stopPropagation()(阻止事件传递);
  3. 避坑点:需关闭Swiper的系统嵌套滚动,同步控制Swiper的启用 / 禁用,过滤微小滑动避免抖动。

在HarmonyOS Next中,若Swiper的nestedScroll无法实现父组件优先,可通过自定义滚动事件处理。使用onTouch事件监听触摸操作,结合Scroll组件的onScroll事件,手动控制滚动优先级。通过判断滚动方向与位置,决定是否阻止事件冒泡或拦截滚动。具体实现需在父组件和Swiper间协调滚动状态,确保父组件先响应滚动。

在HarmonyOS Next中,SwiperNestedScrollMode 目前确实没有提供 PARENT_FIRST 模式。要实现父组件(Scroll)优先滚动,当底部Column可见时先滚动Column,可以手动控制滚动逻辑。

核心思路是:监听Swiper的页面索引和手势状态,结合Scroll的控制器,在特定条件下(Swiper在最后一页且用户向下滑动时)主动触发父Scroll的滚动,并暂时禁止Swiper的滚动。

以下是修改后的关键代码实现:

@Entry
@Component
struct ScrollSwiperFooter {
  private readonly footerHeight: number = 300;
  private scroller: Scroller = new Scroller();
  private swiperCtl: SwiperController = new SwiperController();
  // 新增:记录当前Swiper页码
  @State currentIndex: number = 0;
  // 新增:标记是否应优先滚动父Scroll
  private shouldParentScroll: boolean = false;

  build() {
    Scroll(this.scroller) {
      Column() {
        Swiper(this.swiperCtl) {
          ForEach([0, 1, 2], (i: number) => {
            Text('Page ' + i)
              .fontSize(30)
              .backgroundColor(i % 2 ? '#FFF' : '#EEE')
              .width('100%')
              .height('100%')
          })
        }
        .vertical(true)
        .loop(false)
        .indicator(false)
        .effectMode(EdgeEffect.None)
        .height('100%')
        // 使用SELF_FIRST,但通过手势事件覆盖
        .nestedScroll(SwiperNestedScrollMode.SELF_FIRST)
        // 关键:监听页面变化
        .onChange((index: number) => {
          this.currentIndex = index;
        })
        // 关键:监听手势,实现手动优先级控制
        .onGestureSwipe((event: GestureSwipeEvent) => {
          // 当Swiper在最后一页且用户尝试向下滑动时
          if (this.currentIndex === 2 && event.offsetY < 0) {
            // 阻止Swiper自身滚动
            this.swiperCtl.showPrevious();
            // 触发父Scroll向下滚动
            this.scroller.scrollBy({ xOffset: 0, yOffset: -event.offsetY });
            this.shouldParentScroll = true;
          } else {
            this.shouldParentScroll = false;
          }
        })

        Column() {
          Text('300 vp footer').fontSize(24)
        }
        .backgroundColor('#ffcd1d38')
        .width('100%')
        .height(this.footerHeight)
      }
    }
    .scrollBar(BarState.Off)
    .height('100%')
  }
}

实现原理:

  1. 状态跟踪:通过@State变量currentIndex跟踪Swiper当前页码。
  2. 手势拦截:在Swiper的onGestureSwipe事件中判断条件:
    • currentIndex === 2:确保Swiper已处于最后一页。
    • event.offsetY < 0:检测到向下滑动手势(Next坐标系中向下滑动为负值)。
  3. 滚动控制:当条件满足时:
    • this.swiperCtl.showPrevious():阻止Swiper自身滚动(尝试跳回前一页,实际因已在最后一页可能无效果,主要目的是消耗手势)。
    • this.scroller.scrollBy(...):主动触发父Scroll组件滚动相应的偏移量。
    • 设置shouldParentScroll标志,可用于后续更复杂的联动控制。

注意事项:

  • 此方案通过手势事件模拟了PARENT_FIRST的行为,但滚动动画的流畅度可能略低于原生支持。
  • 需要根据实际布局调整滚动偏移量计算逻辑,示例中直接使用了event.offsetY
  • 如果Swiper内容也是可滚动组件,可能需要更精细的手势冲突处理。

这种手动控制的方式在Next当前API限制下,是实现父组件优先滚动的可行方案。

回到顶部