零基础快速上手HarmonyOS ArkTS开发6---使用Tabs组件进行页面切换、组件状态管理

接着https://www.cnblogs.com/webor2006/p/18275969继续往下学习。

使用Tabs组件进行页面切换:

在上一次我们使用了List和Grid两大组件构建了首页和我的两个界面:

接下来则学习使用Tabs组件来将这俩界面进行一个串联,可以让用户进行多页面的切换。

Tabs组件介绍:

它的定义如下:

可以看出所有的参数都是可选的,另外它有如下常用属性:

1、vertical,设置它主要有如下两种方向:

2、barMode:它有如下几个模式:

Fixed:

Scrollable:

当内容超出范围之后可以左右滑:

3、barWidth、barHeight则是用来设置宽高,如下:

设置TabBar位置和排列方向:

对于TabBar它有如下三种常用的使用场景:

顶部Tab导航栏:

它对应Tabs组件的代码如下:

其中核心是通过barPosition来设置Tabs的位置在顶部,通过vertical属性设置为水平方向。

侧边导航栏:

这种场景通常在平板横屏下来使用,代码可以这样来设置:

 

底部导航栏:

可以通过如下代码来实现:

使用Tabs组件构建底部导航:

在实际开发中往往UI设计效果是多样的,使用系统的Tabs组件实现可能不一定能满足样式, 此时就需要自定义TabBar的样式了,所以下面来看一下如何自定义TabBar的样式。

tabBar属性介绍:

在自定义TabBar样式之前,先了解一下它的属性,TabBar上显示的内容是通过Tabs的子组件TabContent的tabBar属性来设置的,比如下面使用tabBar属性来实现了一个默认样式的页签:

tabBar属性还支持使用@Builder装饰器修饰的函数,所以我们可以使用@Builder装饰器构建一个生成自定义TabBar样式的函数。

构建自定义TabBar:

接下来咱们则来学习一下如何使用@Builder构建这种样式的一个底部页签:

该页签包含有图片和文本,先来贴一下整体的实现代码:

下面来看一下实现的过程:

1、定义currentIndex:

用来表示当前选中的页签是哪一个。

2、创建TabsController对象:

用来控制页签的切换。

3、使用@Builder定义TabBuilder函数:

用于生成自定义TabBar样式的页签组件,它包含四个参数:
1、title:页签显示的标题文本;

2、index:页签的索引;

3、selectedImg:页签被选中时的图片;

4、normalImg:页签未被选中时的图片;

而它是由两个元素构成,一个是Image,一个是Text,最后再给页签设置一个onClick的点击事件,进行页签的切换:

构建主页面:

先上代码:

1、设置barPosition为BarPosition.End,让其显示在底部:

2、设置TabsController,用来控制页签:

3、包含两个TabContent,一个首页,一个设置页:

需要注意,TabContent不支持设置通用的宽度和高度属性,宽度默认撑满父组件Tabs,高度则是由父组件Tabs高度和TabBar组件高度决定。

4、使用前面定义的TabBuilder函数给这俩个TabContent设置tabBar属性:

也就是页签显示的内容。

5、barWidth属性设置TabBar的宽度为100%,barHeight属性设置高度、barMode为使页签平均分配TabBar的宽度:

添加onChange事件:

这样就实现了一个左右可以切换的主界面了。

实践:

接下来回到之前练习的顶目中使用Tabs组件来将已实现的两个页面(首页、我的)进行一个串联。

1、对已编写的文件名先命一个名:

目前对于已经实现的两个界面命名不太对:

这里将其挪到一个单独的包中:

其中Home.ets和Setting.ets由于需要被Home.ets所引用,所以需要export一下:

2、使用Tabs组件:

import CommonConstants from '../common/constants/CommonConstants';
import Home from "../view/Home"
import Setting from "../view/Setting"
/**
 * Main page
 */
@Entry
@Component
struct MainPage {
  @State currentIndex: number = CommonConstants.HOME_TAB_INDEX;
  private tabsController: TabsController = new TabsController();

