鸿蒙原生 ArkTS 布局实践:用 Row 实现标签栏(Tag Bar)布局


目录

  1. 引言:为什么选择 Row 实现标签栏
  2. 项目环境与准备工作
  3. 核心布局容器:Row 深度解析
  4. 标签栏组件化设计思路
  5. 基础标签栏:最简单的 Row + Text 组合
  6. 可切换标签栏:状态管理与交互反馈
  7. 图标+文字标签栏:丰富视觉表现力
  8. 可关闭标签栏:复合组件与数组操作
  9. 页面整合与滚动适配
  10. 布局要点总结与最佳实践
  11. 常见问题与调试技巧
  12. 实际项目中的集成建议
  13. 结语

1. 引言:为什么选择 Row 实现标签栏

在移动端应用开发中,标签栏(Tag Bar)是一种极其常见的 UI 模式。它广泛应用于商品分类筛选、兴趣标签选择、关键词展示、搜索结果过滤等场景。典型的标签栏由多个水平排列的标签组成,每个标签呈现为一个圆角矩形背景的文字块,用户可以点击切换选中状态。

1.1 标签栏的 UI 特征

标签栏的视觉特征可以归纳为三点:

  • 横向排列:所有标签沿水平方向依次排布,一行展示不下时支持横向滚动
  • 等间距或紧凑间距:标签之间有固定的间距,整体看起来整齐
  • 选中态高亮:当前选中的标签通常以不同的颜色或背景进行区分

1.2 Row 容器的天然适配性

在 HarmonyOS 的 ArkUI 框架中,Row 是最基础的线性布局容器之一,其核心能力就是让子组件沿水平方向(主轴)排列。这与标签栏的「横向排列」需求完全吻合。相比于使用 Flex 或 Grid 来实现标签栏,Row 具有以下优势:

特性 Row 实现 其他方案
代码简洁度 极高,无需额外配置 Flex 需要设置 direction,Grid 需要列数配置
子项间距控制 直接通过 space 参数 需要手动计算或额外容器
可滚动扩展 外层包一层 Scroll 即可 同理
对齐方式 支持多种垂直对齐 各方案差异不大
性能 轻量,无额外布局开销 Grid 的布局计算更复杂

因此,Row + Text 的组合是实现标签栏的「黄金搭档」,也是鸿蒙原生布局中最推荐的方案之一。


2. 项目环境与准备工作

2.1 开发环境要求

本文示例基于 HarmonyOS NEXT 开发,API 版本为 24,对应 ArkTS 声明式开发范式。需要以下环境:

工具 版本建议
DevEco Studio 5.0.0 及以上
HarmonyOS SDK API 24
ArkTS 声明式 UI 范式
目标设备 HarmonyOS NEXT 模拟器或真机

2.2 创建项目

在 DevEco Studio 中创建一个新的 Empty Ability 项目,选择 ArkTS 语言和 API 24。项目初始化后,核心目录结构如下:

entry/
├── src/main/ets/
│   ├── entryability/EntryAbility.ets
│   └── pages/Index.ets
├── src/main/resources/
│   ├── base/profile/main_pages.json
│   └── base/element/
├── build-profile.json5
└── oh-package.json5

2.3 引入必要模块

本示例中使用的 promptAction(弹窗提示)来自 @kit.ArkUI,需要在页面顶部导入:

import { promptAction } from '@kit.ArkUI';

该模块提供了 showToast 方法,用于在用户操作标签时给出轻量级的反馈提示。


3. 核心布局容器:Row 深度解析

在深入标签栏实现之前,有必要先全面理解 Row 容器的布局机制。Row 是 ArkUI 中最常用的布局容器之一,属于「线性布局」家族。

3.1 Row 的基本语法

Row({ space: number }) {
  // 子组件依次排列
}
  • space:主轴方向(水平)上子组件之间的间距,单位为 vp(虚拟像素)
  • 子组件按照声明顺序从左到右排列

3.2 Row 的核心属性

属性 作用 常用值
.space() 主轴间距,也可在构造函数中传入 8, 10, 12, 16
.alignItems() 交叉轴(垂直)对齐方式 VerticalAlign.Top / Center / Bottom
.justifyContent() 主轴对齐方式 FlexAlign.Start / Center / End / SpaceBetween / SpaceAround
.width() / .height() 容器尺寸 '100%' 或固定值
.padding() 内边距 12{ left, right, top, bottom }

