鸿蒙ArkUI开发-实现增删Tab页签

news2024/10/2 22:22:29

场景介绍

部分应用在使用过程中需要自定义添加或删除标签的场景,比如在浏览器中的顶部标签栏中需要新打开或关闭网页页签,而这种场景与Tabs组件效果类似,但Tabs组件不提供增加或删除页签的功能,不能自由的增加删除页签,需要开发者自己实现Tabs中增删页签功能。本文以浏览器中增加或删除页签为例,实现Tabs中页签的增删功能。

效果呈现

如下动图所示:

环境要求

本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发:

  • IDE: DevEco Studio 4.0 Release
  • SDK: Ohos_sdk_public 4.0.10.13 (API Version 10 Release)

实现原理

本例涉及的关键特性以及实现方案如下:

  • 通过@Builder自定义封装一个导航页签栏,并通过ForEach完成对Tabs组件的内容页和导航页签栏的动态渲染。
  • 通过TabsController的changeIndex可实现页面的跳转,传入的index是数组中对应的索引值。
  • 页签的增加通过数组的push方法,增加数组元素。
  • 删除页签通过通过删除页面对应数组的索引值处的数据完成,删除后页面跳转位置根据业务逻辑要求确定跳转页面对应的索引值。

开发步骤

  1. 整体布局分为两部分:页面内容和页签部分。页面视图部分使用Tabs,其中页签对应显示的内容需要放入TabContent组件中。页签部分通过@Builder自定义封装一个tabBar组件,放入Tabs中的tabBar属性中。

    页面视图代码块:

    Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
      ForEach(this.tabArray, (item: number) => {
        TabContent() {
          Text('我是页面 ' + item + " 的内容")
            .height('100%')
            .width('100%')
            .fontSize(30)
            .backgroundColor("#ffffffff")
        }.tabBar(this.Tab(item))
      }, (item: number) => item.toString() + util.generateRandomUUID())
    }
    .barMode(BarMode.Scrollable, { margin: 30 })
    .onChange((index) => {
      this.focusIndex = index
    })
    .barHeight(30)
    
    

    自定义tabBar代码块:

    //控制页签渲染的数组
    @State tabArray: Array<number> = [0]
    //Tabs组件当前显示的页签
    @State focusIndex: number = 0
    //创建页签时的页签index
    @State pre: number = -1
    //Tabs组件控制器,根据组件下标控制tab跳转
    private controller: TabsController = new TabsController()
    
    //自定义导航页签栏
    @Builder
    Tab(tabNumber: number) {
      Row({ space: 20 }) {
        Text("页签 " + tabNumber).fontSize(18)
        Image($r('app.media.ic_public_cancel_filled')).width(20).height(20)
      }
      .justifyContent(FlexAlign.Center)
      .constraintSize({ minWidth: 35 })
      .width(120)
      .height(30)
      .borderRadius({ topLeft: 10, topRight: 10 })
      .backgroundColor(this.tabArray.indexOf(tabNumber) === this.focusIndex ? "#ffffffff" : "#ffb7b7b7")
    }
    
    
  2. 实现页签和页面视图的联动:这里主要通过TabsController的changeIndex来实现对应的试图跳转,但需要注意由于之后会增删数组元素的操作,所以此处传入的index值是选择页面的tabNumber在数组中的索引值。

    this.focusIndex = this.tabArray.indexOf(tabNumber)
    
    
  3. 增添数组元素实现增加页签的效果:增添数组元素使用数组的push方法在tabArray添加数据即可,但由于此demo原始定义的数组是连续的自然数,后续增删数组会打乱原有顺序,所以此处处理为先判断最后一个元素的值再加1,由于TabBar的渲染是通过ForEach被@State修饰的数组,因此当tabArray中添加数据后系统会通知ForEach便利数组重新渲染页面。

    this.tabArray.push(this.tabArray[this.tabArray.length - 1] + 1)
    
    
  4. 通常在添加新页面后,浏览器会将页面跳转到新添加的页面,因此在向tabArray中完成数据推送后,需要将页签通过TabsController中的changeIndex方法跳转到最后一个。

    this.focusIndex = this.tabArray.length - 1
    this.controller.changeIndex(this.focusIndex)
    
    
  5. 当用户选择另一个页签时,可通过自定义页签中通用事件onClick进行控制,当用户点击待选择的页签后,获取当前页签对应的下标,然后通过TabsController中的changeIndex方法进行跳转,此外可以通过更改页签背景颜色标识被选中页签的。

    .backgroundColor(this.tabArray.indexOf(tabNumber) === this.focusIndex ? "#ffffffff" : "#ffb7b7b7")
    .onClick(() => {
      this.focusIndex = this.tabArray.indexOf(tabNumber)
      this.controller.changeIndex(this.focusIndex)
    })
    
    
  6. 删除页面有三种情况(删除不同位置的页面)。

    • 第一种情况是被关闭页面为最后一个页面,且当前选中的页面为最后一个页面,如果当前被选中页面是刚刚被其他页面创建的情况,因此页面需要跳回到创建被删除页签的页签(逻辑参考chrome浏览器)

      if (this.pre >= 0) {
        this.focusIndex = this.pre
      } 
      this.tabArray.splice(this.tabArray.indexOf(tabNumber), 1)
      
      

      如果不是的话直接将当前显示页签下下标设置为前一个页签下标,tabArray数组通过splice方法删除页签,并通过TabsController完成跳转,此外页面只要有关闭操作,页面就不可以跳回打开该页面的页面,即将 pre设置为-1。

      this.focusIndex = this.focusIndex - 1
      this.tabArray.splice(this.tabArray.indexOf(tabNumber), 1)
      this.pre = -1
      this.controller.changeIndex(this.focusIndex)
      
      
    • 第二种情况,当用户当前选择的不是最后一个标签,然后直接删除其他页签时,可以直接删除删除,但是需要重新计算当前选中页签在tabArray中的实时位置,到新注意需要排除用户在最后一个页签删除当前页签的情况。

      //当前选择页面的对应数组中的数据值
      let focusNumber = this.tabArray[this.focusIndex]
      //用于判断是否是用户在最后一个页签删除当前页签的情况
      if (this.tabArray.indexOf(focusNumber) >= 0) {
        this.focusIndex = this.tabArray.indexOf(focusNumber)
      }
      this.controller.changeIndex(this.focusIndex)
      
      
    • 第三种情况,当用户当前选择的不是最后一个标签,且删除被选中页面,直接删除,然后通过TabsController完成跳转,不需要额外操作。

    说明

    • 由于Tabs组件中的导航页签栏会占满屏幕,导致添加按钮无法直接添加到与页签直接平齐的位置,因此通过层叠布局(Stack)的方式,将添加页签按钮覆盖到Tabs组件上,通过Stack中的对齐方式将添加按钮调整到合适位置。

    • 用于Tabs添加或删除子节点时,ForEach需要重新将所有页签进行重新渲染,如果在添加或删除完页签后直接调用TabsController中的changeIndex进行跳转,页面无法调到指定页签。这是由于ForEach还未将组件渲染完成,将子组件挂载到Tabs中,因此建议通过setTimeOut延迟一段时间再进行跳转,经过验证大概50ms后即可,开发者可再自行验证。

    setTimeout(() => {
     this.controller.changeIndex(this.focusIndex)
    }, 50)
    
    