  build() {
    Tabs({
      barPosition: BarPosition.End,
      controller: this.tabsController
    }) {
      TabContent() {
        Home()
      }
      .padding({ left: $r('app.float.mainPage_padding'), right: $r('app.float.mainPage_padding') })
      .backgroundColor($r('app.color.mainPage_backgroundColor'))

      TabContent() {
        Setting()
      }
      .padding({ left: $r('app.float.mainPage_padding'), right: $r('app.float.mainPage_padding') })
      .backgroundColor($r('app.color.mainPage_backgroundColor'))
    }
    .width(CommonConstants.FULL_PARENT)
    .backgroundColor(Color.White)
    .barHeight($r('app.float.mainPage_barHeight'))
    .barMode(BarMode.Fixed)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }
}

这块代码都比较简单,此时运行的效果如下:

框架有了,但是底部少了Tab项的显示,所以接下来加上它,这里就需要用到上面所介绍的使用@Builder定义TabBuilder函数的知识了:

  @Builder TabBuilder(title: string, index: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === index ? selectedImg : normalImg)
        .width($r('app.float.mainPage_baseTab_size'))
        .height($r('app.float.mainPage_baseTab_size'))
      Text(title)
        .margin({ top: $r('app.float.mainPage_baseTab_top') })
        .fontSize($r('app.float.main_tab_fontSize'))
        .fontColor(this.currentIndex === index ? $r('app.color.mainPage_selected') : $r('app.color.mainPage_normal'))
    }
    .justifyContent(FlexAlign.Center)
    .height($r('app.float.mainPage_barHeight'))
    .width(CommonConstants.FULL_PARENT)
    .onClick(() => {
      this.currentIndex = index;
      this.tabsController.changeIndex(this.currentIndex);
    })
  }

然后设置一下tabBar属性:

运行:

 

组件状态管理:

最终案例效果:

在学习之前,先来看一下最终要实现的一个效果,感觉还是挺复杂的:

这是一个对任务目标的一个管理的功能,里面有增删改查的功能,比较容易理解,这里面就涉及到了常用的状态管理的知识点了,也是开发中非常重要的知识点。

简介:

通常应用中的界面都是动态的, 比如拿上面所说的这个之后要来实践的这个案例来说,当用户点击目标项时,界面则会呈现展开和收起的状态:

此时界面需要呈现不同的内容和高度,此时就需要对界面进行更新:

如下面这样的效果:

而在ArkUI中它具有状态驱动UI更新的特点:

所以只需要通过一个变量来记录状态,当“用户交互或外部事件” 引起“状态”进行改变时, ArkUI就会自动更新界面中受影响的部分。

管理组件状态:

ArkUI框架提供了多个管理状态的装饰器,不同的装饰器适用于不同的场景,所以接下来来看一下常见的几个组件状态管理的场景。

组件内的状态管理:@State

概述:

如上面所介绍的,在目标项中点击会对它进行展开和收起,如下:

Untitled4

那剖析一下这两种状态下目标项的界面呈现:

1、目标项:收起状态

image

会显示目标名称、进度百分比、更新时间。

2、目标项:展开状态

image

当点击时,则会变成展开状态,此时会出现进度控制面板、滑动进度条、底部按钮,此时高度也明显增加了。

实现原理:

接下来看一下如何来实现这样的组件内的状态变化:
1、先给目标一这个目标项定义isExpanded变量用来记录它的状态,当为false时表示目标项收起:

image

为true时,则表示目标项展开:

image

2、使用@State装饰器修饰isExpanded变量:

image

使其成为目标项内部的状态变量,ArkUI框架会建立isExpanded与目标项之间的绑定:

image

当isExpaned变化时,目标项则会自动的进行展开或收起,如果不加@State修饰的话,isExpanded它只是一个普通的变量,ArkUI它不会建立isExpanded与目标项之间的绑定的,此时界面也不会自动进行更新了。

代码示例:

image

其中可以看到,它是处理的组件内部的一个状态。

从父组件单向同步状态:@Prop

概述:

像这种场景则需要使用到父组件向子组件进行单向同步状态:

image

当点击“编辑”,列表会进入编辑模式:

image

此时界面的变化为:列表显示“取消”、“全选”、勾选框,底部按钮变为“删除”,以及目标项右侧弹出勾选框。 

此用户点击“取消”时,则会进入到非编辑模式,此时列表会显示“编辑”文本和“添加子目标”按钮,目前项勾选项消失。

所以列表和目标项的内容都会随编辑模式的变化而变化。

实现原理:

先来分析一下目前页面的构成结构:

image

其中可以看出TargetListItem是TargetList的子组件,其中这俩组件都需要有一个变量来记录是编辑还是非编辑模式:

image

