鸿蒙HarmonyOS开发:基于Swiper组件和自定义指示器实现多图片进度条轮播功能
文章目录
- 一、概述
- 1、场景介绍
- 2、技术选型
- 二、实现方案
- 1、图片区域实现
- 2、底部导航点设计
- 3、手动切换
- 三、所有代码
- 1、设置沉浸式
- 2、外层Tabs效果
- 3、ImageSwiper组件
- 四、效果展示
一、概述
在短视频平台上,经常可以见到多图片合集。它的特点是:由多张图片组成一个合集,图片可以自动进行轮播,也可以手动去进行图片切换。自动轮播时,图片下方的进度条缓慢加载至完成状态;手动切换时,图片下方的进度条瞬间切换至已完成状态或未完成状态。
由于原生Swiper组件自带的导航点指示器目前只支持数字和圆点的样式,不支持对应的特殊样式,因此需要通过自定义指示器(即进度条)来模拟底部的导航条效果。


1、场景介绍
常见的图文作品,可以自动循环播放和手动切换播放合集中的图片。
-
当作品自动播放时,图片每过几秒会自动切换到下一张,且下方进度条进度与该图片的停留时间匹配。
-
当作品手动播放时,下方进度条会跟着图片的滑动切换而改变成未完成状态或已完成状态。
2、技术选型
从技术角度看,图文作品轮播效果可通过Swiper组件和它的指示器的联动效果实现,由于Swiper组件的指示器无法自定义,所以需要拆开实现:
-
上面图片的轮播部分继续使用Swiper组件实现。
-
下面的指示器,由于Swiper组件只有两种显示模式,一个是圆点,一个是数字,很明显是不能实现进度条的效果。所以需要关闭原生指示器,自定义一个指示器。
二、实现方案
1、图片区域实现
图片区域需要使用Swiper组件来实现。将图片合集的数据传入Swiper组件后,需要对Swiper组件设置一些属性,来完成图片自动轮播效果:
-
通过设置loop属性控制是否循环播放,该属性默认值为true。当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
-
通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。autoPlay为true时,会自动切换播放子组件。
-
通过设置interval属性,控制子组件与子组件之间的播放间隔。interval属性默认值为3000,单位毫秒。
-
通过设置indicator属性为false,来关闭Swiper组件自带的导航点指示器样式。
-
通过设置indicatorInteractive属性为false,来设置禁用组件导航点交互功能。
Swiper(this.swiperController) {LazyForEach(this.data, (item: PhotoData) => {Image($r(`app.media.` + item.id)).width('100%').height('100%').objectFit(ImageFit.Cover)}, (item: PhotoData) => JSON.stringify(item))
}
.width('100%')
.height('100%')
.autoPlay(true)
.indicator(false)
.loop(false)
.indicatorInteractive(false)
.duration(300)
.curve(Curve.Linear)
示意效果如下图所示。

