当前位置: 首页 > news >正文

【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的使用可以说是特别的频繁&#xff0c;但是官方提供的Tabs使用起来&#xff0c;存在tab选项卡切换动画滞后的问题。 原始动画无法满足产品的UI需求&#xff0c;因此&#xff0c;这篇文章将实现下面页面滑动&#xff0c;tab选项卡实时滑动的动画效果。 实现逻…...

Sass 模块化革命:深入解析 @use 语法,打造高效 CSS 架构

文章目录 前言use 用法1. 模块化与命名空间2. use 中 as 语法的使用3. as * 语法的使用4. 私有成员的访问5. use 中with默认值6. use 导入问题总结下一篇预告&#xff1a; 前言 在上一篇中&#xff0c;我们深入探讨了 Sass 中 import 语法的局限性&#xff0c;正是因为这些问题…...

【渗透测试】反弹 Shell 技术详解(一)

反弹 Shell 技术详解 一、前置知识 反弹 shell&#xff08;Reverse Shell&#xff09;是一种技术&#xff0c;攻击者利用它可以在远程主机上获得一个交互式的命令行接口。通常情况下&#xff0c;反弹 shell 会将标准输入&#xff08;stdin&#xff09;、标准输出&#xff08;…...

python:pymunk + pygame 模拟六边形中小球弹跳运动

向 chat.deepseek.com 提问&#xff1a;编写 python 程序&#xff0c;用 pymunk, 有一个正六边形&#xff0c;围绕中心点缓慢旋转&#xff0c;六边形内有一个小球&#xff0c;六边形的6条边作为墙壁&#xff0c;小球受重力和摩擦力、弹力影响&#xff0c;模拟小球弹跳运动&…...

Windows 图形显示驱动开发-WDDM 3.2-本机 GPU 围栏对象(二)

GPU 和 CPU 之间的同步 CPU 必须执行 MonitoredValue 的更新&#xff0c;并读取 CurrentValue&#xff0c;以确保不会丢失正在进行的信号中断通知。 当向系统中添加新的 CPU 等待程序时&#xff0c;或者如果现有的 CPU 等待程序失效时&#xff0c;OS 必须修改受监视的值。OS …...

23种设计模式之《模板方法模式(Template Method)》在c#中的应用及理解

程序设计中的主要设计模式通常分为三大类&#xff0c;共23种&#xff1a; 1. 创建型模式&#xff08;Creational Patterns&#xff09; 单例模式&#xff08;Singleton&#xff09;&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点。 工厂方法模式&#xff0…...

DEV-C++ 为什么不能调试?(正确解决方案)

为了备战pat考试&#xff0c;专门下载了DEV C&#xff0c;然后懵圈的发现&#xff0c;怎么无法调试(╯□&#xff09;╯︵ ┻━┻ 然后整了半天&#xff0c;终于在网上找到相应的解决方案&#xff01;&#xff01;&#xff01;-> Dev C 5.11 调试初始设置 <- 一共四步…...

【C++设计模式】第五篇:原型模式(Prototype)

注意&#xff1a;复现代码时&#xff0c;确保 VS2022 使用 C17/20 标准以支持现代特性。 克隆对象的效率革命 1. 模式定义与用途​ ​ 核心思想​ ​原型模式&#xff1a;通过复制现有对象​&#xff08;原型&#xff09;来创建新对象&#xff0c;而非通过new构造。​关键用…...

深入 Vue.js 组件开发:从基础到实践

深入 Vue.js 组件开发&#xff1a;从基础到实践 Vue.js 作为一款卓越的前端框架&#xff0c;其组件化开发模式为构建高效、可维护的用户界面提供了强大支持。在这篇博客中&#xff0c;我们将深入探讨 Vue.js 组件开发的各个方面&#xff0c;从基础概念到高级技巧&#xff0c;助…...

maven导入spring框架

在eclipse导入maven项目&#xff0c; 在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…...

数据守护者:备份文件的重要性与自动化实践策略