而变化的触发源是在TargetList的父组件中,它需要将这个编辑状态传递到子组件TargetListItem中:

image

问题:子组件TargetListItem如何感知到父组件TargetList编辑模式的状态变化呢?

根据上面对组件内的状态变化的场景可以得知,需要给父子组件定义一个状态变量:

image

而同样的,对于子组件也需要有一个监听变化的变量,但是这次它的变量的变化是需要受父组件的影响的,在ArkUI中则可以使用@Prop装饰器,由它修饰的变量可以和其父组件中的状态建立单向同步关系,所以:

image

这样就可以实现TargetListItem的编辑模式状态随其父组件TargetList的编辑模式状态变化而变化的场景。

代码示例:

1、先看父组件:

image

而在父组件中需要添加点击事件,用来改变状态:

image

2、再来看子组件:

此时则需要使用@Prop来进行修饰:

image

父子组件双向同步状态和监听状态变化:@Link、@Watch

概述:

有这么一个场景:

Untitled4

当目标一是展开状态时,点击目标三,则目标三展开,然后目标一收起,也就是同一时刻只能有一个项是展开的。

这里有一个问题:目标三可以通过用户的点击感知到变化而展开,但是目标一是如何感知到目标三被点击了呢?这里就需要使用到父子组件双向同步状态的机制了。

实现原理:

1、每个列表项都有其位置索引值index:

image

2、记录被点击的目标项索引:clickIndex

image

这里可以在目标项中使用@state定义clickIndex,比如点击了目标三,此时的clickIndex=2,如果父组件列表也能感知clickIndex的变化,那么根据上面所学@state父子组件的单向同步,很自然的目标一的clickIndex也会为2,如下:

image

现在的问题在于:

image

目前所学的只有@Prop实现父到子的单向同步,像这样:

image

在ArkUI中,需要使用@Link来实现双向同步,如下:

image

代码示例:

image

接下来,有一个关键的步骤,就是使用$符进行父子组件的双向绑定:

image

现在目标一感知到了目标三被点击了,目标一还需要从展开状态变为收起状态,此时还需要使用@Watch声明状态的监听:

image

这个能明白不,也就是当子组件监听到clickIndex变化时,则会自动回调onClickIndexChanged方法,然后将子组件内部的isExpanded属性进行改变,然后实现展开与收起的效果。

实践:

接下来则动手完成这个“工作目标”的效果,来巩固一下上面理论所学的状态管理。

1、新建工程:

image

目前我使用的IDE是最新的版本(2025.9):

image

2、更改app的名称:

现在运行有app名称长这样:

image

改一下让其见名之义,这块改名还有一些小坑:

image

正常把这块的名称给改了就行了对吧:

image

照理运行出来桌面上显示的应该是这个名称,那为啥显示的是“label”呢?其实这块也得改:

image

image

所以要改的话,需要改一下这块:

image

此时再运行:

image

3、界面分析:

接下来则来进行主界面的搭建了,先来简单分析一下界面的构成,主要由三个区域:

image

当然还有一些点击事件之类的效果,先按着上面的区域一步步来实现,要自己手把手来实现其实也不轻松,另外参考官方的开源代码以组件化的方式来进行实现,由于这块的代码量也不少,所以肯定不能从0来写了,照着官方的代码边copy边一点点来撸,当你能撸完那么对于整个这块新学的知识也就大概清楚了,实际工作中也不可能完全从0开撸,都是抄袭加改良。

4、标题:
这个比较简单,直接上代码:

image

注意了,这里报了个错:

image

目前还是一个普通的方法,要将其变成一个UI组件才行,得使用如下装饰器:@Builder

image 

image

其中关于这些文本和常量,官方的DEMO中都有,这里就不贴了,回头我把这块的源码附在文章中:

image

此时运行:

image

左边得来点间距,这里其实给Column设置一下属性既可:

image

image

5、总目标信息区:

这里将其封装成一个组件:

 

image

而它的布局又分为上下两个区域:

image

1、目标项:

import { CommonConstants } from '../common/constant/CommonConstant'

@Component
export default struct TargetInfomation {
  build() {
    Column() {
      this.TargetItem()//目标项
    }
    .padding($r('app.float.target_padding'))
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height($r('app.float.target_info_height'))
    .backgroundColor(Color.White)
    .borderRadius(CommonConstants.TARGET_BORDER_RADIUS)
  }

