鸿蒙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。

  @Builder
  progressComponent() {
    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 ability
    hilog.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?: Resource
  text?: string
}

//自定义tabBar样式
@Extend(Column)
function tabBarContainerStyle() {
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .backgroundColor(Color.Transparent)
}

@Entry
@Component
struct MultipleImagePage {
  @State selectedTabIndex: number = 0
  private tabBars: TabBar[] = [
    { text: '首页' },
    { text: '朋友' },
    { text: '发布', icon: $r('app.media.add') },
    { text: '消息' },
    { text: '我' }
  ]

  //文字tabBar
  @Builder
  TabBarTextBuilder(tabBarText: string, tabIndex: number) {
    Column() {
      Text(tabBarText)
        .fontColor(Color.White)
        .opacity(
          this.selectedTabIndex === tabIndex ? 1 : 0.6
        )
    }
    .tabBarContainerStyle()
  }

  //中间图片tabBar
  @Builder
  TabBarIconBuilder(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;

  // 进度条
  @Builder
  progressComponent() {
    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 })
  }

  //底部文字
  @Builder
  bottomTextComponent() {
    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)
  }

  //右侧操作栏
  @Builder
  rightOperateComponent() {
    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();
    }
  }
}

四、效果展示

  • 运行应用后,不滑动屏幕时,图片自动轮播,且下方进度条缓慢增长至已完成状态,播放完成时,会继续循环播放。

  • 滑动屏幕时,图片跟随滑动方向而进行切换,此时会关闭自动轮播和循环播放的效果,且下方进度条瞬间增长至已完成状态。

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/948854.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二十八周学习周报

目录 摘要Abstract1 GFPGAN1.1 总体结构1.2 实验研究1.3 代码分析 总结 摘要 本周主要的学习内容是GFPGAN模型。GFPGAN是一种基于生成对抗网络(GAN)的模型&#xff0c;其利用封装在预训练的人脸GAN中的丰富多样的先验进行人脸图像的修复。这种生成面部先验&#xff08;GFP&…

MCP(Model Context Protocol)模型上下文协议 进阶篇3 - 传输

MCP 目前定义了两种标准的客户端-服务端通信传输机制&#xff1a; stdio&#xff08;标准输入输出通信&#xff09;HTTP with Server-Sent Events (SSE)&#xff08;HTTP 服务端发送事件&#xff09; 客户端应尽可能支持 stdio。此外&#xff0c;客户端和服务端也可以以插件方…

NVIDIA DLI课程《NVIDIA NIM入门》——学习笔记

先看老师给的资料&#xff1a; NVIDIA NIM是 NVIDIA AI Enterprise 的一部分&#xff0c;是一套易于使用的预构建容器工具&#xff0c;目的是帮助企业客户在云、数据中心和工作站上安全、可靠地部署高性能的 AI 模型推理。这些预构建的容器支持从开源社区模型到 NVIDIA AI 基础…

物联网云平台:构建物联网生态的核心

我们常说的物联网&#xff0c;简称是IoT&#xff0c; 全称 Internet of Things。 用通俗的语言理解物联网&#xff0c;其实就是万事万物的互联网络。物联网概念也已经传播很多年了&#xff0c; 目前正在各行各业发挥力量。 要构建一个物联网生态&#xff0c; 我们首先想到的是智…

VS2022引入sqlite数据库交互

法一&#xff1a;用官网编译好的动态库(推荐) 下载所需文件 sqlite官网地址&#xff1a;https://www.sqlite.org/howtocompile.html 下载以下的2个压缩包 第一个压缩包 sqlite-amalgamation-xxxx.zip&#xff0c;xxxx是版本号,保持一致即可&#xff0c;这里面有sqite3.h 第…

设计模式学习[15]---适配器模式

文章目录 前言1.引例2.适配器模式2.1 对象适配器2.2 类适配器 总结 前言 这个模式其实在日常生活中有点常见&#xff0c;比如我们的手机取消了 3.5 m m 3.5mm 3.5mm的接口&#xff0c;只留下了一个 T y p e − C Type-C Type−C的接口&#xff0c;但是我现在有一个 3.5 m m 3.…

Markdown如何导出Html文件Markdown文件

Markdown如何导出Html文件Markdown文件 前言语法详解小结其他文章快来试试吧☺️ Markdown 导出 HTML &#x1f448;点击这里也可查看 前言 Markdown的源文件以md为后缀。Markdown是HTML语法的简化版本&#xff0c;它本身不带有任何样式信息。我们所看到的Markdown网页(如&…

Python安装(新手详细版)

前言 第一次接触Python&#xff0c;可能是爬虫或者是信息AI开发的小朋友&#xff0c;都说Python 语言简单&#xff0c;那么多学一些总是有好处的&#xff0c;下面从一个完全不懂的Python 的小白来安装Python 等一系列工作的记录&#xff0c;并且遇到的问题也会写出&#xff0c…

