【高心星出品】

Swiper组件实现复杂轮播图

概述

在各类应用和网站中,轮播图的使用非常广泛,它在信息展示和用户交互方面扮演着重要角色。轮播图不仅能在有限的屏幕区域内展示更多内容,还能有效地将关键的信息传递给用户。在开发应用或网站时,可以通过轮播图优先展示重要内容,次要内容则随后呈现,用户能够自主控制浏览节奏,滑动交互也能为用户带来发现内容的愉悦感,从而提升用户体验。

本文将通过以下两个场景介绍如何使用Swiper组件实现不同的轮播效果。

使用Swiper实现图文作品合集:图文作品合集由图片和文字组合而成,通过Swiper组件来动态展示图片,实现图片的轮播效果。

实现轮播图片叠加效果:轮播图的叠加效果(多层轮播图视觉叠加)可以创造独特的视觉层次和交互体验。

使用Swiper实现图文作品合集

场景描述

在一些短视频平台上,经常能看到由图片和文字组合而成的作品集。这些作品集通常由多张图片构成,支持自动轮播或手动切换。

  1. 当作品自动播放时,图片会每隔几秒自动切换到下一张,且下方的进度条进度与每张图片的停留时间相匹配。
  2. 当用户主动触发播放操作时,下方进度条会跟着图片的滑动切换而改变成未完成状态或已完成状态。

效果如图所示。

E6187A5BF.gif

实现原理

图文作品轮播可以通过Swiper组件及其指示器的联动效果来实现。由于Swiper组件的指示器不可自定义,因此需要分开实现。

图片区域需要使用Swiper组件来实现。将图片合集的数据传入Swiper组件后,需要对Swiper组件设置一些属性,来完成图片自动轮播效果。

  1. 通过设置loop属性控制是否循环播放,该属性默认值为true。当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
  2. 通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。autoPlay为true时,会自动切换播放子组件。
  3. 通过设置interval属性,控制子组件与子组件之间的播放间隔。interval属性默认值为3000,单位毫秒。
  4. 通过设置indicator属性为false,来关闭Swiper组件自带的导航点指示器样式。

底部导航点(进度条)有三种样式:未完成状态的样式、已完成状态的样式和正在进行进度增长的样式。

  1. 进度条布局:开发者可以使用层叠布局 (Stack),配合Row容器来实现进度条的布局。
  2. 图文播放时间与进度条匹配:要实现进度条缓慢增长至完成状态且用时与图片播放时间相匹配的效果,可以给Row容器组件添加属性动画 (animation),设置duration(动画持续时间)与图片播放时间匹配即可。
  3. 进度条状态切换:通过比较当前图片的currentIndex与进度条的index,当currentIndex大于index时,应将进度条样式设置为已完成状态;反之,则设置为未完成状态。可以通过将进度条的背景颜色设置为Color.White或Color.Grey来实现这两种状态的切换。

