HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画?

HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画? 一个切换时另一个跟着切换,动画保持同步,可以做到吗?

4 回复

【背景知识】

Swiper组件内包含了PanGesture拖动手势事件,用于滑动轮播子组件。disableSwipe属性设为true会取消内部的PanGesture事件监听。

  • index设置当前在容器中显示的子组件的索引值。该属性支持$$双向绑定变量。
  • onAnimationStart切换动画开始时触发该回调。
  • changeIndex控制Swiper翻页至指定页面。

【解决方案】

1、Swiper的index属性支持$$双向绑定变量,可以将两个Swiper绑定同一个变量。关键代码如下:

@State showIndex: number = 0;
// ...
Swiper(this.swiperControllerA) {
  // ...
}.index($$this.showIndex)
Swiper(this.swiperControllerB) {
  // ...
}.index($$this.showIndex)
// ...

手指滑动一个Swiper切换结束后,另一个Swiper也会切换,但是非手指滑动的Swiper没有动画效果,且相当于一前一后切换,不是同步滑动。

2、Swiper切换子组件的动画开始时会触发onAnimationStart事件,可以在onAnimationStart回调中使用SwiperController的changeIndex方法切换另一个Swiper。

private swiperControllerA: SwiperController = new SwiperController();
private swiperControllerB: SwiperController = new SwiperController();
// ...
Swiper(this.swiperControllerA) {
  // ...
}
.onAnimationStart((index: number, targetIndex: number) => {
  this.swiperControllerB.changeIndex(targetIndex, true)
})
Swiper(this.swiperControllerB) {
  // ...
}
.onAnimationStart((index: number, targetIndex: number) => {
  this.swiperControllerA.changeIndex(targetIndex, true)
})

手指滑动一个Swiper切换过程中,另一个Swiper也会切换,两个Swiper具有动画效果,但是非手指滑动的Swiper会略微滞后

3、如果想保持完全同步可以参考scroll + swiper 案例:

import display from '@ohos.display';
import componentUtils from '@ohos.arkui.componentUtils';