3.3 Row 与 Flex 的关系

在 ArkUI 中,Row 实际上是 Flex 的一个特殊子类——它固定了 direction: FlexDirection.Row(主轴水平)。二者的关系如同 Android 中的 LinearLayoutFlexboxLayout。Row 提供了更简洁的 API,而 Flex 则提供了更灵活的控制。

// Row 写法(简洁)
Row({ space: 10 }) { ... }

// Flex 等效写法(灵活)
Flex({ direction: FlexDirection.Row, space: 10 }) { ... }

3.4 Row 的嵌套能力

Row 可以嵌套 Row 或 Column,形成复杂的布局。在本示例的「可关闭标签栏」中,每个标签内部就是一个嵌套的 Row:

Row({ space: 10 }) {           // 外层 Row:标签横向排列
  Row({ space: 4 }) {          // 内层 Row:标签文本 + 删除按钮
    Text('鸿蒙')
    Text('×')
  }
}

3.5 Row 的性能考量

Row 的布局算法是 O(n) 的——只需遍历子组件一次即可确定每个子项的位置。与之相比,Grid 布局的算法复杂度更高。因此,对于标签栏这种「一行展示、数量有限」的场景,Row 是性能最优的选择。


4. 标签栏组件化设计思路

4.1 组件树结构

整个示例应用的组件层次如下:

Index (Entry Page)                       ← 页面入口
├── Scroll                               ← 页面纵向滚动
│   └── Column                           ← 垂直排列各标签栏组
│       ├── TagBar (基础标签栏)           ← 纯展示
│       ├── TagBar (可切换标签栏)          ← 点击切换选中态
│       ├── IconTagBar (图标标签栏)        ← Emoji 图标 + 渐变色
│       └── ClosableTagBar (可关闭标签栏)   ← 带 × 删除按钮

4.2 数据模型定义

为标签定义一个清晰的接口,有助于后续的扩展和维护:

interface TagItem {
  label: string;      // 标签显示文本
  selected: boolean;  // 是否选中
}

4.3 组件职责划分

组件 职责 状态管理
TagBar 通用标签栏,通过 props 接收数据和标题 内部 @State 管理选中索引
IconTagBar 图标风格标签栏,硬编码数据 内部 @State
ClosableTagBar 可删除标签栏,数据可变 内部 @State,支持 splice
Index 主页面,组装上述子组件,提供标签数据 只读数据,无状态变化

这种设计遵循了「单一职责原则」——每个组件只负责自己的一类功能,数据通过 props 向下传递,状态在组件内部管理。


5. 基础标签栏:最简单的 Row + Text 组合

5.1 代码实现

基础标签栏是整个示例的起点,它展示了 Row 容器最纯粹的使用方式。

@Component
struct TagBar {
  private tags: TagItem[] = [];
  private title: string = '';
  private desc: string = '';
  @State private selectedIndex: number = 0;

  build() {
    Column({ space: 8 }) {
      // 标题
      Text(this.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      // 标签栏核心 —— Row 容器
      Row({ space: 10 }) {
        ForEach(this.tags, (item: TagItem, index: number) => {
          Text(item.label)
            .fontSize(14)
            .fontColor(this.selectedIndex === index ? '#FFFFFF' : '#666666')
            .backgroundColor(this.selectedIndex === index ? '#007AFF' : '#F5F5F5')
            .borderRadius(16)
            .padding({ left: 16, right: 16, top: 6, bottom: 6 })
            .border({
              width: this.selectedIndex === index ? 0 : 1,
              color: '#E0E0E0',
              style: BorderStyle.Solid
            })
            .onClick(() => {
              this.selectedIndex = index;
            })
        }, (item: TagItem) => item.label)
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)
      .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)' })
    }
  }
}

5.2 逐行解读

第 1-4 行:通过 @Component 装饰器声明一个可复用的组件。tagstitledesc 为父组件传入的属性,selectedIndex 为组件内部的状态变量。

第 7 行ForEach 是 ArkTS 中用于遍历数组的内置组件,它会为数组中的每个元素生成对应的 UI。第二个参数是键值生成函数,这里使用 item.label 作为唯一标识,帮助框架高效地进行差异更新。