开发步骤

  1. 为Swiper组件设置loop、autoPlay、interval和indicator属性。在手指未滑动的情况下,图片每3秒会进行一次切换,并且自动进行轮播。

    Swiper(this.swiperController) {
      LazyForEach(this.data, (item: PhotoData, index: number) => {
        Image($r(`app.media.` + item.id))
          .width(this.foldStatus === 2 ? '100%' : '70%')
          .height('100%')
      }, (item: PhotoData) => JSON.stringify(item))
    }
    .loop(true)
    .autoPlay(true)
    //.autoPlay(this.slide ? false : true)
    .interval(3000)
    .indicator(false)
    

    代码逻辑走读:

    1. Swiper组件初始化
      • 使用Swiper组件创建一个轮播图容器,传入this.swiperController作为控制器。
    2. 数据加载与展示
      • 使用LazyForEach遍历this.data数组,其中每个元素是一个PhotoData对象。
      • 对于每个PhotoData对象,创建一个Image组件,图片资源通过$r函数加载,路径为app.media.加上item.id
    3. 图片尺寸调整
      • 图片的宽度根据foldStatus的值动态调整:如果foldStatus为2,宽度为100%;否则宽度为70%。
      • 图片高度固定为100%。
    4. 轮播图配置
      • 设置轮播图为循环模式(.loop(true))。
      • 启用自动播放功能(.autoPlay(true)),每3秒(.interval(3000))切换一次图片(.interval(3000))。
      • 禁用指示器显示(.indicator(false)),即不显示当前页面的指示点。

    示意效果如下图所示。

    在这里插入图片描述

  2. 创建进度条自定义组件progressComponent。代码中,this.progressData为图片集合的数组,this.currentIndex为当前播放的图片在图片集合数组中的索引,index为进度条对应的图片在图片集合数组中的索引。当this.currentIndex > index时,表示图片集合数组中索引0-index的进度条都是已完成状态。

    @Builder
    progressComponent() {
      Row({ space: 5 }) {
        ForEach(this.progressData, (item: PhotoData, index: number) => {
          Stack({ alignContent: Alignment.Start }) {
            // Use the cascading component to stack progress bars of different styles together
            // ...
            Row()
              .zIndex(1)
              .width(this.currentIndex === index ? '100%' : '0')
              .height(2)
              .borderRadius(2)
              .backgroundColor(Color.White)
              // Add a growth animation to the progress bar
              .animation({
                duration: this.currentIndex === index ? this.duration : 0,
                curve: Curve.Linear,
                iterations: 1,
                playMode: PlayMode.Normal
              })
            // ...
          }
          .layoutWeight(1)
        }, (item: PhotoData) => JSON.stringify(item))
      }
      .width('100%')
      .height(50)
    }
    

    代码逻辑走读:

    1. 函数定义与布局初始化
      • 使用 @Builder装饰器定义 progressComponent函数,这是一个构建器函数,用于创建 UI 组件。
      • 使用 Row组件创建一个水平布局,设置 space为 5,用于在进度条之间添加间距。
    2. 循环渲染进度条
      • 使用 ForEach循环遍历 this.progressData数组,每个元素为 PhotoData类型。
      • 对于每个 PhotoData元素,创建一个 Stack组件,设置 alignContentAlignment.Start,用于对齐子组件。
    3. 进度条样式与动画
      • Stack组件中嵌套一个 Row组件,设置 zIndex为 1,确保其在 Stack中的层级较高。
      • 设置 widthheight属性,根据当前索引是否等于数据索引来决定进度条的宽度和高度。
      • 使用 borderRadius设置圆角效果,并设置背景颜色为白色。
      • 添加动画效果,使用 animation方法定义动画参数,包括持续时间、曲线类型、迭代次数和播放模式。动画的持续时间和曲线类型根据当前索引是否等于数据索引来决定。
    4. 布局权重设置
      • 使用 layoutWeight(1)设置每个 Stack组件的布局权重为 1,确保它们在水平方向上均匀分布。
    5. 整体布局设置
      • 设置 Row组件的 width为 ‘100%’,height为 50,确保整个组件的宽度和高度符合预期。

    示意效果如下图所示。

    在这里插入图片描述

  3. 滑动切换图片后,关闭自动轮播与循环轮播。此时,开发者需要给Swiper组件添加onGestureSwipe事件,来判断页面是否跟手滑动。其中slide为布尔值,用来判断页面是否跟手滑动。默认值为false,当页面跟手滑动时,slide的值为true。当进行滑动切换时,autoPlay、loop属性的取值为false,即关闭自动轮播与循环播放功能。若想实现滑动图片后仍自动循环轮播,直接去掉slide相关代码片段即可。

    Swiper(this.swiperController) {
      // ...
    }
    // ...
    // .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
    //   this.slide = true;
    // })
    

    示意效果如下图所示。

7836D5435B52B.gif

  1. 适配折叠屏,在aboutToAppear生命周期函数中获取设备是否可折叠,并且同时获取折叠状态,通过设备类型以及折叠状态设置Swiper的宽高值。同时绑定change回调事件,当页面变化时,触发回调实时刷新折叠状态值。

    aboutToAppear(): void {
      try {
        this.isFoldable = display.isFoldable();
        // Get the foldable screen status
        let foldStatus: display.FoldStatus = display.getFoldStatus();
        if (this.isFoldable) {
          this.foldStatus = foldStatus;
          let callback: Callback<number> = () => {
            let data: display.FoldStatus = display.getFoldStatus();
            this.foldStatus = data;
          }
          // Monitor the changes in the unfolded status of the foldable screen
          display.on('change', callback);
        }
      } catch (error) {
        let err = error as BusinessError;
        hilog.error(0x0000, 'ImageSwitch', `getFoldStatus failed. code=${err.code}, message=${err.message}`);
      }
      let list: PhotoData[] = [];
      for (let i = 1; i <= 7; i++) {
        let newPhotoData = new PhotoData();
        newPhotoData.id = i;
        list.push(newPhotoData);
      }
      this.progressData = list;
      this.data = new DataSource(list);
    
      // ...
    }
    

    代码逻辑走读:

    1. 折叠屏检测与状态获取
      • 使用 display.isFoldable()方法检测设备是否为折叠屏,并将结果存储在 this.isFoldable中。
      • 调用 display.getFoldStatus()获取当前折叠屏的状态,并存储在 foldStatus中。
    2. 折叠屏状态变化监听
      • 如果设备是折叠屏,设置一个回调函数 callback,该函数在每次折叠屏状态变化时被调用,更新 this.foldStatus的值。
      • 使用 display.on('change', callback)方法监听折叠屏状态的变化。
    3. 异常处理
      • 如果在获取折叠屏状态时发生错误,捕获异常并记录错误信息。
    4. 照片数据初始化
      • 创建一个空的 list数组,用于存储照片数据。
      • 使用循环初始化一个 PhotoData对象,并将其添加到 list中。
      • list赋值给 this.progressData,并使用 DataSource类初始化 this.data

    当折叠状态取值为2时,表示当前折叠状态为折叠。