@Entry
@Component
struct swiperTab {
  private displayInfo: display.Display | null = null;
  private animationDuration: number = 300; //动画时间
  @State swiperIndex: number = 0; //内容区当前Index
  @State swiperWidth: number = 0; // vp 内容区宽度
  @State indicatorWidth: number = 0; // vp 页签宽度
  @State indicatorMarginLeft: number = 5; // vp 当前页签距离左边margin
  @State indicatorIndex: number = 0; //当前页签Index
  @State swipeRatio: number = 0; //判断是否翻页,页面滑动超过一半,tabBar切换到下一页。
  private arr: string[] = ['关注', '推荐', '热点', '上海', '视频', '新时代', '新歌', '新碟', '新片'];
  private textLength: number[] = [2, 2, 2, 2, 2, 3, 2, 2, 2] // 控制页签所在父容器宽度,避免造成下划线目标位置计算错误
  private scroller: Scroller = new Scroller();
  private swiperController: SwiperController = new SwiperController();
  aboutToAppear(): void {
    this.displayInfo = display.getDefaultDisplaySync(); //获取屏幕实例
  }
  // 获取屏幕宽度,单位vp
  private getDisplayWidth(): number {
    return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
  }
  // 获取组件大小、位置、平移缩放旋转及仿射矩阵属性信息。
  private getTextInfo(index: number): Record<string, number> {
    let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());
    try {
      return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }
    } catch (error) {
      return { 'left': 0, 'width': 0 }
    }
  }
  // 当前下划线动画
  private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {
    let nextIndex = index;
    // 滑动范围限制,Swiper不可循环,Scroll保持不可循环
    if (index > 0 && event.currentOffset > 0) {
      nextIndex--; // 左滑
    } else if (index < this.arr.length - 1 && event.currentOffset < 0) {
      nextIndex++; // 右滑
    }
    // 获取当前tabbar的属性信息
    let indexInfo = this.getTextInfo(index);
    // 获取目标tabbar的属性信息
    let nextIndexInfo = this.getTextInfo(nextIndex);
     // 滑动页面超过一半时页面切换
    this.swipeRatio = Math.abs(event.currentOffset / this.swiperWidth);
    let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。
    let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;
    let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;
    this.indicatorIndex = currentIndex;
    return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };
  }
 private scrollIntoView(currentIndex: number): void {
    const indexInfo = this.getTextInfo(currentIndex);
    let tabPositionLeft = indexInfo.left;
    let tabWidth = indexInfo.width;
    // 获取屏幕宽度,单位vp
    const screenWidth = this.getDisplayWidth();
    const currentOffsetX: number = this.scroller.currentOffset().xOffset; //当前滚动的偏移量
    this.scroller.scrollTo({
      // 将tabbar定位在正中间
      xOffset: currentOffsetX + tabPositionLeft - screenWidth / 2 + tabWidth / 2,
      yOffset: 0,
      animation: {
        duration: this.animationDuration,
        curve: Curve.Linear,
      }
    });
    this.underlineScrollAuto(this.animationDuration, currentIndex);
  }
  private startAnimateTo(duration: number, marginLeft: number, width: number): void {
    animateTo({
      duration: duration, // 动画时长
      curve: Curve.Linear, // 动画曲线
      onFinish: () => {
        console.info('play end')
      }
    }, () => {
      this.indicatorMarginLeft = marginLeft;
      this.indicatorWidth = width;
    })
  }

  // 下划线动画
  private underlineScrollAuto(duration: number, index: number): void {
    let indexInfo = this.getTextInfo(index);
    this.startAnimateTo(duration, indexInfo.left, indexInfo.width);
  }
  
  build() {
    Column() {
      // tabbar
      Column() {
        Scroll(this.scroller) {
          Row() {
            ForEach(this.arr, (item: string, index: number) => {
              Column() {
                Text(item)
                  .fontSize(16)
                  .borderRadius(5)
                  .fontColor(this.indicatorIndex === index ? Color.Red : Color.Black)
                  .fontWeight(this.indicatorIndex === index ? FontWeight.Bold : FontWeight.Normal)
                  .margin({ left: 5, right: 5 })
                  .id(index.toString())
                  .onAreaChange((oldValue: Area, newValue: Area) => {
                    if (this.indicatorIndex === index &&
                      (this.indicatorMarginLeft === 0 || this.indicatorWidth === 0)) {
                      if (newValue.globalPosition.x != undefined) {
                        let positionX = Number.parseFloat(newValue.globalPosition.x.toString());
                        this.indicatorMarginLeft = Number.isNaN(positionX) ? 0 : positionX;
                      }
                      let width = Number.parseFloat(newValue.width.toString());
                      this.indicatorWidth = Number.isNaN(width) ? 0 : width;
                    }
                  })
                  .onClick(() => {
                    this.indicatorIndex = index;
                    // 点击tabbar下划线动效
                    this.underlineScrollAuto(this.animationDuration, index);
                    this.scrollIntoView(index);
                    // 跟swiper进行联动
                    this.swiperIndex = index;
                  })
              }
              .width(this.textLength[index] * 28)
            }, (item: string) => item)
          }
          .blendMode(BlendMode.SRC_IN, BlendApplyType.OFFSCREEN)
          .backgroundColor(Color.Transparent)
          .height(32)
        }
        // 设置tabbar文字两端显隐
        .linearGradient({
          angle: 90,
          colors: [['rgba(0, 0, 0, 0)', 0], ['rgba(0, 0, 0, 1)', 0], ['rgba(0, 0, 0, 1)', 0.9], ['rgba(0, 0, 0, 0)', 1]]
        })
        .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
        .width('100%')
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .edgeEffect(EdgeEffect.None)
        // 滚动事件回调, 返回滚动时水平、竖直方向偏移量
        .onScroll((xOffset: number, yOffset: number) => {
          this.indicatorMarginLeft -= xOffset;
        })
        // 滚动停止事件回调
        .onScrollStop(() => {
          this.underlineScrollAuto(0, this.indicatorIndex);
        })

        Column()
          .width(this.indicatorWidth)
          .height(2)
          .borderRadius(2)
          .backgroundColor(Color.Red)
          .alignSelf(ItemAlign.Start)
          .margin({ left: this.indicatorMarginLeft, top: 5 })
      }
      .width('100%')
      .margin({ top: 15, bottom: 10 })

      Swiper(this.swiperController) {
        ForEach(this.arr, (item: string, index: number) => {
          Column() {
            Text(item)
          }
          .width('100%')
          .height('100%')
          .alignItems(HorizontalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
          .onAreaChange((oldValue: Area, newValue: Area) => {
            let width = Number.parseFloat(newValue.width.toString());
            this.swiperWidth = Number.isNaN(width) ? 0 : width;
          })
        }, (item: string) => item)
      }
      .onChange((index: number) => {
        this.swiperIndex = index;
      })
      .cachedCount(2)
      // 跟自定义tabbar进行联动效果
      .index(this.swiperIndex)
      .indicator(false)
      .curve(Curve.Linear)
      .loop(false)
      .onAnimationStart((index: number, targetIndex: number, event: SwiperAnimationEvent) => {
        // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。
        this.indicatorIndex = targetIndex;
        this.underlineScrollAuto(this.animationDuration, targetIndex);
      })
      .onAnimationEnd((index: number, event: SwiperAnimationEvent) => {
        // 切换动画结束时触发该回调。下划线动画停止。
        let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
        this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);
        this.scrollIntoView(index);
      })
      .onGestureSwipe((index: number, event: SwiperAnimationEvent) => {
        // 在页面跟手滑动过程中,逐帧触发该回调。
        let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
        this.indicatorIndex = currentIndicatorInfo.index; //当前页签index
        this.indicatorMarginLeft = currentIndicatorInfo.left; //当前页签距离左边margin
        this.indicatorWidth = currentIndicatorInfo.width; //当前页签宽度
      })
    }
    .width('100%')
    .height('100%')
  }
}

更多关于HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


用同一个变量进行控制

在HarmonyOS Next中,可通过绑定SwiperController实现两个Swiper完全同步。为两个Swiper组件设置相同的controller属性,通过监听onChange事件,使用swipeTo方法确保切换索引一致。动画同步需确保两个Swiper的duration和curve参数完全相同。

是的,可以通过绑定相同的 current 属性和 change 事件来实现两个 Swiper 的完全同步。具体步骤如下:

  1. 在数据模型中定义一个变量(如 currentIndex)用于控制两个 Swiper 的当前索引。
  2. 为两个 Swiper 组件设置相同的 current 属性,绑定到 currentIndex
  3. 为每个 Swiper 添加 change 事件监听,当任意一个 Swiper 切换时,更新 currentIndex 以触发另一个 Swiper 同步切换。

示例代码(基于 ArkTS):

@State currentIndex: number = 0;

build() {
  Column() {
    Swiper() {
      // Swiper1 内容
    }
    .current(this.currentIndex)
    .onChange((index: number) => {
      this.currentIndex = index;
    })

    Swiper() {
      // Swiper2 内容
    }
    .current(this.currentIndex)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }
}

这样,无论是手动滑动还是程序控制,两个 Swiper 的切换行为和动画都会保持同步。

回到顶部