第 8-10 行Text(item.label) 是标签的视觉本体。通过链式调用的方式依次设置:

  • fontSize(14):标准正文字号
  • fontColor():选中时白色,未选中时灰色
  • backgroundColor():选中时蓝色,未选中时浅灰
  • borderRadius(16):圆角,使标签呈胶囊状
  • padding():内边距,控制标签内部文字与边缘的距离
  • border():未选中时显示细边框,选中时隐藏

5.3 布局效果

当父组件传入 7 个标签数据后,页面呈现效果如下:

┌─────────────────────────────────────────────┐
│  ① 基础标签栏(默认展示)                      │
│  标签横向等间距排列,圆角背景,简约风格          │
│  ┌──────────────────────────────────────────┐ │
│  │ [推荐]  [鸿蒙]  [ArkTS]  [HarmonyOS]    │ │
│  │ [NEXT]  [DevEco]  [OpenHarmony]         │ │
│  └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

每个标签像一个独立的胶囊,横向均匀排列。白色的背景加上轻微的阴影,让标签栏区域在页面中有「浮起」的层次感。


6. 可切换标签栏:状态管理与交互反馈

6.1 状态管理的核心机制

在 ArkTS 中,@State 装饰器是驱动 UI 更新的核心机制。当 @State 修饰的变量发生变化时,框架会自动重新渲染引用了该变量的视图。

@State private selectedIndex: number = 0;

TagBar 组件中,selectedIndex 即当前选中标签的索引。点击标签时更新该值,UI 自动响应:

.onClick(() => {
  this.selectedIndex = index;
  promptAction.showToast({
    message: `选中标签:${item.label}`,
    duration: 1500
  });
})

6.2 条件样式的三目表达式

ArkTS 的模板语法支持在表达式中直接使用三目运算符来实现条件样式:

.fontColor(this.selectedIndex === index ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedIndex === index ? '#007AFF' : '#F5F5F5')

这种写法的好处是:

  • 代码紧凑:无需写 if-else 分支
  • 关联明确:样式与状态直接绑定,阅读时一目了然
  • 修改集中:要调整选中/未选中样式,只需修改这一处

6.3 使用 Toast 增强交互反馈

promptAction.showToast 是 HarmonyOS 提供的轻量级提示接口,适合在用户操作后给出即时反馈。参数包括:

  • message:提示文本(字符串)
  • duration:显示时长,单位毫秒,建议 1500~2000ms

在本例中,点击标签时弹窗显示「选中标签:某某」,使用户明确感知到操作已被响应。

6.4 状态与 UI 的映射关系

可以用一个简单的表格来表示 selectedIndex 与标签视觉样式之间的映射:

selectedIndex 该标签 fontColor 该标签 backgroundColor 边框
=== index #FFFFFF 白色 #007AFF 蓝色 无(0px)
!== index #666666 灰色 #F5F5F5 浅灰 1px solid #E0E0E0

这种「状态 → 样式」的映射正是声明式 UI 的核心思想:开发者只描述「当状态为 X 时,UI 应该长什么样」,框架负责在状态变化时自动更新 UI。


7. 图标+文字标签栏:丰富视觉表现力

7.1 从纯文本到图文混排

基础标签只能展示文字,在实际项目中,我们往往需要在标签中加入图标来提升识别度。IconTagBar 组件展示了两种增强手段:

  1. Emoji 图标:直接在文本中嵌入 Emoji 字符,无需额外资源文件
  2. 渐变背景:选中时使用 linearGradient 创建渐变效果

7.2 数据定义

@State private tags: string[] = [
  '📱 手机', '💻 电脑', '⌚ 手表', '📺 电视', '🎧 耳机', '📷 相机'
];

这里的标签是字符串数组,文本直接包含 Emoji。由于不需要 selected 字段,使用 string[]TagItem[] 更简洁。

7.3 渐变背景的实现

.linearGradient({
  direction: GradientDirection.RightBottom,
  colors: this.selectedIndex === index
    ? [['#667EEA', 0], ['#764BA2', 1]]
    : undefined
})

linearGradient 是 ArkTS 提供的线性渐变 API:

  • direction:渐变方向,RightBottom 表示从左上到右下
  • colors:颜色数组,每个元素是 [颜色值, 位置(0~1)] 的元组。undefined 表示不使用渐变