完整实例

完整示例代码如下:

import util from '@ohos.util'

@Entry
@Component
struct Drag {
  //控制页签渲染的数组
  @State tabArray: Array<number> = [0]
  //Tabs组件当前显示的页签下标
  @State focusIndex: number = 0
  //创建页签时的页签index
  @State pre: number = -1
  //Tabs组件控制器,根据组件下标控制tab跳转
  private controller: TabsController = new TabsController()

  // 单独的页签
  @Builder
  Tab(tabNumber: number) {
    Row({ space: 20 }) {
      Text("页签 " + tabNumber).fontSize(18)
      Image($r('app.media.ic_public_cancel_filled')).width(20).height(20).onClick(() => {
        //获取Tabs组件当前显示的页签中显示的数字
        let focusNumber = this.tabArray[this.focusIndex]
        //被删除的页签是否是当前选中的页签,且是最后一个页签
        if (this.focusIndex === this.tabArray.indexOf(tabNumber) && this.focusIndex == this.tabArray.length - 1) {
          //判断是否需要跳回到创建该页签时的页签,如果不需要直接跳转到前一个页签
          if (this.pre >= 0) {
            this.focusIndex = this.pre
          } else {
            this.focusIndex = this.focusIndex - 1
          }
        }
        this.tabArray.splice(this.tabArray.indexOf(tabNumber), 1)
        this.pre = -1
        //对应删除页面中的第二种情况
        if (this.tabArray.indexOf(focusNumber) >= 0) {
          this.focusIndex = this.tabArray.indexOf(focusNumber)
        }
        //设置50ms 延迟跳转
        setTimeout(() => {
          this.controller.changeIndex(this.focusIndex)
        }, 50)
      })
    }
    .justifyContent(FlexAlign.Center)
    .constraintSize({ minWidth: 35 })
    .width(120)
    .height(30)
    .borderRadius({ topLeft: 10, topRight: 10 })
    .backgroundColor(this.tabArray.indexOf(tabNumber) === this.focusIndex ? "#ffffffff" : "#ffb7b7b7")
    .onClick(() => {
      this.focusIndex = this.tabArray.indexOf(tabNumber)
      setTimeout(() => {
        this.controller.changeIndex(this.focusIndex)
      }, 50)
    })
  }

