【HarmonyOS Next】自定义Tabs
背景
项目中Tabs的使用可以说是特别的频繁,但是官方提供的Tabs使用起来,存在tab选项卡切换动画滞后的问题。

原始动画无法满足产品的UI需求,因此,这篇文章将实现下面页面滑动,tab选项卡实时滑动的动画效果。

实现逻辑
需求讲解
- 需要实现固定宽度下,放下6个选项卡。
- 在没有选择时宽度均匀分配,选中时显示图标并且增加宽度。
- 实现下方内容区域滑动时,上面选项卡实时跳动。
- 实现动画效果,使整体操作更加流畅。
实现思路
1. 选项卡
- 选项卡使用Row布局组件+layoutWeight属性,来实现平均布局。通过选项卡的选择索引来实现是否选中判断。
- 选中时,layoutWeight值为1.5;没有选中时,layoutWeight值为1.
- 使用animation属性,只要layoutWeight值变化时,可以触发动画。
- 在外包裹的布局容器中,添加onAreaChange事件,用来计算整体Tab组件的宽度。
Row() {Text(name).fontSize(16).fontWeight(this.SelectedTabIndex == index ? FontWeight.Bold : FontWeight.Normal).textAlign(TextAlign.Center).animation({ duration: 300 })Image($r('app.media.send')).width(14).height(14).margin({ left: 2 }).visibility(this.SelectedTabIndex == index ? Visibility.Visible : Visibility.None).animation({ duration: 300 })}.justifyContent(FlexAlign.Center).layoutWeight(this.SelectedTabIndex == index ? 1.5 : 1).animation({ duration: 300 })
2. 定位器
- 使用Rect定义背景的形状和颜色+Stack布局+position属性,实现定位器的移动。
- position属性中通过Left值的变化来实现Rect的移动。但是在swiper的滑动中会出现滑动一点然后松开的情况,因此,需要两个值同时在实现中间的移动过程。
Stack() {Rect().height(30).stroke(Color.Black).radius(10).width(this.FirstWidth).fill("#bff9f2").position({left: this.IndicatorLeftOffset + this.IndicatorOffset,bottom: 0}).animation({ duration: 300, curve: Curve.LinearOutSlowIn })}.width("100%").alignRules({center: { anchor: "Tabs", align: VerticalAlign.Center }})
3.主要内容区
- 使用Swiper组件加载对应的组件,这里需要注意的是,Demo没有考虑到内容比较多的优化方案,可以设置懒加载方案来实现性能的提升。
- onAnimationStart事件,实现监测控件是向左移动还是向右移动,并且修改IndicatorLeftOffset偏移值。
- onAnimationEnd事件,将中间移动过程值IndicatorOffset恢复成0。
- onGestureSwipe事件,监测组件的实时滑动,这个事件在onAnimationStart和onAnimationEnd事件之前执行,执行完后才会执行onAnimationStart事件。因此,这个方法需要实时修改定位器的偏移数值。
- 偏移数值是通过swiper的移动数值和整体宽度的比例方式进行计算,松手后的偏移方向,由onAnimationStart和onAnimationEnd事件来确定最终的距离
Swiper(this.SwiperController) {ForEach(this.TabNames, (name: string, index: number) => {Column() {Text(`${name} - ${index}`).fontSize(24).fontWeight(FontWeight.Bold)}.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center).height("100%").width("100%")})}.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {if (targetIndex > index) {this.IndicatorLeftOffset += this.OtherWidth;} else if (targetIndex < index) {this.IndicatorLeftOffset -= this.OtherWidth;}this.IndicatorOffset = 0this.SelectedTabIndex = targetIndex}).onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {this.IndicatorOffset = 0}).onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {let move: number = this.GetOffset(extraInfo.currentOffset);//这里需要限制边缘情况if ((this.SelectedTabIndex == 0 && extraInfo.currentOffset > 0) ||(this.SelectedTabIndex == this.TabNames.length - 1 && extraInfo.currentOffset < 0)) {return;}this.IndicatorOffset = extraInfo.currentOffset < 0 ? move : -move;}).onAreaChange((oldValue: Area, newValue: Area) => {let width = newValue.width.valueOf() as number;this.SwiperWidth = width;}).curve(Curve.LinearOutSlowIn).loop(false).indicator(false).width("100%").id("MainContext").alignRules({top: { anchor: "Tabs", align: VerticalAlign.Bottom },bottom: { anchor: "__container__", align: VerticalAlign.Bottom }})
代码文件
- 里面涉及到资源的小图标,可以自己区定义的,文章就不提供了。
@Entry
@ComponentV2
struct Index {/*** 标头名称集合*/@Local TabNames: string[] = ["飞机", "铁路", "自驾", "地铁", "公交", "骑行"]/*** Tab选择索引*/@Local SelectedTabIndex: number = 0/*** 标点移动距离*/@Local IndicatorLeftOffset: number = 0/*** 标点在swiper的带动下移动的距离*/@Local IndicatorOffset: number = 0/*** 第一个宽度*/@Local FirstWidth: number = -1/*** 其他的宽度*/@Local OtherWidth: number = -1/*** Swiper控制器*/@Local SwiperController: SwiperController = new SwiperController()/*** Swiper容器宽度*/@Local SwiperWidth: number = 0build() {RelativeContainer() {Stack() {Rect().height(30).stroke(Color.Black).radius(10).width(this.FirstWidth).fill("#bff9f2").position({left: this.IndicatorLeftOffset + this.IndicatorOffset,bottom: 0}).animation({ duration: 300, curve: Curve.LinearOutSlowIn })}.width("100%").alignRules({center: { anchor: "Tabs", align: VerticalAlign.Center }})Row() {ForEach(this.TabNames, (name: string, index: number) => {Row() {Text(name).fontSize(16).fontWeight(this.SelectedTabIndex == index ? FontWeight.Bold : FontWeight.Normal).textAlign(TextAlign.Center).animation({ duration: 300 })Image($r('app.media.send')).width(14).height(14).margin({ left: 2 }).visibility(this.SelectedTabIndex == index ? Visibility.Visible : Visibility.None).animation({ duration: 300 })}.justifyContent(FlexAlign.Center).layoutWeight(this.SelectedTabIndex == index ? 1.5 : 1).animation({ duration: 300 }).onClick(() => {this.SelectedTabIndex = index;this.SwiperController.changeIndex(index, false);animateTo({ duration: 500, curve: Curve.LinearOutSlowIn }, () => {this.IndicatorLeftOffset = this.OtherWidth * index;})})})}.width("100%").height(30).id("Tabs").onAreaChange((oldValue: Area, newValue: Area) => {let tabWidth = newValue.width.valueOf() as number;this.FirstWidth = 1.5 * tabWidth / (this.TabNames.length + 0.5);this.OtherWidth = tabWidth / (this.TabNames.length + 0.5);})Swiper(this.SwiperController) {ForEach(this.TabNames, (name: string, index: number) => {Column() {Text(`${name} - ${index}`).fontSize(24).fontWeight(FontWeight.Bold)}.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center).height("100%").width("100%")})}.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {if (targetIndex > index) {this.IndicatorLeftOffset += this.OtherWidth;} else if (targetIndex < index) {this.IndicatorLeftOffset -= this.OtherWidth;}this.IndicatorOffset = 0this.SelectedTabIndex = targetIndex}).onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {this.IndicatorOffset = 0}).onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {let move: number = this.GetOffset(extraInfo.currentOffset);//这里需要限制边缘情况if ((this.SelectedTabIndex == 0 && extraInfo.currentOffset > 0) ||(this.SelectedTabIndex == this.TabNames.length - 1 && extraInfo.currentOffset < 0)) {return;}this.IndicatorOffset = extraInfo.currentOffset < 0 ? move : -move;}).onAreaChange((oldValue: Area, newValue: Area) => {let width = newValue.width.valueOf() as number;this.SwiperWidth = width;}).curve(Curve.LinearOutSlowIn).loop(false).indicator(false).width("100%").id("MainContext").alignRules({top: { anchor: "Tabs", align: VerticalAlign.Bottom },bottom: { anchor: "__container__", align: VerticalAlign.Bottom }})}.height('100%').width('100%').padding(10)}/*** 需要注意的点,当前方法仅计算偏移值,不带方向* @param swiperOffset* @returns*/GetOffset(swiperOffset: number): number {let swiperMoveRatio: number = Math.abs(swiperOffset / this.SwiperWidth);let tabMoveValue: number = swiperMoveRatio >= 1 ? this.OtherWidth : this.OtherWidth * swiperMoveRatio;return tabMoveValue;}
}
总结
这里实现了新的Tab选项卡的定义。但是没有进行高度封装,想法是方便读者理解组件的使用逻辑,而不是直接提供给读者进行调用。希望这篇文章可以帮助到你~~
相关文章:
【HarmonyOS Next】自定义Tabs
背景 项目中Tabs的使用可以说是特别的频繁,但是官方提供的Tabs使用起来,存在tab选项卡切换动画滞后的问题。 原始动画无法满足产品的UI需求,因此,这篇文章将实现下面页面滑动,tab选项卡实时滑动的动画效果。 实现逻…...
Sass 模块化革命:深入解析 @use 语法,打造高效 CSS 架构
文章目录 前言use 用法1. 模块化与命名空间2. use 中 as 语法的使用3. as * 语法的使用4. 私有成员的访问5. use 中with默认值6. use 导入问题总结下一篇预告: 前言 在上一篇中,我们深入探讨了 Sass 中 import 语法的局限性,正是因为这些问题…...
【渗透测试】反弹 Shell 技术详解(一)
反弹 Shell 技术详解 一、前置知识 反弹 shell(Reverse Shell)是一种技术,攻击者利用它可以在远程主机上获得一个交互式的命令行接口。通常情况下,反弹 shell 会将标准输入(stdin)、标准输出(…...
python:pymunk + pygame 模拟六边形中小球弹跳运动
向 chat.deepseek.com 提问:编写 python 程序,用 pymunk, 有一个正六边形,围绕中心点缓慢旋转,六边形内有一个小球,六边形的6条边作为墙壁,小球受重力和摩擦力、弹力影响,模拟小球弹跳运动&…...
Windows 图形显示驱动开发-WDDM 3.2-本机 GPU 围栏对象(二)
GPU 和 CPU 之间的同步 CPU 必须执行 MonitoredValue 的更新,并读取 CurrentValue,以确保不会丢失正在进行的信号中断通知。 当向系统中添加新的 CPU 等待程序时,或者如果现有的 CPU 等待程序失效时,OS 必须修改受监视的值。OS …...
23种设计模式之《模板方法模式(Template Method)》在c#中的应用及理解
程序设计中的主要设计模式通常分为三大类,共23种: 1. 创建型模式(Creational Patterns) 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。 工厂方法模式࿰…...
DEV-C++ 为什么不能调试?(正确解决方案)
为了备战pat考试,专门下载了DEV C,然后懵圈的发现,怎么无法调试(╯□)╯︵ ┻━┻ 然后整了半天,终于在网上找到相应的解决方案!!!-> Dev C 5.11 调试初始设置 <- 一共四步…...
【C++设计模式】第五篇:原型模式(Prototype)
注意:复现代码时,确保 VS2022 使用 C17/20 标准以支持现代特性。 克隆对象的效率革命 1. 模式定义与用途 核心思想 原型模式:通过复制现有对象(原型)来创建新对象,而非通过new构造。关键用…...
深入 Vue.js 组件开发:从基础到实践
深入 Vue.js 组件开发:从基础到实践 Vue.js 作为一款卓越的前端框架,其组件化开发模式为构建高效、可维护的用户界面提供了强大支持。在这篇博客中,我们将深入探讨 Vue.js 组件开发的各个方面,从基础概念到高级技巧,助…...
maven导入spring框架
在eclipse导入maven项目, 在pom.xml文件中加入以下内容 junit junit 3.8.1 test org.springframework spring-core ${org.springframework.version} org.springframework spring-beans ${org.springframework.version} org.springframework spring-context ${org.s…...
数据守护者:备份文件的重要性与自动化实践策略
在数字化浪潮席卷全球的今天,数据已成为企业运营和个人生活中不可或缺的核心资源。无论是企业的财务报表、客户资料,还是个人的家庭照片、工作文档,这些数据都承载着巨大的价值。然而,数据丢失的风险无处不在,硬件故障…...
MyBatis @Param 注解详解:指定的参数找不到?
MyBatis Param 注解详解 1. Param 注解的作用 Param 注解用于显式指定方法参数的名称,让 MyBatis 在 SQL 映射文件(XML)或注解中通过该名称访问参数。 核心场景: 方法有多个参数时,避免参数名丢失或混淆。参数为简单…...
【项目日记(八)】内存回收与联调
前言 我们前面实现了三层缓存申请的过程,并完成了三层缓存申请过程的联调!本期我们来介绍三层的缓存的回收机制以及三层整体联调释放的过程。 目录 前言 一、thread cache 回收内存 二、central cache 回收内存 • 如何确定一个对象对应的span • …...
性能测试监控工具jmeter+grafana
1、什么是性能测试监控体系? 为什么要有监控体系? 原因: 1、项目-日益复杂(内部除了代码外,还有中间件,数据库) 2、一个系统,背后可能有多个软/硬件组合支撑,影响性能的因…...
016.3月夏令营:数理类
016.3月夏令营:数理类: 中国人民大学统计学院: http://www.eeban.com/forum.php?modviewthread&tid386109 北京大学化学学院第一轮: http://www.eeban.com/forum.php?m ... 6026&extrapage%3D1 香港大学化学系夏令营&a…...
CS144 Lab Checkpoint 0: networking warm up
Set up GNU/Linux on your computer 我用的是Ubuntu,按照指导书上写的输入如下命令安装所需的软件包: sudo apt update && sudo apt install git cmake gdb build-essential clang \ clang-tidy clang-format gcc-doc pkg-config glibc-doc tc…...
靶场之路-VulnHub-DC-6 nmap提权、kali爆破、shell反连
靶场之路-VulnHub-DC-6 一、信息收集 1、扫描靶机ip 2、指纹扫描 这里扫的我有点懵,这里只有两个端口,感觉是要扫扫目录了 nmap -sS -sV 192.168.122.128 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4p1 Debian 10deb9u6 (protoc…...
给没有登录认证的web应用添加登录认证(openresty lua实现)
这阵子不是deepseek火么?我也折腾了下本地部署,ollama、vllm、llama.cpp都弄了下,webui也用了几个,发现nextjs-ollama-llm-ui小巧方便,挺适合个人使用的。如果放在网上供多人使用的话,得接入登录认证才好&a…...
3月5日作业
代码作业: #!/bin/bash# 清空目录函数 safe_clear_dir() {local dir"$1"local name"$2"if [ -d "$dir" ]; thenwhile true; doread -p "检测到 $name 目录已存在,请选择操作: 1) 清空目录内容 2) 保留目…...
【MySQL】增删改查
目录 一、新增(Create) 单行数据 全列插入 多行数据 指定列插入 插入时间 二、查询(Retrieve) 全列查询 指定列查询 查询字段为表达式 别名 去重:DISTINCT 排序:ORDER BY 条件查询࿱…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