这里使用了紫色系渐变(#667EEA#764BA2),与基础标签的蓝色形成视觉差异化。

7.4 横向滚动的适配

标签数量较多或屏幕宽度有限时,标签栏可能无法在一行内完全展示。解决方案是包裹一层 Scroll 组件:

Scroll() {
  Row({ space: 10 }) {
    // 标签列表
  }
  .width('100%')
  .padding(12)
}
.scrollable(ScrollDirection.Horizontal)

Scroll 组件在子内容超出自身尺寸时,会根据 scrollable 设置的滚动方向提供滚动能力。ScrollDirection.Horizontal 表示横向滚动。

需要注意的是,Scroll 组件需要显式设置 scrollable 属性才能启用滚动(虽然大部分场景下它是默认开启的,但显式指定更可靠)。

7.5 Emoji 在鸿蒙系统中的兼容性

Emoji 在 HarmonyOS 上的渲染效果取决于系统字体。常见 Emoji(如 📱 💻 ⌚ 📺 🎧 📷)在 API 24 上均有良好支持。如果项目需要更专业的图标效果,建议使用 SVG 图标或 Image 组件加载 PNG 图标。


8. 可关闭标签栏:复合组件与数组操作

8.1 复合标签的嵌套布局

每个可关闭标签由「文本 + 删除按钮」组成,这两个元素需要水平排列,因此内部也需要一个 Row:

Row({ space: 4 }) {          // 内层:标签内容
  Text(item.label)           // 标签文本
  Text('×')                  // 删除按钮
}
.padding({ left: 12, right: 10, top: 6, bottom: 6 })
.backgroundColor('#F0F5FF')
.borderRadius(16)
.border({ width: 1, color: '#B3D4FF', style: BorderStyle.Solid })

整个标签栏的外层又是一个 Row:

Row({ space: 10 }) {         // 外层:标签横向排列
  // ... 每个标签是一个嵌套 Row
}

这就形成了「Row 套 Row」的两层嵌套结构。

8.2 删除操作的数组更新

点击 × 按钮时,需要从数组中移除当前标签:

.onClick(() => {
  this.tags.splice(index, 1);    // 删除当前项
  this.tags = [...this.tags];    // 重新赋值触发刷新
  promptAction.showToast({
    message: `已移除标签:${item.label}`,
    duration: 1500
  });
})

这里有一个关键细节:虽然 splice 修改了数组,但 @State 对于数组的监听是基于引用变化的。如果直接修改数组内容而不改变引用,框架可能无法检测到变化。因此需要 this.tags = [...this.tags] 创建一个新数组来强制触发 UI 更新。

8.3 删除动画的缺失与补偿

HarmonyOS 的 ForEach 默认不提供删除动画。如果需要动画效果,可以考虑使用 animateTo 包裹状态更新:

animateTo({ duration: 200 }, () => {
  this.tags.splice(index, 1);
  this.tags = [...this.tags];
});

但这个功能需要 API 25+ 的部分支持,在 API 24 上存在一定限制。本文示例未包含动画,读者可根据项目需求自行添加。

8.4 适用场景分析

可关闭标签栏最常见的应用场景包括:

场景 说明
搜索历史标签 用户可删除不需要的历史搜索词
已选筛选条件 用户激活的筛选条件显示为标签,点击 × 取消
标签编辑页 用户自定义标签列表,支持增删改
文件/图片标签 为照片或文档添加或移除分类标签

9. 页面整合与滚动适配

9.1 主页面 Index 的设计

Index 是页面的入口组件,使用 @Entry 装饰器标记。它的职责是:

  1. 定义页面标题和描述
  2. 提供标签数据(basicTagsselectableTags
  3. 组装并排列各个子组件(TagBar × 2, IconTagBar, ClosableTagBar)

9.2 页面纵向滚动

多个标签栏组垂直排列,当内容超出屏幕高度时,需要整体纵向滚动。使用 Scroll 包裹 Column 实现:

Scroll() {
  Column({ space: 16 }) {
    // 所有内容
  }
  .width('100%')
  .padding(16)
}
.scrollable(ScrollDirection.Vertical)
.backgroundColor('#F2F3F5')

这里 Scroll 的滚动方向是 ScrollDirection.Vertical,表示纵向滚动。页面背景色设为 #F2F3F5(浅灰色),与每个标签栏模块的白色背景形成对比,增强层次感。

9.3 路由配置

main_pages.json 中注册页面路径:

{
  "src": [
    "pages/Index"
  ]
}

main_pages.jsonsrc 数组中的第一个页面即为应用的启动页。多个页面可以注册为数组中的多个元素,通过 router.pushUrl 进行页面间跳转。

9.4 完整页面呈现效果

运行时,页面的整体结构呈现为:

┌─────────────────────────────────────┐
│  Tag Bar 标签栏布局示例               │
│  基于 Row + Text 实现的多组标签栏...  │
│                                     │
│  ┌──────────────────────────────┐   │
│  │ ① 基础标签栏                   │   │
│  │ [推荐] [鸿蒙] [ArkTS] [...]   │   │
│  └──────────────────────────────┘   │
│                                     │
│  ┌──────────────────────────────┐   │
│  │ ② 可切换标签栏                 │   │
│  │ [全部] [资讯] [教程]  [开源]  │   │
│  │ [问答] [招聘]                 │   │
│  └──────────────────────────────┘   │
│                                     │
│  ┌──────────────────────────────┐   │
│  │ ③ 图标+文字标签栏              │   │
│  │ ← [📱 手机] [💻 电脑] [...] →│   │
│  └──────────────────────────────┘   │
│                                     │
│  ┌──────────────────────────────┐   │
│  │ ④ 可关闭标签栏                 │   │
│  │ [鸿蒙×] [ArkTS×] [...]       │   │
│  └──────────────────────────────┘   │
│                                     │
│  ← 纵向滑动查看更多 →               │
└─────────────────────────────────────┘

10. 布局要点总结与最佳实践

10.1 核心要点回顾

通过本文的四个示例组件,我们可以总结出使用 Row 实现标签栏的核心要点:

要点一:Row 是骨架,Text 是血肉

Row 负责将标签水平排列,Text 负责呈现标签的视觉样式。二者配合,形成「容器 + 内容」的黄金组合。

要点二:圆角 + 内边距 = 标签感

.borderRadius(16)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })

