鸿蒙应用开发UI基础第三十八节:Swiper轮播布局-首页Banner与滑动卡片实战开发
·
【学习目标】
- 掌握
Swiper轮播组件的核心架构、布局约束规则,理解轮播容器的核心适配逻辑 - 吃透
Swiper八大核心能力:循环播放、自动轮播、轮播方向控制、导航点自定义、箭头样式配置、页面切换控制、多子项同屏显示、自定义切换动画 - 掌握
SwiperController控制器的完整用法,实现手动翻页、指定页跳转、动画模式自定义等精细化页面控制 - 掌握轮播组件的性能优化方案,理解预加载、懒加载适配、内存管控的最佳实践
- 独立实现首页广告Banner、商品卡片轮播、Tabs页签联动三大高频业务场景,完成可直接落地的商业级轮播组件标准化开发
- 攻克轮播开发高频踩坑点,解决数据增删视图跳动、动画不流畅、样式适配等核心问题
一、工程目录结构
SwiperDemo/
├── entry/
│ └── src/
│ └── main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets // 应用入口
│ │ ├── pages/
│ │ │ ├── Index.ets // 导航首页
│ │ │ ├── SwiperBaseDemo.ets // 基础全量示例
│ │ │ └── HomeBannerDemo.ets // 首页Banner商业实战
│ │ ├── model/
│ │ │ └── SwiperItemData.ets 轮播数据模型
│ │ ├── components/
│ │ │ └── BannerCard.ets // 轮播Banner卡片组件
│ │ └── datasource/
│ │ └── SwiperDataSource.ets // 轮播Banner数据源
│ ├── resources/
│ └── module.json5 // 模块配置文件
└── build-profile.json5 // 工程配置文件
导航首页代码(Index.ets)
import router from '@ohos.router';
@Entry
@Component
struct Index {
build() {
Column({ space: 20 }) {
Text('Swiper 轮播布局教学示例')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 });
Button('1. 轮播基础全量示例', { type: ButtonType.Capsule })
.width('85%')
.height(55)
.fontSize(17)
.onClick(() => {
router.pushUrl({ url: 'pages/SwiperBaseDemo' });
});
Button('2. 首页Banner商业实战', { type: ButtonType.Capsule })
.width('85%')
.height(55)
.fontSize(17)
.onClick(() => {
router.pushUrl({ url: 'pages/HomeBannerDemo' });
});
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
.padding(20)
}
}
二、轮播布局核心基础
2.1 核心概念与架构
轮播布局是App首页、活动运营页最主流的滑动切换容器方案,核心由Swiper轮播容器组件承载,可对多个子组件进行循环滑动切换展示。
它的核心特点是支持手势滑动、自动轮播、循环切换、自定义指示器、精细化动画控制,完美适配广告Banner、卡片滑动、全屏内容切换等场景。针对复杂页面场景,Swiper还提供预加载机制,利用主线程空闲时间提前构建组件,优化滑动体验。
核心铁则:Swiper作为容器组件,可嵌套任意布局组件作为轮播子项,无需专属子组件配合,子项尺寸默认跟随Swiper容器规则自适应。
2.2 Swiper核心布局与约束规则
- 若Swiper设置了自身尺寸属性,则轮播过程中始终以该尺寸生效,子项尺寸跟随容器适配
- 若未设置自身尺寸,分两种情况:
- 设置了
prevMargin/nextMargin属性:Swiper尺寸跟随其父组件 - 未设置
prevMargin/nextMargin属性:Swiper尺寸自动根据子组件大小设置
- 设置了
- 仅设置
loop=true时,可实现首尾无限循环切换,滑动到第一页/最后一页时可继续无缝切换 - 配合
LazyForEach使用时,可通过maintainVisibleContentPosition属性保证数据增删时,当前可见内容位置不变,避免视图跳动
三、基础用法
本示例将覆盖Swiper轮播的所有核心基础知识点,包括循环播放、自动轮播、轮播方向、导航点自定义、箭头配置、控制器控制、多子项同屏、懒加载适配等。
3.1 轮播项数据模型
export interface SwiperItemData {
id: string;
title: string;
imageUrl: string;
}
3.2 数据源
import { SwiperItemData } from '../model/SwiperItemData';
// 懒加载数据源
export class SwiperDataSource implements IDataSource {
private dataList: SwiperItemData[] = [];
private listener: DataChangeListener | undefined;
constructor(initList: SwiperItemData[]) {
this.dataList = initList;
}
// 头部新增数据
addHeaderData(data: SwiperItemData): void {
this.dataList.unshift(data);
this.listener?.onDataAdd(0);
}
// 删除头部数据
deleteHeaderData(): void {
if (this.dataList.length > 0) {
this.dataList.shift();
this.listener?.onDataMove(0, 1);
}
}
totalCount(): number {
return this.dataList.length;
}
getData(index: number): SwiperItemData {
return this.dataList[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener(): void {
this.listener = undefined;
}
}
3.3 轮播基础示例
import { SwiperItemData } from '../model/SwiperItemData';
import { LengthMetrics } from '@kit.ArkUI';
import { SwiperDataSource } from '../datasource/SwiperDataSource';
import { util } from '@kit.ArkTS';
@Entry
@Component
struct SwiperBaseDemo {
// 轮播基础数据
private swiperDataList: SwiperItemData[] = [
{ id: util.generateRandomUUID(true), title: '轮播页1', imageUrl:`https://picsum.photos/1024/960`,
},
{ id: util.generateRandomUUID(true), title: '轮播页2', imageUrl:`https://picsum.photos/1024/960`,
},
{ id: util.generateRandomUUID(true), title: '轮播页3', imageUrl:`https://picsum.photos/1024/960`,
},
{ id: util.generateRandomUUID(true), title: '轮播页4', imageUrl:`https://picsum.photos/1024/960`,
},
{ id: util.generateRandomUUID(true), title: '轮播页5', imageUrl:`https://picsum.photos/1024/960`,
}
];
// 数据源与控制器
private dataSource: SwiperDataSource = new SwiperDataSource(this.swiperDataList);
private swiperController: SwiperController = new SwiperController();
// 状态变量
@State currentIndex: number = 0;
@State isAutoPlay: boolean = true;
@State isLoop: boolean = true;
@State isVertical: boolean = false;
@State displayCount: number = 1;
build() {
Column({ space: 20 }) {
// 顶部标题
Text('Swiper 轮播基础全量示例')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 10 })
// 核心轮播容器
Swiper(this.swiperController) {
LazyForEach(this.dataSource, (item: SwiperItemData) => {
// 轮播子项内容
Stack({alignContent:Alignment.TopEnd}){
Image(item.imageUrl)
Text(item.title)
.fontSize(30)
.padding(10)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
.borderRadius(12)
}, (item: SwiperItemData) => item.id)
}
// 核心属性1:循环播放控制
.loop(this.isLoop)
// 核心属性2:自动轮播控制
.autoPlay(this.isAutoPlay)
// 核心属性3:自动轮播间隔,单位毫秒
.interval(3000)
// 核心属性4:轮播方向控制
.vertical(this.isVertical)
// 核心属性5:当前显示索引
.index(this.currentIndex)
// 核心属性6:单页显示子项数量
.displayCount(this.displayCount)
// 核心属性7:指示器自定义
.indicator(
Indicator.dot()
.bottom(LengthMetrics.vp(10), true) // 忽略组件尺寸,贴底显示
.space(LengthMetrics.vp(6)) // 导航点间距
.itemWidth(8)
.itemHeight(8)
.selectedItemWidth(16)
.selectedItemHeight(8)
.color('#CCCCCC')
.selectedColor('#007AFF')
)
// 核心属性8:导航箭头显示与自定义
.displayArrow({
showBackground: true,
isSidebarMiddle: true,
backgroundSize: 24,
backgroundColor: Color.White,
arrowSize: 18,
arrowColor: Color.Blue
}, false)
// 核心属性9:懒加载数据增删时,保持可见内容位置不变
.maintainVisibleContentPosition(true)
// 宽高设置
.width('100%')
.height(250)
.backgroundColor('#F5F5F5')
.borderRadius(12)
// 核心事件1:页面切换完成后触发,返回当前索引
.onSelected((index: number) => {
console.info(`轮播切换到第${index}`);
this.currentIndex = index;
})
// 核心事件2:索引变化时实时触发
.onChange((index: number) => {
this.currentIndex = index;
})
// 控制按钮区
Column({ space: 12 }) {
// 基础控制按钮
Row({ space: 12 }) {
Button('上一页')
.onClick(() => {
this.swiperController.showPrevious();
})
Button('下一页')
.onClick(() => {
this.swiperController.showNext();
})
}
// 跳转指定页
Row({ space: 12 }) {
Text(`当前页:${this.currentIndex}`)
.fontSize(14)
Button('跳转到第3页')
.onClick(() => {
this.swiperController.changeIndex(2, true);
})
}
// 开关控制区
Row({ space: 12 }) {
Text(`自动轮播:${this.isAutoPlay}`)
.fontSize(14)
Toggle({ type: ToggleType.Switch, isOn: this.isAutoPlay })
.onChange((isOn: boolean) => {
this.isAutoPlay = isOn;
})
}
Row({ space: 12 }) {
Text(`循环播放:${this.isLoop}`)
.fontSize(14)
Toggle({ type: ToggleType.Switch, isOn: this.isLoop })
.onChange((isOn: boolean) => {
this.isLoop = isOn;
})
}
Row({ space: 12 }) {
Text(`垂直轮播:${this.isVertical}`)
.fontSize(14)
Toggle({ type: ToggleType.Switch, isOn: this.isVertical })
.onChange((isOn: boolean) => {
this.isVertical = isOn;
})
}
// 显示数量控制
Row({ space: 12 }) {
Text(`单页显示数量:${this.displayCount}`)
.fontSize(14)
Button('1个')
.onClick(() => {
this.displayCount = 1;
})
Button('2个')
.onClick(() => {
this.displayCount = 2;
})
}
// 数据增删控制
Row({ space: 12 }) {
Button('头部新增数据')
.onClick(() => {
const newData: SwiperItemData = {
id: util.generateRandomUUID(true),
title: '新增轮播页',
imageUrl: `https://picsum.photos/375/${Math.floor(Math.random() * 300) + 100}`
};
this.dataSource.addHeaderData(newData);
})
Button('头部删除数据')
.onClick(() => {
this.dataSource.deleteHeaderData();
})
}
}
.width('100%')
.padding(12)
.backgroundColor($r('sys.color.comp_background_list_card'))
.borderRadius(12)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
3.2 知识点讲解
- 基础核心属性:通过
loop/autoPlay/interval/vertical/index/displayCount六大属性,覆盖轮播的基础播放、方向、显示数量等核心控制能力 - 指示器自定义:通过
indicator属性实现导航点的位置、间距、尺寸、颜色全量定制,同时通过bottom(bottom, ignoreSize)参数解决导航点底部间距问题 - 导航箭头配置:通过
displayArrow属性实现箭头的显示、样式、背景自定义,满足不同设计风格的需求 - 控制器精细化控制:通过
SwiperController实现上一页、下一页、跳转到指定页的手动控制,覆盖业务中按钮控制轮播的高频需求 - 懒加载与数据适配:实现
IDataSource接口,配合LazyForEach实现数据懒加载,通过maintainVisibleContentPosition属性解决数据增删时的视图跳动问题 - 生命周期事件:通过
onSelected/onChange事件监听轮播页面切换,实现索引同步、埋点统计、联动UI更新等业务逻辑 - 预加载优化:Swiper原生预加载机制,利用主线程空闲时间提前构建组件,优化滑动体验
四、商业级实战案例:App首页Banner与Tabs联动开发
在掌握了基础用法之后,我们通过完整的App首页Banner实战,落地轮播的高级特性、自定义动画、与Tabs联动等商业级需求,实现可直接复用的首页轮播模块。
4.1 模拟数据准备(utils/SwiperMockData.ets)
// Banner数据模型
@Observed
export class BannerItem {
// 类成员属性
id: string;
title: string;
coverUrl: string;
linkUrl: string;
// 构造函数:创建对象时自动赋值
constructor(id: string, title: string, coverUrl: string, linkUrl: string) {
this.id = id;
this.title = title;
this.coverUrl = coverUrl;
this.linkUrl = linkUrl;
}
// 静态方法:生成模拟数据
static generateBannerData(): BannerItem[] {
return [
new BannerItem(
'1',
'春日新品首发',
'https://picsum.photos/1920/1080?random=1',
'pages/ActivitySpring'
),
new BannerItem(
'2',
'限时优惠活动',
'https://picsum.photos/1920/1080?random=2',
'pages/ActivitySale'
),
new BannerItem(
'3',
'会员专属福利',
'https://picsum.photos/1920/1080?random=3',
'pages/MemberCenter'
),
new BannerItem(
'4',
'新品上市',
'https://picsum.photos/1920/1080?random=4',
'pages/NewProduct'
),
new BannerItem(
'5',
'品牌日狂欢',
'https://picsum.photos/1920/1080?random=5',
'pages/BrandDay'
)
];
}
}
export class HomeTabItem {
id: string;
name: string;
// 构造函数
constructor(id: string, name: string) {
this.id = id;
this.name = name;
}
// 静态方法生成数据
static generateHomeTabData(): HomeTabItem[] {
return [
new HomeTabItem('1', '推荐'),
new HomeTabItem('2', '新品'),
new HomeTabItem('3', '热卖'),
new HomeTabItem('4', '优惠'),
new HomeTabItem('5', '会员')
];
}
}
4.2 可复用Banner卡片组件(components/BannerCard.ets)
import { BannerItem } from "../model/BannerItem";
@Reusable
@Component
export struct BannerCard {
@ObjectLink banner: BannerItem;
aboutToReuse(params: Record<string, BannerItem>) {
}
build() {
Stack() {
Image(this.banner.coverUrl)
.width('100%')
.height('100%')
// Banner标题
Text(this.banner.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.position({ left: 20, bottom: 30 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.height('100%')
.onClick(() => {
console.info(`点击Banner:${this.banner.title},跳转链接:${this.banner.linkUrl}`);
})
}
}
4.3 首页Banner与Tabs联动示例代码(pages/HomeBannerDemo.ets)
import { BannerCard } from '../component/BannerCard';
import { BannerItem } from '../model/BannerItem';
import { HomeTabItem } from '../model/HomeTabItem';
import { LengthMetrics } from '@kit.ArkUI';
@Entry
@Component
struct HomeBannerDemo {
// 数据初始化
@State bannerList: BannerItem[] = BannerItem.generateBannerData();
@State tabList: HomeTabItem[] = HomeTabItem.generateHomeTabData();
// 状态变量
@State currentIndex: number = 0;
@State tabCurrentIndex: number = 0;
// 控制器
private swiperController: SwiperController = new SwiperController();
private tabsController: TabsController = new TabsController();
build() {
Column() {
// 顶部状态栏
Row() {
Text('首页')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Image($r('app.media.icon_search'))
.width(24)
.height(24)
.fillColor('#333333')
}
.width('100%')
.height(50)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 核心Banner轮播区
Swiper(this.swiperController) {
ForEach(this.bannerList, (item: BannerItem) => {
BannerCard({ banner: item })
.width('100%')
.height('100%')
}, (item: BannerItem) => item.id)
}
.loop(true)
.autoPlay(true)
.interval(4000)
.indicator(
Indicator.dot()
.bottom(10, true)
.space(LengthMetrics.vp(6))
.itemWidth(6)
.itemHeight(6)
.selectedItemWidth(12)
.selectedItemHeight(6)
.color('#FFFFFF80')
.selectedColor(Color.White)
)
.width('100%')
.height(200)
.onSelected((index: number) => {
this.currentIndex = index;
})
// 分类Tab栏
Tabs({ barPosition: BarPosition.Start, index:this.currentIndex,controller: this.tabsController }) {
ForEach(this.tabList, (tab: HomeTabItem, index: number) => {
TabContent() {
Column() {
Text(`${tab.name}内容区`)
.fontSize(16)
.fontColor('#666666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
}
.tabBar(this.TabBarBuilder(tab.name,index))
}, (tab: HomeTabItem) => tab.id)
}
.onTabBarClick((index: number) => {
this.tabCurrentIndex = index;
// Tab点击同步切换Swiper
this.swiperController.changeIndex(index % this.bannerList.length, true);
})
.barMode(BarMode.Scrollable)
.onChange((index:number)=>{
this.tabCurrentIndex = index
})
.animationDuration(300)
.backgroundColor(Color.White)
.barHeight(50)
.barMode(BarMode.Fixed)
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// TabBar自定义构建器
@Builder
TabBarBuilder(name: string, index: number) {
Column() {
Text(name)
.fontSize(index === this.tabCurrentIndex ? 16 : 14)
.fontColor(index === this.tabCurrentIndex ? '#007AFF' : '#666666')
.fontWeight(index === this.tabCurrentIndex ? FontWeight.Bold : FontWeight.Medium)
}
.padding({ left: 16, right: 16 })
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
五、最佳实践与高频避坑指南
5.1 开发最佳实践
- 循环播放规范:广告Banner、卡片轮播等场景默认开启
loop=true,引导页、步骤页等有明确首尾边界的场景关闭循环播放 - 自动轮播规范:首页Banner推荐开启自动轮播,间隔设置为3000-5000毫秒,避免切换过快影响用户浏览;用户手动滑动后暂停自动轮播,滑动结束后恢复
- 指示器规范:轮播项超过3个时必须显示指示器,指示器位置推荐底部居中,选中态与未选中态要有明确视觉区分
- 性能规范:轮播项超过5个时,必须使用
LazyForEach懒加载,配合@Reusable装饰器实现组件复用,降低内存占用 - 数据适配规范:动态增删轮播数据时,必须开启
maintainVisibleContentPosition=true,避免数据变化导致视图跳动,影响用户体验
六、内容总结
- 核心架构:轮播布局采用
Swiper容器组件为核心,可嵌套任意布局组件作为子项,无需专属子组件配合,天然支持手势滑动与循环切换 - 核心配置:通过
loop/autoPlay/interval控制播放规则,通过indicator/displayArrow自定义指示器与箭头,通过SwiperController实现精细化控制 - 核心能力:循环播放、自动轮播、横竖屏方向切换、多子项同屏显示、自定义切换动画、与Tabs双向联动、懒加载数据适配
- 核心场景:App首页广告Banner、商品卡片轮播、引导页、全屏短视频切换、活动页图片轮播等商业级高频场景
- 性能优化:懒加载数据适配、组件复用、预加载机制、
maintainVisibleContentPosition视图位置保持,解决轮播滑动卡顿、视图跳动等核心问题
六、代码仓库
- 工程名称:SwiperDemo
- 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
七、下节预告
下一节我们将进入触摸手势交互体系的系统学习,从 Tap、Pan、Pinch、Rotation、Swipe 等基础手势,到手势绑定优先级、父子事件分发、组合手势(顺序/并行/互斥)与多层级手势控制,全面掌握鸿蒙交互开发能力。
更多推荐

所有评论(0)