【共创季稿事节】鸿蒙原生 ArkTS 布局实践:用 Row 实现标签栏(Tag Bar)布局
鸿蒙原生 ArkTS 布局实践:用 Row 实现标签栏(Tag Bar)布局
目录
- 引言:为什么选择 Row 实现标签栏
- 项目环境与准备工作
- 核心布局容器:Row 深度解析
- 标签栏组件化设计思路
- 基础标签栏:最简单的 Row + Text 组合
- 可切换标签栏:状态管理与交互反馈
- 图标+文字标签栏:丰富视觉表现力
- 可关闭标签栏:复合组件与数组操作
- 页面整合与滚动适配
- 布局要点总结与最佳实践
- 常见问题与调试技巧
- 实际项目中的集成建议
- 结语
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 中的 LinearLayout 与 FlexboxLayout。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 装饰器声明一个可复用的组件。tags、title、desc 为父组件传入的属性,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 组件展示了两种增强手段:
- Emoji 图标:直接在文本中嵌入 Emoji 字符,无需额外资源文件
- 渐变背景:选中时使用
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 装饰器标记。它的职责是:
- 定义页面标题和描述
- 提供标签数据(
basicTags、selectableTags) - 组装并排列各个子组件(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.json 的 src 数组中的第一个页面即为应用的启动页。多个页面可以注册为数组中的多个元素,通过 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 性能优化建议
- 控制标签数量:单行标签建议不超过 8 个,过多标签可采用「显示更多」折叠
- 避免深度嵌套:Row 嵌套不超过 3 层,否则影响布局性能
- 使用 LazyForEach:如果标签数量极大(50+),建议替换
ForEach为LazyForEach - 减少不必要的状态变量:只对需要驱动的 UI 的变量使用 @State
11. 常见问题与调试技巧
11.1 标签换行问题
问题:标签数量过多时自动换行,导致布局错乱。
解法:在 Row 外层包裹 Scroll,并设置 scrollable(ScrollDirection.Horizontal)。Row 本身不支持换行,超出部分截断或滚动正是其预期行为。
11.2 点击不响应
问题:点击标签没有切换高亮。
排查步骤:
- 确认
@State selectedIndex是否正确声明 - 确认
.onClick()回调中是否正确更新了this.selectedIndex - 确认
ForEach的键值函数是否稳定(避免每次都变化导致组件重建)
11.3 删除标签后 UI 不更新
问题:点击 × 删除按钮后,标签从数组中移除了但界面没变化。
原因:@State 装饰器监听的是数组的引用变化,而不是内容变化。splice 只是修改了内容,引用没变。
解法:
this.tags.splice(index, 1);
this.tags = [...this.tags]; // 创建新数组,触发 UI 更新
11.4 渐变颜色不生效
问题:设置了 linearGradient 但标签背景没有渐变效果。
检查点:
direction值是否正确?GradientDirection.RightBottom而不是BottomRightcolors参数格式是否为[['#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 的布局原理入手,逐步构建了四种不同风格的标签栏组件:
- 基础标签栏——展示了 Row + Text 的最简组合
- 可切换标签栏——演示了 @State 状态驱动的交互模式
- 图标+文字标签栏——引入了 Emoji 和渐变背景,增强了视觉表现
- 可关闭标签栏——通过嵌套 Row 和数组操作,实现了复合标签组件
每种实现都配有完整的中文代码注释,并附带了布局要点的说明。希望读者能够通过本文,举一反三,将 Row 布局应用到更多实际场景中。
13.2 进一步探索的方向
- LazyForEach + 大量标签:当标签数量达到几十甚至上百个时,使用
LazyForEach实现按需加载 - 拖拽排序标签:结合
DragEventAPI 实现标签的拖拽重排 - 标签分组:多行多列的标签云布局,可以用 Flex + Wrap 实现
- 自定义手势:通过
PanGesture和SwipeGesture为标签添加滑动删除功能 - 主题适配:根据深色/浅色模式自动切换标签配色,使用
@Styles和@Extend复用样式
13.3 写在最后
HarmonyOS NEXT 的 ArkTS 声明式 UI 框架为开发者提供了强大而简洁的布局能力。Row 容器虽小,却是构建高效 UI 的重要基石。掌握 Row + Text 组合实现标签栏的技巧,不仅可以直接应用到项目中,更能帮助开发者理解「组件化 + 状态驱动」的声明式开发核心理念。
本文示例代码已完整发布于项目
entry/src/main/ets/pages/Index.ets,API 版本 24,可直接在 DevEco Studio 中运行预览。



更多推荐



所有评论(0)