这是将一个普通 Text 变成「Tag」样式的关键组合。borderRadius 使标签变为胶囊状,padding 控制标签内部留白。

要点三:@State 驱动 UI 更新

@State private selectedIndex: number = 0;

状态变量是声明式 UI 的引擎。改变状态,UI 自动响应。

要点四:条件表达式控制样式

.fontColor(condition ? activeColor : inactiveColor)

三目运算符与链式 API 的结合,让条件样式简洁而明确。

要点五:数组操作触发刷新

this.tags = [...this.tags];

修改 @State 数组时需要重新赋值引用来触发更新。

10.2 最佳实践清单

实践 说明 优先级
使用接口定义数据结构 TagItem,提升代码可维护性 ⭐⭐⭐
组件化拆分 每组标签栏独立为 @Component,职责清晰 ⭐⭐⭐
状态局部化 将 @State 放在最需要它的组件内部 ⭐⭐⭐
滚动适配 标签多时用 Scroll 包裹 Row ⭐⭐
使用 key ForEach 的第三个参数传递唯一 key ⭐⭐
统一样式常量 将颜色、字号等提取为常量或资源文件 ⭐⭐
考虑触摸区域 padding 最小 6vp,保证手指可点

10.3 性能优化建议

  1. 控制标签数量:单行标签建议不超过 8 个,过多标签可采用「显示更多」折叠
  2. 避免深度嵌套:Row 嵌套不超过 3 层,否则影响布局性能
  3. 使用 LazyForEach:如果标签数量极大(50+),建议替换 ForEachLazyForEach
  4. 减少不必要的状态变量:只对需要驱动的 UI 的变量使用 @State

11. 常见问题与调试技巧

11.1 标签换行问题

问题:标签数量过多时自动换行,导致布局错乱。

解法:在 Row 外层包裹 Scroll,并设置 scrollable(ScrollDirection.Horizontal)。Row 本身不支持换行,超出部分截断或滚动正是其预期行为。

11.2 点击不响应

问题:点击标签没有切换高亮。

排查步骤

  1. 确认 @State selectedIndex 是否正确声明
  2. 确认 .onClick() 回调中是否正确更新了 this.selectedIndex
  3. 确认 ForEach 的键值函数是否稳定(避免每次都变化导致组件重建)

11.3 删除标签后 UI 不更新

