
文章目录前言代码讲解页面入口怎么安排数据模型先稳住状态变量控制页面反馈布局代码不要从样式开始读Builder 和方法承担复用使用方式完整代码关键代码解析interface 不是摆设State 是交互的开关列表和卡片要靠数据驱动事件回调要短一点样式参数别急着抽常量总结前言轮播图不是只能滑动。控制器、自动播放和指示器同步才是业务里经常会改的地方。我建议把这篇当成一个可以直接改造的 ArkUI 页面模板来看。先跑起来确认页面展示和交互都正常再把里面的模拟数据替换成接口数据最后再调整颜色、间距和业务字段。这样改起来比较稳不容易一边改样式一边把状态逻辑弄乱。这篇文章会按「页面意图、数据结构、状态流转、布局组织、交互细节」这条线来读代码。你不用从第一行样式硬看到最后先抓住主线后面的细节会轻松很多。代码讲解页面入口怎么安排代码里的入口组件是SwiperAutoPlayShowcasePage。如果你把它当成单独页面预览保留Entry就可以如果要塞进现有工程的路由体系通常会去掉Entry再由外层页面或路由模块统一管理。这个习惯很重要。一个项目里入口页面太多预览时容易混后期拆组件也会麻烦。我的做法是案例阶段保留入口接业务时只保留真正需要被路由访问的页面。数据模型先稳住本案例涉及的数据模型主要有BannerItem_leci、FeatureCard、PropDesc。这些interface的作用不是为了“看起来规范”而是给 UI 层划边界。比如列表项需要标题、描述、颜色、选中态就应该在模型里说清楚。后续从接口拿数据时可以在请求层做字段映射别让后端字段名直接污染页面代码。如果你准备接真实业务建议把模拟数组替换成接口返回后的 ViewModel。页面只关心自己要渲染什么不关心接口原始字段长什么样。状态变量控制页面反馈本案例里的关键状态包括currentIndex、isAutoPlay。State的职责是让 UI 对用户操作有反馈。比如选中某个卡片、切换分类、展开内容、修改输入值、控制加载中状态这些都应该由状态驱动。状态一变相关 UI 自动刷新这也是 ArkUI 声明式开发最核心的体验。这里有个容易踩的坑不要把所有数据都塞进State。静态配置用普通数组就够了只有用户会改、接口会更新、页面需要重新渲染的数据才适合放进状态里。布局代码不要从样式开始读这个页面里比较关键的组件有Row横向排列、左右分栏、按钮组或卡片行都依赖它来控制水平方向的节奏。Column纵向组织标题、内容、操作区适合搭页面主骨架。Stack负责图层叠放常见于角标、遮罩、封面文字压图。Scroll用于长内容容器锚点跳转和返回顶部都离不开滚动控制。Swiper不仅能做轮播配合控制器后还能做暂停、切换、状态同步。读布局时我更建议先找大容器再看内部如何拆块。比如一个页面通常会先用Column纵向放标题区、内容区、操作区内容区里再用Row、Grid、List或其他容器细分。这样看代码层级会比逐行看.padding()、.fontSize()清楚得多。如果页面显示错位优先检查容器关系而不是马上改像素。很多问题不是某个.margin()不对而是外层容器的宽高、滚动方向、权重分配或对齐方式没想清楚。Builder 和方法承担复用这篇没有额外拆出 Builder页面结构主要集中在build()中。辅助方法不多逻辑主要写在事件回调和渲染表达式里。Builder更适合复用 UI 片段比如卡片、列表行、头部区域、按钮组。普通方法更适合放计算逻辑比如筛选列表、统计数量、切换状态、生成颜色。两者分清楚后页面会干净很多。我不建议把所有东西都塞进build()。刚开始代码少还行需求一多build()会变成几百行后面改一个按钮都要找半天。使用方式把下面的完整代码放到 ArkTS 页面文件里即可运行。文章中的组件名已经是语义化命名不包含编号式案例名称接路由时直接使用SwiperAutoPlayShowcasePage就行。如果你要改成业务页面优先改三处数据模型、状态变量、事件方法。样式可以后调先保证数据流和交互是通的。完整代码// SwiperAutoPlayShowcasePage: 轮播图组件 - Swiper 基础与自动播放 // 知识点: Swiper、autoPlay、interval、loop、indicator、SwiperController interface BannerItem_leci { bannerId: number bannerTitle: string bannerSubtitle: string bannerEmoji: string gradientColors: string[] actionLabel: string } interface FeatureCard { featureId: number featureTitle: string featureDesc: string featureEmoji: string featureBg: string } interface PropDesc{ prop: string, desc: string } Entry Component struct SwiperAutoPlayShowcasePage { State currentIndex: number 0 State isAutoPlay: boolean true private swiperController: SwiperController new SwiperController() private bannerList: BannerItem_leci[] [ { bannerId: 0, bannerTitle: 鸿蒙世界 · 开启新时代, bannerSubtitle: 全场景智慧体验从这里开始, bannerEmoji: , gradientColors: [#1E3A5F, #2196F3], actionLabel: 立即探索 }, { bannerId: 1, bannerTitle: 618 超级购物节, bannerSubtitle: 百亿补贴 · 全品类五折起, bannerEmoji: ️, gradientColors: [#C62828, #FF5252], actionLabel: 马上抢购 }, { bannerId: 2, bannerTitle: 在线课程限时免费, bannerSubtitle: HarmonyOS 开发从入门到精通, bannerEmoji: , gradientColors: [#1B5E20, #43A047], actionLabel: 免费学习 }, { bannerId: 3, bannerTitle: 新用户专享福利, bannerSubtitle: 注册即送 100 元新人大礼包, bannerEmoji: , gradientColors: [#4A148C, #9C27B0], actionLabel: 领取福利 }, ] private featureCards: FeatureCard[] [ { featureId: 1, featureTitle: 智能家居, featureDesc: 一键掌控全屋设备, featureEmoji: , featureBg: #E3F2FD }, { featureId: 2, featureTitle: 运动健康, featureDesc: 科学记录每一步, featureEmoji: , featureBg: #E8F5E9 }, { featureId: 3, featureTitle: 智慧出行, featureDesc: 实时导航无缝接力, featureEmoji: , featureBg: #FFF8E1 }, { featureId: 4, featureTitle: 影音娱乐, featureDesc: 沉浸式视听享受, featureEmoji: , featureBg: #FCE4EC }, { featureId: 5, featureTitle: 办公效率, featureDesc: 多端协同高效工作, featureEmoji: , featureBg: #F3E5F5 }, { featureId: 6, featureTitle: 安全隐私, featureDesc: 全方位保障数据安全, featureEmoji: , featureBg: #E0F7FA }, ] build() { Column({ space: 0 }) { // 顶部导航 Row({ space: 0 }) { Text( 首页) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(#1A1A1A) .layoutWeight(1) Text() .fontSize(20) .margin({ right: 12 }) Text() .fontSize(20) } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .backgroundColor(#FFFFFF) .border({ width: { bottom: 1 }, color: #F0F0F0 }) Scroll() { Column({ space: 16 }) { // 主轮播图 Column({ space: 8 }) { Swiper(this.swiperController) { ForEach(this.bannerList, (banner: BannerItem_leci) { Stack({ alignContent: Alignment.BottomStart }) { // 渐变背景 Column() .width(100%) .height(200) .linearGradient({ direction: GradientDirection.RightBottom, colors: [ [banner.gradientColors[0], 0.0], [banner.gradientColors[1], 1.0] ] }) .borderRadius(14) // 大 Emoji 装饰 Text(banner.bannerEmoji) .fontSize(90) .opacity(0.3) .position({ right: 16, top: 20 }) // 内容叠加 Column({ space: 8 }) { Text(banner.bannerTitle) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(#FFFFFF) Text(banner.bannerSubtitle) .fontSize(13) .fontColor(rgba(255,255,255,0.85)) Text(banner.actionLabel) .fontSize(13) .fontColor(banner.gradientColors[1]) .fontWeight(FontWeight.Bold) .padding({ left: 16, right: 16, top: 7, bottom: 7 }) .backgroundColor(#FFFFFF) .borderRadius(20) } .padding({ left: 18, bottom: 18 }) .alignItems(HorizontalAlign.Start) } .width(100%) .height(200) }, (banner: BannerItem_leci) banner.bannerId.toString()) } .autoPlay(this.isAutoPlay) .interval(3000) .loop(true) .duration(500) .curve(Curve.EaseInOut) .indicatorStyle({ bottom: 10, color: rgba(255,255,255,0.5), selectedColor: #FFFFFF, size: 6, }) .onChange((index: number) { this.currentIndex index }) .width(100%) .height(200) // 控制区 Row({ space: 12 }) { Text(当前第 ${this.currentIndex 1}/${this.bannerList.length} 张) .fontSize(13) .fontColor(#888888) .layoutWeight(1) Text(this.isAutoPlay ? ⏸ 暂停自动播放 : ▶ 开始自动播放) .fontSize(13) .fontColor(#007DFF) .onClick(() { this.isAutoPlay !this.isAutoPlay }) Text(← 上一张) .fontSize(13) .fontColor(#555555) .onClick(() { this.swiperController.showPrevious() }) Text(下一张 →) .fontSize(13) .fontColor(#555555) .onClick(() { this.swiperController.showNext() }) } .width(100%) } .padding({ left: 12, right: 12 }) // 功能特性横向 Swiper卡片式 Column({ space: 8 }) { Row({ space: 0 }) { Text(核心功能) .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor(#1A1A1A) .layoutWeight(1) Text(全部 ) .fontSize(13) .fontColor(#007DFF) } .width(100%) .padding({ left: 12, right: 12 }) // 三屏卡片 Swiper() { ForEach([0, 1], (pageIdx: number) { Row({ space: 12 }) { ForEach(this.featureCards.slice(pageIdx * 3, pageIdx * 3 3), (feature: FeatureCard) { Column({ space: 8 }) { Text(feature.featureEmoji) .fontSize(32) .width(56) .height(56) .textAlign(TextAlign.Center) .backgroundColor(feature.featureBg) .borderRadius(14) Text(feature.featureTitle) .fontSize(13) .fontColor(#1A1A1A) .fontWeight(FontWeight.Medium) Text(feature.featureDesc) .fontSize(11) .fontColor(#999999) .textAlign(TextAlign.Center) .maxLines(2) } .layoutWeight(1) .padding({ top: 16, bottom: 16 }) .alignItems(HorizontalAlign.Center) .backgroundColor(#FFFFFF) .borderRadius(12) .shadow({ radius: 4, color: #08000000 }) }, (feature: FeatureCard) feature.featureId.toString()) } .width(100%) .padding({ left: 12, right: 12 }) }, (pageIdx: number) pageIdx.toString()) } .loop(false) .autoPlay(false) .indicatorStyle({ bottom: -4, color: #E0E0E0, selectedColor: #007DFF, size: 6, }) .width(100%) .height(160) } // 知识点说明 Column({ space: 8 }) { Text( Swiper 常用属性) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(#1A1A1A) .width(100%) ForEach([ { prop: autoPlay, desc: 是否自动播放默认 false }, { prop: interval, desc: 自动播放间隔时间ms默认 3000 }, { prop: loop, desc: 是否循环轮播默认 true }, { prop: duration, desc: 切换动画时长ms默认 400 }, { prop: vertical, desc: 是否垂直方向滑动默认 false水平 }, { prop: displayCount, desc: 同时显示的子组件数量默认 1 }, { prop: prevMargin/nextMargin, desc: 设置前后两端的露出量实现卡片效果 }, ], (item: PropDesc) { Row({ space: 10 }) { Text(item.prop) .fontSize(12) .fontColor(#007DFF) .fontWeight(FontWeight.Bold) .padding({ left: 6, right: 6, top: 3, bottom: 3 }) .backgroundColor(#EDF4FF) .borderRadius(4) .flexShrink(0) Text(item.desc) .fontSize(13) .fontColor(#555555) .layoutWeight(1) } .width(100%) .alignItems(VerticalAlign.Center) }, (item:PropDesc) item.prop) } .width(100%) .padding(16) .backgroundColor(#FFFFFF) .borderRadius(12) .margin({ left: 12, right: 12 }) } .padding({ top: 12, bottom: 24 }) } .scrollBar(BarState.Off) .layoutWeight(1) } .width(100%) .height(100%) .backgroundColor(#F5F6FA) } }关键代码解析interface不是摆设很多新手写 ArkUI 页面时会直接在ForEach里使用一堆临时对象。页面小的时候没问题但字段一多就很难知道每个字段到底用于哪里。本案例把数据结构提前声明出来读代码时能很快判断每个 UI 区块依赖哪些字段。后续如果接接口也建议保留这层结构。接口可以变页面模型尽量稳定。这样页面不会因为后端多返回一个字段、少返回一个字段就跟着大改。State是交互的开关本案例的交互反馈都围绕状态展开。点击、切换、输入、刷新这类行为本质上都是修改某个状态值然后让 UI 重新计算显示结果。写这类代码时有一个简单判断如果某个值变化后页面应该立刻变那它大概率应该是状态如果它只是配置项、静态文案、固定颜色就不要放进状态。列表和卡片要靠数据驱动只要页面里出现重复结构就应该优先想到数据数组加ForEach。这样新增一项、删除一项、调整顺序都只动数据不用复制粘贴一段 UI。这个案例的完整代码里已经把主要内容抽成数组或方法。你可以试着多加一条数据看页面是否能自然渲染出来。如果能说明结构是健康的如果加一条就要改很多 UI说明组件拆分还不够。事件回调要短一点.onClick()、onChange()这类回调里可以直接改状态但不建议堆太多逻辑。简单切换可以写在回调里复杂逻辑最好拆成方法。这样做有两个好处一是读页面结构时不会被业务逻辑打断二是后面要加埋点、请求接口、异常处理时有明确的位置可以改。样式参数别急着抽常量这个案例里有不少颜色、间距、圆角、字号。学习阶段直接写在组件链上反而更容易看出效果。等你确定页面风格稳定后再把主题色、通用间距、卡片圆角提到统一常量里。过早抽象会让示例代码变绕。先让页面清楚再谈工程化这是我比较推荐的顺序。总结这个案例真正值得学的不只是某个 ArkUI 组件怎么写而是一个页面如何从数据走到状态再走到布局和交互。你可以按这个顺序改造先替换数据模型再确认状态更新是否正确接着调整布局容器最后打磨样式细节。只要这条主线不乱页面规模变大也不会失控。如果后续继续扩展我会优先把可复用的卡片、列表行、筛选栏或控制区拆成独立组件。这样主页面只保留数据和流程代码会更接近真实项目里的写法。