  @Builder
  TargetItem() {
    Row() {
      Image($r("app.media.ic_main"))
        .width($r('app.float.target_image_length'))
        .height($r('app.float.target_image_length'))
        .objectFit(ImageFit.Fill)
        .borderRadius(CommonConstants.IMAGE_BORDER_RADIUS)
      Column() {
        Text($r('app.string.target_name'))
          .fontSize($r('app.float.target_name_font'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .width(CommonConstants.TITLE_WIDTH)
        Text($r('app.string.target_info'))
          .fontSize($r('app.float.target_desc_font'))
          .margin({ top: $r('app.float.title_margin') })
      }
      .margin({ left: CommonConstants.TARGET_MARGIN_LEFT })
      .alignItems(HorizontalAlign.Start)
    }
    .width(CommonConstants.FULL_WIDTH)
  }
}

这块比较好理解,就不过多解释了,运行看一下:

image 

2、整体进度:

接下来搭建整体进度区域:

image

它整体是水平的,所以用Row,然后左侧是上下布局的,所以使用Column,这块也比较简单,直接贴代码:

image

此时运行看一下:

image

接下来再来实现右侧的圆形进度,这里需要使用到Stack布局了,类似于Android的FrameLayout,如下:

image

此时运行一下:

image

很明显布局不太对,应该是居右显示,这里则需要使用空白填充组件Blank了:

image

再运行:

image

3、将整个组件属性对外暴露:

组件的封装肯定是需要给外部调用用的,目前咱们把组件中的信息都写死了,比如:

image

接下来将这些都定义成属性由外部进行传递,所以需要使用父组件单向同步机制@Prop了,如下:

image

image

此时在父组件调用该组件时,则可以动态来传数据了,如下:

image

4、将文本的样式封装一下:

目前整体的文本的样式跟预期的有一些区别,这里处理一下,先来定义一下,这里需要使用@Extend扩展组件样式的装饰器了(具体的用法可以参考官方此链接:https://developer.huawei.com/consumer/cn/doc/search?type=API&val=@Extend&nextSubType=2&versionValue=hmos-503),如下:

image

image

也就是将通用的属性给封装成了一个扩展属性,这种语法需要熟悉。此时再看一下效果:

image

6、子目标区:

接下来再来比较复杂的子目标区了。

1、新建组件:

image

image 

2、顶部编辑操作区:

import { CommonConstants } from '../common/constant/CommonConstant';

@Component
export default struct TargetList {
  build() {
    Column() {
      //顶部编辑操作区
      Row() {
        Text($r('app.string.sub_goals'))
          .fontSize($r('app.float.secondary_title'))
          .fontWeight(CommonConstants.FONT_WEIGHT_LARGE)
          .fontColor($r('app.color.title_black_color'))
        Blank()
        Text($r('app.string.edit_button'))
          .operateTextStyle($r('app.color.main_blue'))
          .onClick(() => {
            // todo:点击编辑按钮
          })
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.history_line_height'))
      .padding({
        left: $r('app.float.list_padding'),
        right: $r('app.float.list_padding_right')
      })

      //子目标列表
      //底部添加子目标
    }
    .width(CommonConstants.MAIN_BOARD_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
    .padding({ top: $r('app.float.operate_row_margin') })
  }
}

/**
 * Custom text button style.
 */
@Extend(Text)
function operateTextStyle(color: Resource) {
  .fontSize($r('app.float.text_button_font'))
  .fontColor(color)
  .lineHeight($r('app.float.text_line_height'))
  .fontWeight(CommonConstants.FONT_WEIGHT)
}

其中又用到了@Extend扩展组件样式的使用场景,这块未来在实际开发中是非常常用的,所以这块需要学会。上面的代码也比较容易理解,就不过多解释了,运行看一下:

image

3、列表区:

由于目前篇幅比较长了,剩下的会在下篇再详细进行呈现,这个案例还是挺复杂的,状态管理在实际开发中也是非常重要,所以从0撸一遍是很有必要的。

总结:

这次学习的主要核心就是组件的状态管理,是必须要掌握的,当然这块的实战还只实现了一小半,接下来会继续将这个官方的案例完整的进行操练,这篇博文也拖了很长时间了,今年感觉自己完全堕落了,还是不能这样,知识的输出还是不能中断,加油~~

posted on 2025-09-20 01:42  cexo  阅读(75)  评论(0)    收藏  举报

导航