问题:点击 × 删除按钮后,标签从数组中移除了但界面没变化。

原因@State 装饰器监听的是数组的引用变化,而不是内容变化。splice 只是修改了内容,引用没变。

解法

this.tags.splice(index, 1);
this.tags = [...this.tags];  // 创建新数组,触发 UI 更新

11.4 渐变颜色不生效

问题:设置了 linearGradient 但标签背景没有渐变效果。

检查点

  • direction 值是否正确?GradientDirection.RightBottom 而不是 BottomRight
  • colors 参数格式是否为 [['#color', position], ...]
  • 是否同时设置了 backgroundColor?渐变和纯色可以叠加使用,但纯色会作为底色

11.5 Scroll 不滚动

问题:包裹了 Scroll 但内容不能滚动。

排查

  • 确认 .scrollable() 设置了正确的方向
  • 确认内部内容尺寸确实超过了 Scroll 的尺寸(加个临时背景色验证)
  • 检查父容器是否限制了 Scroll 的高度

11.6 编译错误速查

错误 可能原因 修复
Property 'xxx' does not exist on type API 名称拼写错误 查询官方 API 参考
Cannot find name 'GradientDirection' 未导入或写错了 检查大小写:GradientDirection
Type 'undefined' is not assignable 条件表达式分支类型不一致 确保两个分支的类型兼容

12. 实际项目中的集成建议

掌握了标签栏的基础实现之后,让我们进一步探讨如何将这套方案应用到真实的生产环境中。实际项目往往面临更多的细节问题和跨页面复用需求,本节给出一些经过验证的集成策略。

12.1 与路由系统配合

在大型应用中,标签栏通常不仅是一个静态组件,还需要与页面跳转、数据传递等功能协同工作。通过 router 模块可以实现标签与页面的映射:

import { router } from '@kit.ArkUI';

// 在标签点击回调中执行页面跳转
.onClick(() => {
  this.selectedIndex = index;
  // 根据标签索引跳转到不同的目标页面
  switch (index) {
    case 0:
      router.pushUrl({ url: 'pages/HomePage' });
      break;
    case 1:
      router.pushUrl({ url: 'pages/NewsPage' });
      break;
    case 2:
      router.pushUrl({ url: 'pages/TutorialPage' });
      break;
    default:
      break;
  }
})

这种模式适用于底部导航栏与顶部标签栏联动的场景,例如一个新闻应用的分类切换功能。

12.2 多级标签联动

有些场景需要两级标签的联动效果——一级标签切换时,二级标签列表随之变化。可以通过父组件的状态管理来实现:

@Component
struct MultiLevelTagBar {
  @State private primaryIndex: number = 0;

  // 分类数据:每个一级标签对应一组二级标签
  private categories: { title: string; subTags: TagItem[] }[] = [
    {
      title: '编程语言',
      subTags: [
        { label: 'ArkTS', selected: true },
        { label: 'Java', selected: false },
        { label: 'C++', selected: false }
      ]
    },
    {
      title: '开发框架',
      subTags: [
        { label: 'ArkUI', selected: true },
        { label: 'Compose', selected: false },
        { label: 'SwiftUI', selected: false }
      ]
    }
  ];

  build() {
    Column({ space: 12 }) {
      // 一级标签栏
      Row({ space: 8 }) {
        ForEach(this.categories, (cat, index) => {
          Text(cat.title)
            .fontSize(15)
            .fontColor(this.primaryIndex === index ? '#FFFFFF' : '#333333')
            .backgroundColor(this.primaryIndex === index ? '#007AFF' : '#F0F0F0')
            .borderRadius(8)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 })
            .onClick(() => {
              this.primaryIndex = index;
            })
        })
      }

      // 二级标签栏(随一级标签联动变化)
      Row({ space: 10 }) {
        ForEach(this.categories[this.primaryIndex].subTags, (tag) => {
          Text(tag.label)
            .fontSize(13)
            .fontColor('#666666')
            .backgroundColor('#F5F5F5')
            .borderRadius(12)
            .padding({ left: 12, right: 12, top: 4, bottom: 4 })
        })
      }
    }
    .width('100%')
    .padding(12)
  }
}

多级标签联动在电商平台的分类筛选页中尤为常见,例如「品类 → 品牌 → 价格区间」的递进式筛选。

12.3 标签栏的尺寸适配策略