在数字化浪潮席卷全球的今天&#xff0c;数据已成为企业运营和个人生活中不可或缺的核心资源。无论是企业的财务报表、客户资料&#xff0c;还是个人的家庭照片、工作文档&#xff0c;这些数据都承载着巨大的价值。然而&#xff0c;数据丢失的风险无处不在&#xff0c;硬件故障…...

MyBatis @Param 注解详解:指定的参数找不到?

MyBatis Param 注解详解 1. Param 注解的作用 Param 注解用于显式指定方法参数的名称&#xff0c;让 MyBatis 在 SQL 映射文件&#xff08;XML&#xff09;或注解中通过该名称访问参数。 核心场景&#xff1a; 方法有多个参数时&#xff0c;避免参数名丢失或混淆。参数为简单…...

【项目日记(八)】内存回收与联调

前言 我们前面实现了三层缓存申请的过程&#xff0c;并完成了三层缓存申请过程的联调&#xff01;本期我们来介绍三层的缓存的回收机制以及三层整体联调释放的过程。 目录 前言 一、thread cache 回收内存 二、central cache 回收内存 • 如何确定一个对象对应的span • …...

性能测试监控工具jmeter+grafana

1、什么是性能测试监控体系&#xff1f; 为什么要有监控体系&#xff1f; 原因&#xff1a; 1、项目-日益复杂&#xff08;内部除了代码外&#xff0c;还有中间件&#xff0c;数据库&#xff09; 2、一个系统&#xff0c;背后可能有多个软/硬件组合支撑&#xff0c;影响性能的因…...

016.3月夏令营:数理类

016.3月夏令营&#xff1a;数理类&#xff1a; 中国人民大学统计学院&#xff1a; http://www.eeban.com/forum.php?modviewthread&tid386109 北京大学化学学院第一轮&#xff1a; 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&#xff0c;按照指导书上写的输入如下命令安装所需的软件包&#xff1a; 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、指纹扫描 这里扫的我有点懵&#xff0c;这里只有两个端口&#xff0c;感觉是要扫扫目录了 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火么&#xff1f;我也折腾了下本地部署&#xff0c;ollama、vllm、llama.cpp都弄了下&#xff0c;webui也用了几个&#xff0c;发现nextjs-ollama-llm-ui小巧方便&#xff0c;挺适合个人使用的。如果放在网上供多人使用的话&#xff0c;得接入登录认证才好&a…...

3月5日作业

代码作业&#xff1a; #!/bin/bash# 清空目录函数 safe_clear_dir() {local dir"$1"local name"$2"if [ -d "$dir" ]; thenwhile true; doread -p "检测到 $name 目录已存在&#xff0c;请选择操作&#xff1a; 1) 清空目录内容 2) 保留目…...

【MySQL】增删改查

目录 一、新增&#xff08;Create&#xff09; 单行数据 全列插入 多行数据 指定列插入 插入时间 二、查询&#xff08;Retrieve&#xff09; 全列查询 指定列查询 查询字段为表达式 别名 去重&#xff1a;DISTINCT 排序&#xff1a;ORDER BY 条件查询&#xff1…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩

目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

苍穹外卖--缓存菜品

1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓存逻辑分析&#xff1a; ①每个分类下的菜品保持一份缓存数据…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式&#xff1a;dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一&#xff0c;腐蚀跟膨胀属于反向操作&#xff0c;膨胀是把图像图像变大&#xff0c;而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...

深度学习习题2

1.如果增加神经网络的宽度&#xff0c;精确度会增加到一个特定阈值后&#xff0c;便开始降低。造成这一现象的可能原因是什么&#xff1f; A、即使增加卷积核的数量&#xff0c;只有少部分的核会被用作预测 B、当卷积核数量增加时&#xff0c;神经网络的预测能力会降低 C、当卷…...

Python Ovito统计金刚石结构数量

大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换

目录 关键点 技术实现1 技术实现2 摘要&#xff1a; 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式&#xff08;自动驾驶、人工驾驶、远程驾驶、主动安全&#xff09;&#xff0c;并通过实时消息推送更新车…...