【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选项卡的定义。但是没有进行高度封装,想法是方便读者理解组件的使用逻辑,而不是直接提供给读者进行调用。希望这篇文章可以帮助到你~~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RMSNorm模块

目录 代码代码解释1. 初始化方法 __init__2. 前向传播方法 forward3. 总结4. 使用场景 可视化 代码 class RMSNorm(torch.nn.Module):def __init__(self, dim: int, eps: float):super().__init__()self.eps epsself.weight nn.Parameter(torch.ones(dim))def forward(self,…

【USRP】NVIDIA Sionna:用于 6G 物理层研究的开源库

目录 Sionna&#xff1a;用于 6G 物理层研究的开源库主要特点实现6G研究的民主化支持 5G、6G 等模块化、可扩展、可伸缩快速启动您的研究 好处原生人工智能支持综合研究平台开放生态系统 安装笔记使用 pip 安装基于Docker的安装从源代码安装“你好世界&#xff01;”探索锡奥纳…

大模型开发(四):PET项目——新零售决策评价系统(上)

PET项目——新零售决策评价系统&#xff08;上&#xff09; 0 前言1 项目介绍1.1 PET简介1.2 项目背景1.3 项目结构1.4 硬件配置 2 数据处理2.1 数据介绍2.2 提示词模板与标签映射2.3 BERT模型的输入格式2.4 硬模板类2.5 函数式编程2.6 datasets模块主要功能&#xff1a;在本项…

C语⾔数据类型和变量

C 语言的数据类型 类型分类&#xff1a; C 语言提供丰富的数据类型&#xff0c;包括字符型&#xff08;char、signed char、unsigned char&#xff09;、整型&#xff08;short、int、long 等多种&#xff0c;且各有 signed 和 unsigned 修饰形式&#xff09; 、浮点型&#x…

yum源选要配置华为云的源,阿里云用不了的情况

curl -O /etc/yum.repos.d/CentOS-Base.repo https://repo.huaweicloud.com/repository/conf/CentOS-7-reg.repo

JDBC连接数据库(MySQL)教程(包含可能出错的问题)

阅读提示&#xff1a;这篇文章关于Mysql的知识涉及到的不是很多&#xff0c;如果有需要我改天专门写一篇详细的关于mysql的文章&#xff0c;当然点进来的人大部分肯定是了解过mysql的。 一、准备工作&#xff08;驱动包&#xff09; 1.1 下载IntelliJ IDEA&#xff08;主要用…

详细分析KeepAlive的基本知识 并缓存路由(附Demo)

目录 前言1. 基本知识2. Demo2.1 基本2.2 拓展2.3 终极 3. 实战 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 基本知识推荐阅读&#xff1a;KeepAlive知识点 从实战中学习&#xff0c;源自实战中vue路由的…

AI编程,常见的AI编程工具有哪些?如何用AI编程做一个简单的小软件?

随着AI的快速发展&#xff0c;编程不再是专业程序员的专属技能&#xff0c;而逐渐成为一种普通人也能掌握的工具。 如今&#xff0c;即使没有编程基础&#xff0c;也可以通过几种方式轻松入门AI编程&#xff0c;包括直接使用大语言模型进行编程、借助特定的AI软件进行可视化编程…

探秘 Linux 系统编程:进程地址空间的奇妙世界

亲爱的读者朋友们&#x1f603;&#xff0c;此文开启知识盛宴与思想碰撞&#x1f389;。 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 在 Linux 系统编程的领域里&#xff0c;进程地址空间可是个相当重要的…

2025-03-04 学习记录--C/C++-PTA 习题5-5 使用函数统计指定数字的个数

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 二、代码&#xff08;C语言&#xff09;⭐️ #include <stdio.h>int CountDigit( int number, int di…

25年第四本【认知觉醒】

《认知觉醒》&#xff1a;一场与大脑的深度谈判 在信息爆炸的焦虑时代&#xff0c;我们像被抛入湍流的溺水者&#xff0c;拼命抓取各种自我提升的浮木&#xff0c;却在知识的漩涡中越陷越深。这不是一本简单的成功学指南&#xff0c;而是一场关于人类认知系统的深度对话&#…

汽车视频智能包装创作解决方案,让旅途记忆一键升级为影视级大片

在智能汽车时代&#xff0c;行车记录已不再是简单的影像留存&#xff0c;而是承载情感与创意的载体。美摄科技依托20余年视音频领域技术积累&#xff0c;推出汽车视频智能包装创作解决方案&#xff0c;以AI驱动影像处理与艺术创作&#xff0c;重新定义车载视频体验&#xff0c;…

DeepSeek 智慧城市应用:交通流量预测(918)

**摘要&#xff1a;**本文探讨了利用 DeepSeek 技术框架解决城市交通流量预测问题的方法&#xff0c;主要内容包括基于时空图卷积网络&#xff08;ST - GCN&#xff09;的预测模型、多传感器数据融合策略以及实时推理 API 服务的搭建&#xff0c;旨在为智慧城市的交通管理提供高…

如何在随机振动分析中包括缓冲器

总结 在随机振动分析中&#xff0c;准确模拟系统的动态行为对于预测其在随机激励下的响应至关重要。在这种情况下&#xff0c;分立阻尼器&#xff08;如减振器&#xff09;是必不可少的组件&#xff0c;因为它有助于模拟实际系统中的能量耗散机制。通过将离散阻尼器集成到模型…

python3.13安装教程【2025】python3.13超详细图文教程(包含安装包)

文章目录 前言一、python3.13安装包下载二、Python 3.13安装步骤三、Python3.13验证 前言 本教程将为你详细介绍 Python 3.13 python3.13安装教程&#xff0c;帮助你顺利搭建起 Python 3.13 开发环境&#xff0c;快速投身于 Python 编程的精彩实践中。 一、python3.13安装包下…

Transformer 代码剖析6 - 位置编码 (pytorch实现)

一、位置编码的数学原理与设计思想 1.1 核心公式解析 位置编码采用正弦余弦交替编码方案&#xff1a; P E ( p o s , 2 i ) sin ⁡ ( p o s 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i 1 ) cos ⁡ ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos,2i)} \sin\left(\fra…

CF 452A.Eevee(Java实现)

题目分析 输入一个数字-长度&#xff0c;输入一个字符串。判断这个字符串是具体的哪一个单词 思路分析 首先给了长度&#xff0c;那我先判断长度相同的单词&#xff0c;然后再一一对比&#xff0c;如果都能通过&#xff0c;那就输出这个单词 代码 import java.util.*;public …

【监控】使用Prometheus+Grafana搭建服务器运维监控面板(含带BearerToken的Exporter配置)

【监控】使用PrometheusGrafana搭建服务器运维监控面板&#xff08;含带BearerToken的Exporter配置&#xff09; 文章目录 1、Grafana 数据可视化面板2、Prometheus - 收集和存储指标数据3、Exporter - 采集和上报指标数据 1、Grafana 数据可视化面板 Grafana 是一个开源的可视…

ADC采集模块与MCU内置ADC性能对比

2.5V基准电压源&#xff1a; 1. 精度更高&#xff0c;误差更小 ADR03B 具有 0.1% 或更小的初始精度&#xff0c;而 电阻分压方式的误差主要来自电阻的容差&#xff08;通常 1% 或 0.5%&#xff09;。长期稳定性更好&#xff0c;分压电阻容易受到温度、老化的影响&#xff0c;长…

UDP协议(20250303)

1. UDP UDP:用户数据报协议&#xff08;User Datagram Protocol&#xff09;&#xff0c;传输层协议之一&#xff08;UDP&#xff0c;TCP&#xff09; 2. 特性 发送数据时不需要建立链接&#xff0c;节省资源开销不安全不可靠的协议 //一般用在实时性比较高…