  build() {
    Column() {
      Column() {
        // 页签
        // Row() {
        Stack() {
          Row({ space: 7 }) {
            //tabs
            Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
              ForEach(this.tabArray, (item: number) => {
                TabContent() {
                  Text('我是页面 ' + item + " 的内容")
                    .height('100%')
                    .width('100%')
                    .fontSize(30)
                    .backgroundColor("#ffffffff")
                }.tabBar(this.Tab(item))
              }, (item: number) => item.toString() + util.generateRandomUUID())
            }
            .barMode(BarMode.Scrollable, { margin: 30 })
            .onChange((index) => {
              this.focusIndex = index
            })
            .barHeight(30)
          }.width("100%")

          Row() {
            Image($r('app.media.ic_public_add_filled')).onClick(() => {
              if (this.tabArray.length === 0) {
                this.tabArray.push(0)
                this.focusIndex = this.tabArray.length - 1
              } else {
                this.pre = this.focusIndex
                this.tabArray.push(this.tabArray[this.tabArray.length - 1] + 1)
                this.focusIndex = this.tabArray.length - 1
              }
              setTimeout(() => {
                this.controller.changeIndex(this.focusIndex)
              }, 50)
            }).width(20).height(20)
          }.height(30).width(30).backgroundColor("#ffb7b7b7")
          .justifyContent(FlexAlign.Center)
        }
        .alignContent(Alignment.TopEnd)
        .width('100%')
        .backgroundColor("#ffb7b7b7")
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
    }
    .height('100%')
  }
}

本文主要讲解在鸿蒙开发当中ArkUI中的增删Tab页签实现,更多的鸿蒙ArkUI学习,可以参考下面的学习曲线图:

完整版鸿蒙os与openharmony技术路线与文档,可在主页保存

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java编程的利器:Pair和Triple无缝解决多值返回问题,助力编写高效代码