JMeter + Grafana +InfluxDB性能监控 (二)

您可以通过JMeter、Grafana 和 InfluxDB来搭建一个炫酷的基于JMeter测试数据的性能测试监控平台。 下面&#xff0c;笔者详细介绍具体的搭建过程。 安装并配置InfluxDB 您可以从清华大学开源软件镜像站等获得InfluxDB的RPM包&#xff0c;这里笔者下载的是influxdb-1.8.0.x86_…

STL常用容器总结

1.Vector容器特性 vector 容器是一个长度动态改变的动态数组&#xff0c;既然也是数组&#xff0c;那么其内存是一段连续的内存&#xff0c;具有数组的随机存取的优点。 / 1.1.vector特性总结: 1.vector 是动态数组&#xff0c;连续内存空间&#xff0c;具有随机存取效率高的…

BBP飞控板中的坐标系变换

一般飞控板中至少存在以下坐标系&#xff1a; 陀螺Gyro坐标系加速度计Acc坐标系磁强计Mag坐标系飞控板坐标系 在BBP飞控板采用的IMU为同时包含了陀螺&#xff08;Gyro&#xff09;及加速度计&#xff08;Acc&#xff09;的6轴传感器&#xff0c;故Gyro及Acc为同一坐标系。同时…

【OAuth2系列】如何使用OAuth 2.0实现安全授权?详解四种授权方式

作者&#xff1a;后端小肥肠 &#x1f347; 我写过的文章中的相关代码放到了gitee&#xff0c;地址&#xff1a;xfc-fdw-cloud: 公共解决方案 &#x1f34a; 有疑问可私信或评论区联系我。 &#x1f951; 创作不易未经允许严禁转载。 姊妹篇&#xff1a; 【OAuth2系列】集成微…

鸿蒙MPChart图表自定义(六)在图表中绘制游标

在鸿蒙开发中&#xff0c;MPChart 是一个非常强大的图表库&#xff0c;它可以帮助我们创建各种精美的图表。今天&#xff0c;我们将继续探索鸿蒙MPChart的自定义功能&#xff0c;重点介绍如何在图表中绘制游标。 OpenHarmony三方库中心仓 一、效果演示 以下是效果演示图&…

《新概念模拟电路》-电流源电路

电流源电路 本系列文章主要学习《新概念模拟电路》中的知识点。在工作过程中&#xff0c;碰到一些问题&#xff0c;于是又翻阅了模电这本书。我翻阅的是ADI出版的&#xff0c;西安交通大学电工中心杨建国老师编写的模电书。 本文主要是基于前文《新概念模拟电路》-三极管的基础…

Java实现下载excel模板,并实现自定义下拉框

GetMapping("excel/download")ApiOperation(value "模板下载")public void getUserRecordTemplate(HttpServletResponse response, HttpServletRequest request) throws IOException {OutputStream outputStream response.getOutputStream();InputStream…

C 实现植物大战僵尸(四)

C 实现植物大战僵尸&#xff08;四&#xff09; 音频稍卡顿问题&#xff0c;用了 SFML 三方库已优化解决 安装 SFML 资源下载 https://www.sfml-dev.org/download/sfml/2.6.2/ C 实现植物大战僵尸&#xff0c;完结撒花&#xff08;还有个音频稍卡顿的性能问题&#xff0c;待…

回归预测 | MATLAB实现CNN-BiLSTM-Attention多输入单输出回归预测

回归预测 | MATLAB实现CNN-BiLSTM-Attention多输入单输出回归预测 目录 回归预测 | MATLAB实现CNN-BiLSTM-Attention多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 一、方法概述 CNN-BiLSTM-Attention多输入单输出回归预测方法旨在通过融合CNN的局…

Ansible之批量管理服务器

文章目录 背景第一步、安装第二步、配置免密登录2.1 生成密钥2.2 分发公钥2.3 测试无密连接 背景 Ansible是Python强大的服务器批量管理 第一步、安装 首先要拉取epel数据源&#xff0c;执行以下命令 yum -y install epel-release安装完毕如下所示。 使用 yum 命令安装 an…

让css设置的更具有合理性

目录 一、合理性设置宽高 二、避免重叠情况&#xff0c;不要只设置最大宽 三、优先使用弹性布局特性 四、单词、数字换行处理 五、其他编码建议 平常写css时&#xff0c;除了遵循一些 顺序、简化、命名上的规范&#xff0c;让css具有合理性也是重要的一环。 最近的需求场…

【微服务】1、引入;注册中心;OpenFeign

微服务技术学习引入 - 微服务自2016年起搜索指数持续增长&#xff0c;已成为企业开发大型项目的必备技术&#xff0c;中高级java工程师招聘多要求熟悉微服务相关技术。微服务架构介绍 概念&#xff1a;微服务是一种软件架构风格&#xff0c;以专注于单一职责的多个响应项目为基…