零基础快速上手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
概述:
如上面所介绍的,在目标项中点击会对它进行展开和收起,如下:

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

会显示目标名称、进度百分比、更新时间。
2、目标项:展开状态

当点击时,则会变成展开状态,此时会出现进度控制面板、滑动进度条、底部按钮,此时高度也明显增加了。
实现原理:
接下来看一下如何来实现这样的组件内的状态变化:
1、先给目标一这个目标项定义isExpanded变量用来记录它的状态,当为false时表示目标项收起:

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

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

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

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

其中可以看到,它是处理的组件内部的一个状态。
从父组件单向同步状态:@Prop
概述:
像这种场景则需要使用到父组件向子组件进行单向同步状态:

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

此时界面的变化为:列表显示“取消”、“全选”、勾选框,底部按钮变为“删除”,以及目标项右侧弹出勾选框。
此用户点击“取消”时,则会进入到非编辑模式,此时列表会显示“编辑”文本和“添加子目标”按钮,目前项勾选项消失。
所以列表和目标项的内容都会随编辑模式的变化而变化。
实现原理:
先来分析一下目前页面的构成结构:

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

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

问题:子组件TargetListItem如何感知到父组件TargetList编辑模式的状态变化呢?
根据上面对组件内的状态变化的场景可以得知,需要给父子组件定义一个状态变量:

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

这样就可以实现TargetListItem的编辑模式状态随其父组件TargetList的编辑模式状态变化而变化的场景。
代码示例:
1、先看父组件:

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

2、再来看子组件:
此时则需要使用@Prop来进行修饰:

父子组件双向同步状态和监听状态变化:@Link、@Watch
概述:
有这么一个场景:

当目标一是展开状态时,点击目标三,则目标三展开,然后目标一收起,也就是同一时刻只能有一个项是展开的。
这里有一个问题:目标三可以通过用户的点击感知到变化而展开,但是目标一是如何感知到目标三被点击了呢?这里就需要使用到父子组件双向同步状态的机制了。
实现原理:
1、每个列表项都有其位置索引值index:

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

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

现在的问题在于:

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

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

代码示例:

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

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

这个能明白不,也就是当子组件监听到clickIndex变化时,则会自动回调onClickIndexChanged方法,然后将子组件内部的isExpanded属性进行改变,然后实现展开与收起的效果。
实践:
接下来则动手完成这个“工作目标”的效果,来巩固一下上面理论所学的状态管理。
1、新建工程:

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

2、更改app的名称:
现在运行有app名称长这样:

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

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

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


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

此时再运行:

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

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

注意了,这里报了个错:

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

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

此时运行:

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


5、总目标信息区:
这里将其封装成一个组件:

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

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) } }
这块比较好理解,就不过多解释了,运行看一下:
2、整体进度:
接下来搭建整体进度区域:

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

此时运行看一下:

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

此时运行一下:

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

再运行:

3、将整个组件属性对外暴露:
组件的封装肯定是需要给外部调用用的,目前咱们把组件中的信息都写死了,比如:

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


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

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


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

6、子目标区:
接下来再来比较复杂的子目标区了。
1、新建组件:

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

3、列表区:
由于目前篇幅比较长了,剩下的会在下篇再详细进行呈现,这个案例还是挺复杂的,状态管理在实际开发中也是非常重要,所以从0撸一遍是很有必要的。
总结:
这次学习的主要核心就是组件的状态管理,是必须要掌握的,当然这块的实战还只实现了一小半,接下来会继续将这个官方的案例完整的进行操练,这篇博文也拖了很长时间了,今年感觉自己完全堕落了,还是不能这样,知识的输出还是不能中断,加油~~
浙公网安备 33010602011771号