在实际编码中&#xff0c;经常会遇到一个方法需要返回多个值的情况&#xff0c;你编写一个方法&#xff0c;需要同时返回某个操作的结果和一些相关的附加信息。使用传统的方式&#xff0c;你可能需要创建一个包含这些信息的自定义类或者使用集合&#xff08;如 Map&#xff09;…

系统引导程序 Boot Loader——学习笔记

基于嵌入式Linux 的完整系统软件由三个部分组成&#xff1a;系统引导程序、Linux 操作系统内核和文件系统。 系统引导程序 Boot Loader 是系统加电后运行的第一段软件代码&#xff0c;它的作用是加载操作系统或者其他程序到内存中&#xff0c;并将控制权交给它们。 Boot Load…

代码随想录算法训练营第16天 | 104.二叉树的最大深度, 111.二叉树的最小深度 ,222.完全二叉树的节点个数

二叉树理论基础&#xff1a; https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE 104.二叉树的最大深度 题目链接&#xff1a;https://leetcode.cn/problems/maximum-depth-…

Mediasoup Demo-v3笔记(三)——Mediasoup库介绍

Mediasoup基本概念 Worker &#xff1a; 每一个worker就是一个进程&#xff08;节点&#xff09;&#xff0c;进程和进程之间可以通信Router&#xff1a;每一个Router就是一个房间的概念Producer&#xff1a;每一个发出声音和视频的流都是一个ProducerConsumer&#xff1a;每一…

Confluence 的文章导入到 YouTrack KB 中

YouTrack 是有一个 KB 的&#xff0c;我们可以吧 Confluence 的文章全部导入到 YouTrack 的 KB 中。 首先&#xff0c;你需要具有管理员权限&#xff0c;然后选择导入。 然后可以在打开的界面中新增一个导入。 在新增导入中输入 Confluence 在随后的界面中输入你 Confluence …

浅谈ICMP协议

ICMP(Internet Control Message Protocol) 网络控制消息协议是网络层的协议&#xff0c;所谓控制&#xff0c;指的是通过下发指令来判断是否当前主机可达目标主机及不可达时的错误报告。通常使用ping命令和traceroute命令来使用。 ping命令:检测到目标主机是否可达、 tracerout…

SQL语句创建一个简单的银行数据库

目录 一、银行业务E-R图 二、数据库模型图 转换关系模型后&#xff1a; 三、创建数据库 3.1 创建银行业务数据库 四、创建表 4.1 创建客户信息表 4.2 创建银行卡信息表 4.3 创建交易信息表 4.4 创建存款类型表 结果如下&#xff1a; ​编辑 五、插入适量数据 5.1…

Linux中LVM实验

LVM实验&#xff1a; 1、分区 -L是大小的意思-n名称的意思 从vg0&#xff08;卷组&#xff09;分出来 2、格式化LV逻辑卷 LVM扩容 如果icdir空间不够了&#xff0c; 扩展空间lvextend -L 5G /dev/vg0/lv1 /dev/vg0/lv1(pp,vg,lv) 刷新文件系统xfs_growfs /lvdir VG扩容 …

选现货白银投资划不划算?

可以肯定的是选择现货白银投资是划算的&#xff0c;但投资者需要有足够的知识和经验&#xff0c;以及对市场的敏锐观察力。只有这样&#xff0c;投资者才能在现货白银投资中获取收益。在投资市场上&#xff0c;白银作为一种特殊的投资品种&#xff0c;一直以来都备受投资者们的…

01:云计算底层技术奥秘|虚拟化管理|公有云概述

云计算底层技术奥秘&#xff5c;虚拟化管理&#xff5c;公有云概述 虚拟化平台安装验证虚拟化支持 Linux虚拟机创建虚拟机磁盘虚拟机配置文件创建虚拟机 公有云简介 虚拟化平台安装 虚拟化&#xff1a;是一种技术 就是将不可拆分的实体资源变成可以自由划分的逻辑资源&#xf…