不同设备的屏幕宽度差异很大,标签栏在不同尺寸下需要有不同的表现策略:

屏幕宽度 标签数量 推荐方案
< 360vp ≤ 4 个 固定宽度均分
360~480vp 4~6 个 自适应宽度 + 滚动
> 480vp 6 个以上 滚动模式

对于固定宽度均分方案,可以给每个标签设置相同的 layoutWeight 权重值:

Row() {
  ForEach(this.tags, (item: TagItem) => {
    Text(item.label)
      .layoutWeight(1)    // 每个标签平分 Row 的宽度
      .textAlign(TextAlign.Center)
      // ...样式属性
  })
}

这种方式适合标签数量少且长度相近的场景,如底部 Tab 导航。

12.4 从 @State 到 @Link 的状态提升

当前示例中每个标签栏组件的状态都是内部管理的。实际项目中,标签栏的选中状态往往需要与父组件或其他兄弟组件共享。这时需要将状态「提升」到父组件,通过 @Link 装饰器实现双向绑定:

// 父组件
@State currentTag: string = '全部';

build() {
  TagBarShared({
    tags: this.selectableTags,
    currentTag: $currentTag    // 通过 $ 语法传递 @Link
  })
}

// 子组件
@Component
struct TagBarShared {
  private tags: TagItem[] = [];
  @Link currentTag: string;

  build() {
    Row({ space: 10 }) {
      ForEach(this.tags, (item: TagItem) => {
        Text(item.label)
          .fontColor(this.currentTag === item.label ? '#FFFFFF' : '#666666')
          .backgroundColor(this.currentTag === item.label ? '#007AFF' : '#F5F5F5')
          .borderRadius(16)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
          .onClick(() => {
            this.currentTag = item.label;    // 修改 @Link 变量会同步到父组件
          })
      })
    }
  }
}

@Link 的好处在于:子组件修改 currentTag 时,父组件的对应状态也会同步更新,反之亦然。这种双向数据流在复杂页面中非常实用。

12.5 与数据请求的集成

大部分真实场景中,标签栏的数据来自后端接口。结合 @State 和异步请求,可以实现动态加载的标签栏:

import { http } from '@kit.NetworkKit';

@Entry
@Component
struct DynamicTagBarPage {
  @State tags: TagItem[] = [];
  @State loading: boolean = true;

  aboutToAppear() {
    this.fetchTags();
  }

  async fetchTags() {
    try {
      this.loading = true;
      // 模拟网络请求
      let response = await fetch('https://api.example.com/tags');
      let data = await response.json();
      this.tags = data.map((item: string) => ({
        label: item,
        selected: false
      }));
    } catch (error) {
      console.error('获取标签数据失败:', error);
    } finally {
      this.loading = false;
    }
  }

  build() {
    Column() {
      if (this.loading) {
        // 加载中显示占位效果
        LoadingProgress()
          .width(32)
          .height(32)
      } else {
        TagBar({ tags: this.tags, title: '动态标签', desc: '从服务器加载的标签数据' })
      }
    }
    .width('100%')
    .padding(16)
  }
}

注意 aboutToAppear 生命周期——它在组件即将显示时触发,适合执行数据加载逻辑。加载过程中显示 LoadingProgress 组件,加载完成后渲染标签栏。

12.6 无障碍适配建议

为了让标签栏对所有用户都友好,需要关注无障碍访问(Accessibility)的相关配置:

Text(item.label)
  .accessibilityText(item.label)           // 读屏软件朗读的文本
  .accessibilityLevel('auto')               // 启用无障碍聚焦
  .accessibilityDescription('点击选择分类')   // 补充描述

良好的无障碍设计不仅是对特殊需求用户的尊重,也是应用上架 HarmonyOS 应用市场的推荐实践。

12.7 标签栏的样式主题化

如果应用支持深色模式,标签栏的配色也需要跟随主题切换。可以使用 @Styles@Extend 来定义可复用的主题样式:

// 定义主题色变量
@Styles function tagActiveStyle() {
  .backgroundColor('#007AFF')
  .fontColor('#FFFFFF')
}

@Styles function tagInactiveStyle() {
  .backgroundColor('#F5F5F5')
  .fontColor('#666666')
  .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Solid })
}