2、底部导航点设计
底部导航点(进度条)有三种样式:未完成状态的样式、已完成状态的样式和正在进行进度增长的样式。
-
使用层叠布局 (Stack),配合Row容器来实现进度条的布局。
-
要实现进度条缓慢增长至完成状态且用时与图片播放时间相匹配的效果,可以给Row容器组件添加属性动画 (animation),设置duration(动画持续时间)与图片播放时间匹配即可。
-
进度条状态切换:通过播放图片的currentIndex与进度条的index进行比较,当currentIndex大于或等于index时,需要将进度条样式设置成已完成状态,否则是未完成状态。可以通过设置完成时进度条的背景颜色为Color.White或Color.Grey来实现两种样式的进度条切换。
创建自定义组件progressComponent。
@BuilderprogressComponent() {Row({ space: 5 }) {ForEach(this.progressData, (item: PhotoData, index: number) => {Stack({ alignContent: Alignment.Start }) {// 底层灰色Row().zIndex(0).width('100%').height(2).borderRadius(2).backgroundColor(Color.Grey)//上层白色Row().zIndex(1).width(this.currentIndex >= index ? '100%' : 0).height(2).borderRadius(2).backgroundColor(Color.White).animation({duration: this.duration - 400,curve: Curve.Linear,iterations: 1,playMode: PlayMode.Normal,onFinish: () => {if (this.currentIndex === this.progressData.length - 1) {this.duration = 400;this.currentIndex = -1;}}})}.layoutWeight(1)}, (item: PhotoData) => JSON.stringify(item))}.width('100%').height(40)}
上述代码中,this.progressData为图片集合的数组,this.currentIndex为当前播放的图片在图片集合数组中的索引,index为进度条对应的图片在图片集合数组中的索引。当this.currentIndex >= index时,表示图片集合数组中索引0-index的进度条都是已完成状态。
示意效果如下图所示。

3、手动切换
当图片集合手动播放时,随着图片的切换,下方进度条会跟随着切换为已完成状态或未完成状态。此时,开发者需要给Swiper组件添加onGestureSwipe事件,来判断页面是否跟手滑动。
Swiper(this.swiperController) {// ...
}
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {this.slide = true;
})
slide为布尔值,用来判断页面是否跟手滑动。默认值为false,当页面跟手滑动时,slide的值为true。
然后根据slide是否为手动滑动来判断:是否循环播放,是否自动轮播,进度条动画效果等功能。
三、所有代码
-
外层包个Tabs实现仿抖音效果。
-
上面图片的轮播部分使用Swiper组件实现。
-
下面的指示器,需要关闭原生指示器,自定义指示器(进度条)来实现。
-
设置窗口的布局为沉浸式布局,设置状态栏文字颜色为白色。
1、设置沉浸式
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {// Main window is created, set main page for this abilityhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');// 获取该WindowStage实例下的主窗口。const mainWindow = windowStage.getMainWindowSync();// 设置主窗口的布局是否为沉浸式布局。mainWindow.setWindowLayoutFullScreen(true).then(() => {hilog.info(0x0000, 'testTag', 'Succeeded in setting the window layout to full-screen mode');}).catch((err: BusinessError) => {hilog.info(0x0000, 'testTag', 'Failed to set the window layout to full-screen mode. Cause: %{public}s', JSON.stringify(err) ?? '');})// 状态栏文字颜色。const sysBarProps: window.SystemBarProperties = {statusBarContentColor: '#ffffff'};// 设置主窗口三键导航栏、状态栏的属性。mainWindow.setWindowSystemBarProperties(sysBarProps).then(() => {hilog.info(0x0000, 'testTag', 'Succeeded in setting the system bar properties');}).catch((err: BusinessError) => {hilog.info(0x0000, 'testTag', 'Failed to set system bar properties. Cause: %{public}s', JSON.stringify(err) ?? '');})// ........
}
2、外层Tabs效果
其他文章有详细介绍Tabs效果的具体实现。此处不再赘述。
// MultipleImagePage.ets
import { ImageSwiper } from "./ImageSwiper";interface TabBar {icon?: Resourcetext?: string
}//自定义tabBar样式
@Extend(Column)
function tabBarContainerStyle() {.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor(Color.Transparent)
}@Entry
@Component
struct MultipleImagePage {@State selectedTabIndex: number = 0private tabBars: TabBar[] = [{ text: '首页' },{ text: '朋友' },{ text: '发布', icon: $r('app.media.add') },{ text: '消息' },{ text: '我' }]//文字tabBar@BuilderTabBarTextBuilder(tabBarText: string, tabIndex: number) {Column() {Text(tabBarText).fontColor(Color.White).opacity(this.selectedTabIndex === tabIndex ? 1 : 0.6)}.tabBarContainerStyle()}//中间图片tabBar@BuilderTabBarIconBuilder(icon: Resource) {Column() {Image(icon).width(36).margin({bottom:8})}.tabBarContainerStyle()}build() {Tabs({ barPosition: BarPosition.End }) {ForEach(this.tabBars, (tabBar: TabBar, index) => {TabContent() {if (index === 0) {ImageSwiper()} else {Column() {Text(tabBar.text).fontColor(Color.White).fontSize(40)}.width('100%').height('100%').alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center).backgroundColor(Color.Black)}}.tabBar(tabBar.icon ?this.TabBarIconBuilder(tabBar.icon) :this.TabBarTextBuilder(tabBar.text, index))}, (tabBar: TabBar) => JSON.stringify(tabBar))}.barOverlap(true) // 设置TabBar是否背后变模糊并叠加在TabContent之上。.barHeight(56) // 设置TabBar的高度值。.backgroundColor(Color.Transparent).barBackgroundColor(Color.Transparent) // 设置TabBar的背景颜色。.barBackgroundBlurStyle(BlurStyle.NONE) // 设置TabBar的背景模糊材质。关闭模糊.divider({ strokeWidth: 1, color: 'rgba(255, 255, 255, 0.20)' }) // 设置区分TabBar和TabContent的分割线样式。.onChange((index: number) => {this.selectedTabIndex = index}).hitTestBehavior(HitTestMode.Transparent) // 设置组件的触摸测试类型。自身和子节点都响应触摸测试,不会阻塞兄弟节点的触摸测试。不会影响祖先节点的触摸测试。}
}
3、ImageSwiper组件
import { DataSource, PhotoData } from '../model/ImageData'
import { OperateButton } from "./OperateButton";@Extend(Text)
function videoInfoStyle() {.fontSize(14).fontColor('rgba(255, 255, 255, 0.80)')
}@Component
export struct ImageSwiper {private swiperController: SwiperController = new SwiperController();@State progressData: PhotoData[] = [];@State data: DataSource = new DataSource([]);@State currentIndex: number = -1;@State duration: number = 3000;@State slide: boolean = false;// 进度条@BuilderprogressComponent() {Row({ space: 5 }) {ForEach(this.progressData, (item: PhotoData, index: number) => {Stack({ alignContent: Alignment.Start }) {Row().zIndex(0).width('100%').height(2).borderRadius(2).backgroundColor(Color.Grey)Row().zIndex(1).width(this.currentIndex >= index && !this.slide ? '100%' : 0).height(2).borderRadius(2).backgroundColor(Color.White).animation(!this.slide ? {duration: this.duration - 400,curve: Curve.Linear,iterations: 1,playMode: PlayMode.Normal,onFinish: () => {if (this.currentIndex === this.progressData.length - 1) {this.duration = 400;this.currentIndex = -1;}}} : { duration: 0 })Row().zIndex(2).width(this.currentIndex >= index && this.slide ? '100%' : 0).height(2).borderRadius(2).backgroundColor(Color.White)}.layoutWeight(1)}, (item: PhotoData) => JSON.stringify(item))}.width('100%').height(40).margin({ bottom: 60 })}//底部文字@BuilderbottomTextComponent() {Column({ space: 15 }) {Row({ space: 10 }) {Text('@山猫').fontColor(Color.White)Text('2024-12-23 14:52').videoInfoStyle()}Text('海的那边,是迷雾中的诗,是浪尖上的歌,还是梦里的远方').videoInfoStyle()}.padding(16).width('80%').alignItems(HorizontalAlign.Start).margin({ right: '20%', bottom: 100 }).hitTestBehavior(HitTestMode.Transparent)}//右侧操作栏@BuilderrightOperateComponent() {Column({ space: 20 }) {OperateButton({head: $r('app.media.user'),likeCount: 123,commentCount: 234,collectCount: 345,shareCount: 456})}.width('20%').padding(16).margin({ bottom: 100 })}//轮播数据创建aboutToAppear() {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);}build() {Stack({ alignContent: Alignment.BottomEnd }) {//轮播图片Swiper(this.swiperController) {LazyForEach(this.data, (item: PhotoData) => {Image($r(`app.media.` + item.id)).width('100%').height('100%').objectFit(ImageFit.Cover)}, (item: PhotoData) => JSON.stringify(item))}.width('100%').height('100%').autoPlay(!this.slide ? true : false).indicator(false).loop(!this.slide ? true : false).indicatorInteractive(false).duration(400).curve(Curve.Linear).onChange((index) => {this.duration = 3000;this.currentIndex = index;}).onAppear(() => {this.currentIndex = 0;}).onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {this.slide = true;})// 底部文字描述this.bottomTextComponent();// 右侧操作栏this.rightOperateComponent()// 进度条this.progressComponent();}}
}
四、效果展示
-
运行应用后,不滑动屏幕时,图片自动轮播,且下方进度条缓慢增长至已完成状态,播放完成时,会继续循环播放。
-
滑动屏幕时,图片跟随滑动方向而进行切换,此时会关闭自动轮播和循环播放的效果,且下方进度条瞬间增长至已完成状态。


相关文章:
鸿蒙HarmonyOS开发:基于Swiper组件和自定义指示器实现多图片进度条轮播功能
文章目录 一、概述1、场景介绍2、技术选型 二、实现方案1、图片区域实现2、底部导航点设计3、手动切换 三、所有代码1、设置沉浸式2、外层Tabs效果3、ImageSwiper组件 四、效果展示 一、概述 在短视频平台上,经常可以见到多图片合集。它的特点是:由多张…...
Excel 身份证号计算年龄
1. 设置身份证号列格式 复制身份证列值到记事本或其他地方重新设置身份证号列单元格格式为“文本”将复制出去的身份证号重新复制粘贴回来 2. 年龄列单元格中添加公式 DATEDIF(DATE(LEFT(MID(A2, 7, 8), 4), MID(MID(A2, 7, 8), 5, 2), RIGHT(MID(A2, 7, 8), 2)), TODAY(), …...
【2024年-6月-14日-开源社区openEuler实践记录】探索 test - tools:高效测试的开源宝库
开篇引言 大家好,我是 fzr123,在软件开发领域深耕多年,一直致力于探索各种提升效率的工具与技术。今天,我将为大家深入介绍一款在测试领域极具价值的开源项目——test - tools,它为开发者们提供了一系列强大的测试功能…...
2022浙江大学信号与系统笔记
原视频地址:2022浙江大学信号与系统(含配套课件和代码) - 胡浩基老师-哔哩哔哩 ⭐⭐⭐ 我的笔记:飞书链接 - 信号与系统 基于视频,记得笔记,加了点自己的补充(有的是问 ChatGPT 的)…...
DeepSeek-VL2
《DeepSeek-VL2: Mixture-of-Experts Vision-Language Models for Advanced Multimodal Understanding》是 DeepSeek-AI 团队发布的关于视觉语言模型 DeepSeek-VL2 的论文,以下是对该论文的详细介绍: 研究背景与动机 多模态理解的重要性:在当…...
前端⾯试⼋股⽂
1.http 和 https 的基本概念 - http: 是⼀个客⼾端和服务器端请求和应答的标准(TCP),⽤于从 WWW 服务器传输超⽂本到本地浏 览器的超⽂本传输协议。 - https:是以安全为⽬标的 HTTP 通道,即 HTTP 下 加⼊ SSL 层进⾏加密。其作⽤…...
【Rust自学】8.6. HashMap Pt.2:更新HashMap
8.6.0. 本章内容 第八章主要讲的是Rust中常见的集合。Rust中提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。 第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编…...
Python异常处理详解:概念、语法与实践
1. 异常的概念 在Python中,异常(Exception)是程序运行时出现的错误或不正常情况。异常通常表示程序在运行时遇到了无法继续执行的条件。Python通过 try/except 语句来捕获和处理异常。 异常可以分为两类: 内建异常:…...
Kotlin在医疗大健康域的应用实例探究与编程剖析(上)
一、引言 1.1 研究背景与意义 在当今数字化时代,医疗行业正经历着深刻的变革。随着信息技术的飞速发展,尤其是人工智能、大数据、物联网等新兴技术的广泛应用,医疗行业数字化转型已成为必然趋势。这种转型旨在提升医疗服务的效率和质量,优化医疗资源配置,为患者提供更加…...
QT----------QT Data Visualzation
实现思路: 配置项目:在 .pro 文件中添加 QT datavisualization 以引入 QT Data Visualization 模块。创建主窗口:使用 QMainWindow 作为主窗口,添加 Q3DScatter、Q3DBars 和 Q3DSurface 等三维视图组件。初始化和创建三维图表&a…...
什么是Sight Words(信号词)
🧡什么是Sight Words(信号词) 简单来说,Sight Words就是我们在日常英语中常用的一些基本词汇。可以把它想象成是学练英语的“基础词汇”,这些词在各种考试中经常出现,也是在生活中必不可少的。 …...
SpringBoot日志快速集成详解-生产实战
SpringBoot日志快速集成详解 1. 添加依赖2. 创建 logback-spring.xml 配置文件示例 logback-spring.xml 配置: 3. 启用 Spring Boot 自动配置4. 配置 Spring Boot 启动日志级别5. 运行与验证 博文专注于最快速的实战,没有那么多逼逼叨叨的理论࿰…...
路由技术在网络中的作用及特点
作用:路径选择:在复杂的网络拓扑结构中,路由技术能够根据网络的当前状态和目标地址,为数据报文选择一条最佳的传输路径,确保数据能够快速、准确地到达目的地。例如,在互联网中,当用户访问一个网…...
【Python系列】Flask 与 FastAPI:两个 Python Web 框架的对比分析
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
云手机:虚拟技术的革命性应用与实体手机的优劣对比
在近年来,随着互联网的火速发展,云手机作为一种新兴的技术产品,在游戏行业特别是手游市场中掀起了一股热潮。云手机,顾名思义,是架设在ARM虚拟服务器上的手机,其独特的虚拟技术为用户带来了前所未有的使用体…...
3. C语言 数据类型
本章目录: 前言:C语言中的数据类型分类1. 基本数据类型1.1 整数类型1.2 浮点类型1.3 字符型常量1.4 字符串常量 2. 枚举类型3. void 类型void类型的使用示例: 4. 类型转换4.1 隐式类型转换4.2 显式类型转换类型转换的注意事项 5. 小结 前言&a…...
npm install 安装选项 -d -s -g
在使用 npm install 时,-d、-g 和 -s 是不同的选项,它们分别代表不同的安装模式或行为。以下是它们的详细解释: 1. -d:--save-dev 含义:将包安装为开发依赖(devDependencies)。使用场景&#…...
pdf预览兼容问题- chrome浏览器105及一下预览不了
使用的"tato30/vue-pdf": "^1.11.2"预览插件,发现chrome浏览器105及一下预览不了 pdfPreview预览组件: <template><div id"vue_pdf_view"><div class"tool_tip"><template v-if"pa…...
【可实战】需求分析-测试计划↓-测试设计-测试执行-测试总结↓(包含测试计划、测试总结模板,以公司要求为准)
一、完成软件测试工作的必要步骤 需求分析-测试计划-测试设计-测试执行-测试总结 二、测试计划 (一)测试计划模版 模板在线查看: https://docs.qq.com/doc/DV2hTamxJWnNDaUFF 模板(百度网盘): 链接&…...
MySQL 03 章——基本的SELECT语句
一、SQL概述 (1)SQL背景知识 SQL(Structured Query Language,结构化查询语言)是使用关系模型的数据库应用语言,与数据直接打交道不同的数据库管理系统生产厂商都支持SQL语句,但都有特有内容 …...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
tomcat入门
1 tomcat 是什么 apache开发的web服务器可以为java web程序提供运行环境tomcat是一款高效,稳定,易于使用的web服务器tomcathttp服务器Servlet服务器 2 tomcat 目录介绍 -bin #存放tomcat的脚本 -conf #存放tomcat的配置文件 ---catalina.policy #to…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
