Swiper 组件实现复杂轮播图
概述
在各类应用和网站中,轮播图的使用非常广泛,它在信息展示和用户交互方面扮演着重要角色。轮播图不仅能在有限的屏幕区域内展示更多内容,还能有效地将关键的信息传递给用户。在开发应用或网站时,可以通过轮播图优先展示重要内容,次要内容则随后呈现,用户能够自主控制浏览节奏,滑动交互也能为用户带来发现内容的愉悦感,从而提升用户体验。
本文将通过以下两个场景介绍如何使用 Swiper 组件实现不同的轮播效果:
- 使用 Swiper 实现图文作品合集:图文作品合集由图片和文字组合而成,通过 Swiper 组件来动态展示图片,实现图片的轮播效果。
- 实现轮播图片叠加效果:轮播图的叠加效果(多层轮播图视觉叠加)可以创造独特的视觉层次和交互体验。
使用 Swiper 实现图文作品合集
场景描述
在一些短视频平台上,经常能看到由图片和文字组合而成的作品集。这些作品集通常由多张图片构成,支持自动轮播或手动切换。
- 当作品自动播放时,图片会每隔几秒自动切换到下一张,且下方的进度条进度与每张图片的停留时间相匹配。
- 当用户主动触发播放操作时,下方进度条会跟着图片的滑动切换而改变成未完成状态或已完成状态。
效果如图所示。

实现原理
图文作品轮播可以通过 Swiper 组件及其指示器的联动效果来实现。由于 Swiper 组件的指示器不可自定义,因此需要分开实现。
图片区域需要使用 Swiper 组件来实现。将图片合集的数据传入 Swiper 组件后,需要对 Swiper 组件设置一些属性,来完成图片自动轮播效果。
- 通过设置 loop 属性控制是否循环播放,该属性默认值为 true。当 loop 为 true 时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果 loop 为 false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
- 通过设置 autoPlay 属性,控制是否自动轮播子组件。该属性默认值为 false。autoPlay 为 true 时,会自动切换播放子组件。
- 通过设置 interval 属性,控制子组件与子组件之间的播放间隔。interval 属性默认值为 3000,单位毫秒。
- 通过设置 indicator 属性为 false,来关闭 Swiper 组件自带的导航点指示器样式。
底部导航点(进度条)有三种样式:未完成状态的样式、已完成状态的样式和正在进行进度增长的样式。
- 进度条布局:开发者可以使用层叠布局 (Stack),配合 Row 容器来实现进度条的布局。
- 图文播放时间与进度条匹配:要实现进度条缓慢增长至完成状态且用时与图片播放时间相匹配的效果,可以给 Row 容器组件添加属性动画 (animation),设置 duration(动画持续时间)与图片播放时间匹配即可。
- 进度条状态切换:通过比较当前图片的 currentIndex 与进度条的 index,当 currentIndex 大于 index 时,应将进度条样式设置为已完成状态;反之,则设置为未完成状态。可以通过将进度条的背景颜色设置为 Color.White 或 Color.Grey 来实现这两种状态的切换。
开发步骤
- Swiper 组件初始化:
- 使用
Swiper组件创建一个轮播图容器,传入this.swiperController作为控制器。
- 使用
- 数据加载与展示:
- 使用
LazyForEach遍历this.data数组,其中每个元素是一个PhotoData对象。 - 对于每个
PhotoData对象,创建一个Image组件,图片资源通过$r函数加载,路径为app.media.加上item.id。
- 使用
- 图片尺寸调整:
- 图片的宽度根据
foldStatus的值动态调整:如果foldStatus为 2,宽度为 100%;否则宽度为 70%。 - 图片高度固定为 100%。
- 图片的宽度根据
- 轮播图配置:
- 设置轮播图为循环模式(
.loop(true))。 - 启用自动播放功能(
.autoPlay(true)),每 3 秒(.interval(3000))切换一次图片。 - 禁用指示器显示(
.indicator(false)),即不显示当前页面的指示点。
- 设置轮播图为循环模式(
- 函数定义与布局初始化:
- 使用
@Builder装饰器定义progressComponent函数,这是一个构建器函数,用于创建 UI 组件。 - 使用
Row组件创建一个水平布局,设置space为 5,用于在进度条之间添加间距。
- 使用
- 循环渲染进度条:
- 使用
ForEach循环遍历this.progressData数组,每个元素为PhotoData类型。 - 对于每个
PhotoData元素,创建一个Stack组件,设置alignContent为Alignment.Start,用于对齐子组件。
- 使用
- 进度条样式与动画:
- 在
Stack组件中嵌套一个Row组件,设置zIndex为 1,确保其在Stack中的层级较高。 - 设置
width和height属性,根据当前索引是否等于数据索引来决定进度条的宽度和高度。 - 使用
borderRadius设置圆角效果,并设置背景颜色为白色。 - 添加动画效果,使用
animation方法定义动画参数,包括持续时间、曲线类型、迭代次数和播放模式。动画的持续时间和曲线类型根据当前索引是否等于数据索引来决定。
- 在
- 布局权重设置:
- 使用
layoutWeight(1)设置每个Stack组件的布局权重为 1,确保它们在水平方向上均匀分布。
- 使用
- 整体布局设置:
- 设置
Row组件的width为 '100%',height为 50,确保整个组件的宽度和高度符合预期。
- 设置
滑动切换图片后,关闭自动轮播与循环轮播。此时,开发者需要给 Swiper 组件添加 onGestureSwipe 事件,来判断页面是否跟手滑动。其中 slide 为布尔值,用来判断页面是否跟手滑动。默认值为 false,当页面跟手滑动时,slide 的值为 true。当进行滑动切换时,autoPlay、loop 属性的取值为 false,即关闭自动轮播与循环播放功能。若想实现滑动图片后仍自动循环轮播,直接去掉 slide 相关代码片段即可。
Swiper(this.swiperController) {
// ...
}
// .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
// this.slide = true;
// })
示意效果如下图所示。
创建进度条自定义组件 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)
}
为 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)
适配折叠屏,在 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);
// ...
}
代码逻辑走读:当折叠状态取值为 2 时,表示当前折叠状态为折叠。
实现轮播图的叠加效果
场景描述
轮播图的叠加效果通过视觉层叠、内容交互动画,能显著提升信息传达的丰富性和用户体验的沉浸感。通常情况下,轮播图是按照顺序依次轮播,但是有时候也希望能够以一种重叠的方式进行展示,即当前展示的图片覆盖在前一个图片上方,给用户一种更加流畅的切换体验。
效果如图所示。