// 在标签组件中使用
Text(item.label)
  .fontSize(14)
  .borderRadius(16)
  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
  .tagActiveStyle()       // 实际使用时需要条件判断

深色模式下,建议将未选中标签的背景色从 #F5F5F5 调整为 #333333,文字颜色从 #666666 调整为 #CCCCCC,以保证对比度符合 WCAG 标准。

12.8 标签栏在横屏模式下的适配

HarmonyOS 设备支持横竖屏切换,标签栏在横屏模式下需要充分利用横向空间。一种策略是增加标签的内边距,让标签在视觉上更舒展:

// 监听屏幕方向变化
@State private isLandscape: boolean = false;

aboutToAppear() {
  let callback = (info: window.Configuration) => {
    this.isLandscape = info.isLandscape;
  };
  window.getLastWindow(getContext(), (err, win) => {
    win.on('configurationChange', callback);
  });
}

// 根据横竖屏调整标签内边距
Text(item.label)
  .padding({
    left: this.isLandscape ? 24 : 16,
    right: this.isLandscape ? 24 : 16,
    top: this.isLandscape ? 8 : 6,
    bottom: this.isLandscape ? 8 : 6
  })

横屏模式下更大的内边距让标签在宽屏幕上不会显得过于局促,提升视觉舒适度。

12.9 自定义标签形状

除了标准的胶囊圆角形状,实际项目中有时需要方形标签或半圆形标签。调整 borderRadius 即可实现不同形状:

// 胶囊形(标准)
.borderRadius(16)

// 方形(小圆角)
.borderRadius(4)

// 左侧半圆 + 右侧方形(特殊形状)
.borderRadius({ topLeft: 16, bottomLeft: 16, topRight: 4, bottomRight: 4 })

// 药丸形(极大圆角)
.borderRadius(50)

ArkTS 的 borderRadius 支持分别设置四个角的值,为标签形状提供了极大的灵活性。

12.10 单元测试策略

在自动化测试中,标签栏的测试点主要包括:

测试用例 预期结果 测试方法
点击标签 选中态高亮,其他标签取消高亮 遍历标签列表,逐个点击并检查样式
删除标签 标签从列表中移除 点击 × 按钮,检查标签数量减少
传入空数组 标签栏无内容,不报错 检查组件是否正常渲染
大量标签 超出部分可滚动 检查 Scroll 是否存在且可滑动

在 DevEco Studio 中,可以使用 @ohos/hypium 测试框架为组件编写单元测试,确保标签栏在各种边界条件下都能稳定工作。


13. 结语

13.1 本文总结

本文通过一个完整的 HarmonyOS NEXT 示例应用,详细讲解了如何使用 ArkTS 的 Row 容器实现标签栏布局。我们从 Row 的布局原理入手,逐步构建了四种不同风格的标签栏组件:

  1. 基础标签栏——展示了 Row + Text 的最简组合
  2. 可切换标签栏——演示了 @State 状态驱动的交互模式
  3. 图标+文字标签栏——引入了 Emoji 和渐变背景,增强了视觉表现
  4. 可关闭标签栏——通过嵌套 Row 和数组操作,实现了复合标签组件

每种实现都配有完整的中文代码注释,并附带了布局要点的说明。希望读者能够通过本文,举一反三,将 Row 布局应用到更多实际场景中。

13.2 进一步探索的方向

  • LazyForEach + 大量标签:当标签数量达到几十甚至上百个时,使用 LazyForEach 实现按需加载
  • 拖拽排序标签:结合 DragEvent API 实现标签的拖拽重排
  • 标签分组:多行多列的标签云布局,可以用 Flex + Wrap 实现
  • 自定义手势:通过 PanGestureSwipeGesture 为标签添加滑动删除功能
  • 主题适配:根据深色/浅色模式自动切换标签配色,使用 @Styles@Extend 复用样式

13.3 写在最后

HarmonyOS NEXT 的 ArkTS 声明式 UI 框架为开发者提供了强大而简洁的布局能力。Row 容器虽小,却是构建高效 UI 的重要基石。掌握 Row + Text 组合实现标签栏的技巧,不仅可以直接应用到项目中,更能帮助开发者理解「组件化 + 状态驱动」的声明式开发核心理念。

本文示例代码已完整发布于项目 entry/src/main/ets/pages/Index.ets,API 版本 24,可直接在 DevEco Studio 中运行预览。


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