【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 条件查询࿱…...
C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案
在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…...
Monorepo架构: Nx Cloud 扩展能力与缓存加速
借助 Nx Cloud 实现项目协同与加速构建 1 ) 缓存工作原理分析 在了解了本地缓存和远程缓存之后,我们来探究缓存是如何工作的。以计算文件的哈希串为例,若后续运行任务时文件哈希串未变,系统会直接使用对应的输出和制品文件。 2 …...