实现原理
使用层叠布局 (Stack) 可以实现图片的叠加,结合滑动手势来实现图片的切换,并通过 animateTo 添加相应的切换动画。通过给图片添加以下属性来实现。
- zIndex 属性来设置组件的堆叠顺序,Index 值越大,显示层级越高,即 zIndex 值大的组件会覆盖在 zIndex 值小的组件上方。
- visibility 属性来控制显示或隐藏,值为 Visibility.Hidden 时,表示隐藏,但参与布局进行占位;值为 Visibility.Visible 时,表示显示;值为 Visibility.None 时,表示隐藏,但不参与布局,不进行占位。
- scale 属性来控制是否缩放,可以分别设置 X 轴、Y 轴、Z 轴的缩放比例,默认值为 1。
开发步骤
- Stack 布局初始化:使用 Stack 布局容器,并设置其 alignContent 属性为 Alignment.Start,这意味着子组件将从起始位置对齐。
- ForEach 循环:遍历 this.data 数组,为每个元素创建一个 Column 组件。
- Column 组件:每个 Column 包含一个 Text 组件,显示数组中的字符串元素。
- 样式和位置属性:为每个 Column 设置 zIndex、visibility、opacity、backgroundColor、width、height、offset 和 scale 属性,这些属性根据数组中的对应值动态设置。
- 唯一键生成:使用 `(item: string, index: number) => ${item}index`` 作为 ForEach 的键生成函数,确保每个组件的唯一性。
- onAppear 事件:在组件首次出现时触发,设置
this.isAppear为 true,表示组件已经出现。 - 手势开始时的处理:
- 当用户开始滑动时,清除任何正在运行的定时器,并将状态变量
isStart设置为false。同时,设置某个数组元素的可见性为Visibility.Visible,表示某个卡片或界面元素现在可见。
- 当用户开始滑动时,清除任何正在运行的定时器,并将状态变量
- 手势进行时的处理:
- 获取滑动事件的水平偏移量
offsetX,并根据这个值计算出一个缩放因子distanceScl。这个缩放因子用于调整当前页卡及其下一层页卡的显示效果,实现滑动时的动画效果。 - 调用
changeSubPageWhenUpdate方法,更新第二层和第三层页卡的显示状态,使用之前计算的distanceScl作为参数。 - 再次调用
changeSubPageWhenUpdate方法,更新第四层页卡的显示状态,这次不使用distanceScl调整透明度,而是根据是否达到阈值决定是否切换页面。
- 获取滑动事件的水平偏移量
- 手势结束时的处理:
- 获取滑动事件的速度
velocityX,并根据滑动距离和速度决定接下来执行的操作。 - 如果滑动距离小于某个阈值
OFFSET_DISTANCE_4_SWICH_THREHOLD,则认为用户想要切换页面,更新可见性状态。 - 如果滑动距离大于阈值,或者滑动速度超过某个阈值
SWITCH_PAGE_VELOCITY_THREHOLD,则根据滑动方向执行页面切换或淡出效果。 - 使用之前计算的
distanceScl作为参数。 - 再次调用
changeSubPageWhenUpdate方法,更新第四层页卡的显示状态,这次不使用distanceScl调整透明度,而是根据是否达到阈值决定是否切换页面。
- 获取滑动事件的速度
在 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) {
// left
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
}
})
}))
使用 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;
})