实现轮播图的叠加效果

场景描述

轮播图的叠加效果通过视觉层叠、内容交互动画,能显著提升信息传达的丰富性和用户体验的沉浸感。通常情况下,轮播图是按照顺序依次轮播,但是有时候也希望能够以一种重叠的方式进行展示,即当前展示的图片覆盖在前一个图片上方,给用户一种更加流畅的切换体验。

效果如图所示。

在这里插入图片描述

实现原理

使用层叠布局 (Stack)可以实现图片的叠加,结合滑动手势来实现图片的切换,并通过animateTo添加相应的切换动画。通过给图片添加以下属性来实现。

  • zIndex属性来设置组件的堆叠顺序,Index值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。
  • visibility属性来控制显示或隐藏,值为Visibility.Hidden时,表示隐藏,但参与布局进行占位;值为Visibility.Visible时,表示显示;值为Visibility.None时,表示隐藏,但不参与布局,不进行占位。
  • scale属性来控制是否缩放,可以分别设置X轴、Y轴、Z轴的缩放比例,默认值为1。

开发步骤

  1. 使用Stack组件将图片进行层叠布局,并给其设置zIndex、visibility、scale等属性。

    Stack({ alignContent: Alignment.Start }) {
      ForEach(this.data, (item: string, index: number) => {
        Column() {
          Text(item)
            .fontSize(30)
            .fontColor(Color.Blue)
        }
        .zIndex(this.zIndexArray[index])
        .visibility(this.visibleArray[index])
        .opacity(this.opacityArray[index])
        .backgroundColor(Color.Pink)
        .width(this.sizeArray[index].width)
        .height(this.sizeArray[index].height)
        .offset({ x: this.offsetXArray[index], y: 0 })
        .scale(this.scaleArray[index])
      }, (item: string, index: number) => `${item}index`)
    }
    .onAppear(() => { // It is called only once after mounting
      this.isAppear = true;
    })
    

    代码逻辑走读:

    1. Stack布局初始化:使用Stack布局容器,并设置其alignContent属性为Alignment.Start,这意味着子组件将从起始位置对齐。
    2. ForEach循环:遍历this.data数组,为每个元素创建一个Column组件。
      • Column组件:每个Column包含一个Text组件,显示数组中的字符串元素。
      • 样式和位置属性:为每个Column设置zIndex、visibility、opacity、backgroundColor、width、height、offset和scale属性,这些属性根据数组中的对应值动态设置。
      • 唯一键生成:使用(item: string, index: number) => ${item}index``作为ForEach的键生成函数,确保每个组件的唯一性。
    3. onAppear事件:在组件首次出现时触发,设置this.isAppear为true,表示组件已经出现。
  2. 在Stack上监听左右滑动手势,在手势的回调里面进行图片的动效切换。

    .gesture(
      PanGesture(this.panOption)
        .onActionStart((event: GestureEvent) => {
          clearInterval(timerId); // Stop looping playback
          this.isStart = false;
          this.visibleArray[this.currentIndexArray[3]] = Visibility.Visible;
        })
        .onActionUpdate((event: GestureEvent) => {
          if (!event) {
            return;
          }
          let distanceScl: number = 0;
          let index0 = this.currentIndexArray[PageIndex.FIRSTPAGE];
          // The animation effect of the top card
          this.offsetXArray[index0] = event.offsetX;
          if (this.offsetXArray[index0] < 0) { // 左
            distanceScl = this.offsetXArray[index0] > -OFFSET_DISTANCE_4_FADE_THREHOLD ?
              1.0 + this.offsetXArray[index0] / OFFSET_DISTANCE_4_FADE_THREHOLD : 0;
          } else { // right
            distanceScl = this.offsetXArray[index0] < OFFSET_DISTANCE_4_FADE_THREHOLD ?
              1.0 - this.offsetXArray[index0] / OFFSET_DISTANCE_4_FADE_THREHOLD : 0;
          }
    
          // The animation effect of the second-layer card
          let index1 = this.currentIndexArray[PageIndex.SCENDPAGE];
          this.changeSubPageWhenUpdate(index1, PageIndex.SCENDPAGE, distanceScl, true, true);
    
          // The animation effect of three layers of cards
          let index2 = this.currentIndexArray[PageIndex.THRIDPAGE];
          this.changeSubPageWhenUpdate(index1, PageIndex.THRIDPAGE, distanceScl, true, true);
    
          // The animation effect of four layers of cards
          let index3 = this.currentIndexArray[PageIndex.FOURTHPAGE];
          this.changeSubPageWhenUpdate(index1, PageIndex.FOURTHPAGE, distanceScl, false, true);
        })
        .onActionEnd((event: GestureEvent) => { // Lift your finger
          if (!event) {
            return;
          }
          this.getUIContext().animateTo({
            duration: 200,
            curve: Curve.Linear,
            onFinish: () => { // After the animation effect ends, assign status values to each card to ensure that every component is in the correct state
              // Within the range that triggers the switch page
              if (Math.abs(this.offsetXArray[this.currentIndexArray[PageIndex.FIRSTPAGE]]) <
                OFFSET_DISTANCE_4_SWICH_THREHOLD) {
                this.visibleArray[this.currentIndexArray[PageIndex.FIRSTPAGE]] = Visibility.Visible;
                this.visibleArray[this.currentIndexArray[PageIndex.FOURTHPAGE]] = Visibility.None;
              } else { // Update the status outside the range that triggers the switch page
                this.changePagePropertyWhenFinished();
              }
            }
          }, () => {
            if (this.offsetXArray[this.currentIndexArray[PageIndex.FIRSTPAGE]] > OFFSET_DISTANCE_4_SWICH_THREHOLD ||
              event.velocityX > SWITCH_PAGE_VELOCITY_THREHOLD) { // Fade out of the page to the right
              this.changePageWhenEnd(OFFSET_DISTANCE_4_FADE_THREHOLD, true, true);
            } else if (this.offsetXArray[this.currentIndexArray[PageIndex.FIRSTPAGE]] <
              -OFFSET_DISTANCE_4_SWICH_THREHOLD || event.velocityX < -SWITCH_PAGE_VELOCITY_THREHOLD) { // Fade out of the page to the left
              this.changePageWhenEnd(OFFSET_DISTANCE_4_FADE_THREHOLD, true, false);
            } else {
              this.changePageWhenEnd(0, false, true); // Return
            }
          })
        })
    )
    

    代码逻辑走读:

    1. 手势开始时的处理
      • 当用户开始滑动时,清除任何正在运行的定时器,并将状态变量 isStart设置为 false。同时,设置某个数组元素的可见性为 Visibility.Visible,表示某个卡片或界面元素现在可见。
    2. 手势进行时的处理
      • 获取滑动事件的水平偏移量 offsetX,并根据这个值计算出一个缩放因子 distanceScl。这个缩放因子用于调整当前页卡及其下一层页卡的显示效果,实现滑动时的动画效果。
      • 调用 changeSubPageWhenUpdate方法,更新第二层和第三层页卡的显示状态,使用之前计算的 distanceScl作为参数。
      • 再次调用 changeSubPageWhenUpdate方法,更新第四层页卡的显示状态,这次不使用 distanceScl调整透明度,而是根据是否达到阈值决定是否切换页面。
    3. 手势结束时的处理
      • 获取滑动事件的速度 velocityX,并根据滑动距离和速度决定接下来执行的操作。
      • 如果滑动距离小于某个阈值 OFFSET_DISTANCE_4_SWICH_THREHOLD,则认为用户想要切换页面,更新可见性状态。
      • 如果滑动距离大于阈值,或者滑动速度超过某个阈值 SWITCH_PAGE_VELOCITY_THREHOLD,则根据滑动方向执行页面切换或淡出效果。
        使用之前计算的 distanceScl作为参数。
      • 再次调用 changeSubPageWhenUpdate方法,更新第四层页卡的显示状态,这次不使用 distanceScl调整透明度,而是根据是否达到阈值决定是否切换页面。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