搭建Vite和Vue环境

​ 第一步&#xff1a;创建一个文件夹&#xff08;此处为新建文件夹&#xff09;&#xff0c;并通过vscode打开 第二步&#xff1a;鼠标右键新建终端&#xff0c;并在终端处输入代码npm create vuelatest ​第三步&#xff1a;输入该项目名称&#xff08;该项目名称并不是第一…

面向对象编程(进阶)(上)

文章目录 一. 关键字&#xff1a;this1.1 this是什么&#xff1f;1.2 什么时候使用this1.2.1 实例方法或构造器中使用当前对象的成员1.2.2 同一个类中构造器互相调用 1.3 练习 二. 面向对象特征二&#xff1a;继承(Inheritance)2.1 继承的概述2.1.1 生活中的继承2.1.2 Java中的…

乖乖,咱不用BeanUtil.copy了,咱试试这款神级工具(超详细)

引言 在现代Java应用程序开发中&#xff0c;处理对象之间的映射是一个常见而且必不可少的任务。随着项目规模的增长&#xff0c;手动编写繁琐的映射代码不仅耗时且容易出错&#xff0c;因此开发者们一直在寻找更高效的解决方案。比如基于Dozer封装的或者Spring自带的BeanUtil.…

C语言指针进阶之一字符指针

目录 1.指针知识回顾 2.字符指针 2.1字符指针的一般使用 2.2字符指针的另外一种使用 1.指针知识回顾 ①.指针就是个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识了一片空间。 内存会划分成一个个的内存单元&#xff0c;每个内存单元都有一个独立的编号&#xff0…

php怎么输入一个变量,http常用的两种请求方式getpost(ctf基础)

php是网页脚本语言&#xff0c;网页一般支持两种提交变量的方式&#xff0c;即get和post get方式传参 直接在网页URL的后面写上【?a1027】&#xff0c;如果有多个参数则用&符号连接&#xff0c; 如【?a10&b27】 post方式传参 需要借助插件&#xff0c;ctfer必备插…

腾讯云服务器一键部署幻兽帕鲁联机服务器详细教程(Linux系统)

腾讯云作为国内领先的云计算服务商&#xff0c;为广大用户提供了稳定、高效的云计算服务。本文将详细介绍如何在腾讯云服务器&#xff08;Linux系统&#xff09;实现一键部署幻兽帕鲁联机服务器&#xff0c;帮助大家快速搭建自己的游戏联机服务器。 第一步&#xff1a;购买服务…

C#winform上位机开发学习笔记13-串口助手显示系统时间功能添加

1.功能描述 在上位机中显示系统的实时时间 2.代码部分 步骤1&#xff1a;添加文本框控件并设置参数 #此处注意将BackColor颜色修改为非Control&#xff0c;即可正常显示ForeColor颜色&#xff0c;否则该颜色不变&#xff0c;原因暂且不明。 步骤2&#xff1a;添加timer控件…

Docker深入解析:从基础到实践

Docker基础知识 Docker是什么&#xff1a;定义和核心概念解释 Docker是一个开源项目&#xff0c;它诞生于2013年&#xff0c;旨在自动化应用程序的部署过程&#xff0c; 让应用程序能够在轻量级的、可移植的、自给自足的容器中运行。这些容器可以在几乎任何机器上运行&#xf…

linux安装python3.11

yum -y install gcc-c zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite* readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel下载地址 https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tar.xz 上传python文件&#xff…

互信息的简单理解

在介绍互信息之前&#xff0c;首先需要了解一下信息熵的概念&#xff1a;所谓信息熵&#xff0c;是指信息论中对一个随机变量不确定性的度量&#xff0c;对于随机变量x&#xff0c;信息熵的定义为&#xff1a; H ( x ) − ∑ x p ( x ) l o g p ( x ) H(x)-\sum_xp(x)logp(x) …