鸿蒙原生 ArkTS Grid 布局实战:从零构建管理后台 Dashboard 仪表盘



一、前言
2024 年第四季度,HarmonyOS NEXT 正式面向开发者发布,标志着鸿蒙操作系统彻底剥离了 Android 兼容层,以纯正的鸿蒙内核与鸿蒙原生的 ArkTS 声明式 UI 框架走向前台。对于应用开发者而言,这意味着我们需要重新学习一套完整的 UI 布局体系------不再是 XML 布局文件,不再是传统的 View 树,不再是 findViewById 式的节点查找,而是用 TypeScript 语法风格编写的声明式组件树,配合链式属性调用和装饰器驱动的响应式数据流。
在众多原生布局组件中,Grid(网格布局) 是一个极为强大而又容易被低估的组件。它类似于 CSS Grid Layout,但原生集成在 ArkUI 框架中,拥有更好的性能表现和更简洁的链式调用 API。尤其是在构建管理后台的仪表盘(Dashboard)页面时,Grid 几乎是不二之选。Dashboard 天然具有网格化的视觉特征:多行多列的卡片、不同尺寸的区域块、规整的信息密度分布------这些需求与 Grid 的设计哲学高度吻合。
本文将从一个完整的实战案例出发,深入剖析鸿蒙 ArkTS 中 Grid 布局的核心用法、高级技巧和最佳实践,帮助读者快速掌握这一布局利器,并在实际项目中灵活运用。
二、开发环境与项目初始化
2.1 环境要求
开始本教程之前,请确保您的开发环境满足以下条件:
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10/11、macOS 13+ 或 Ubuntu 22.04+ |
| DevEco Studio | 5.0 及以上版本 |
| HarmonyOS SDK | API 24(HarmonyOS NEXT) |
| Node.js | 18.x 及以上(DevEco Studio 内置) |
| ohpm | 鸿蒙包管理器(DevEco Studio 内置) |
2.2 创建项目
打开 DevEco Studio,选择 "Create Project",然后选择 "Empty Ability" 模板,输入项目名称和包名,SDK 选择 API 24。项目创建完成后,默认会生成 entry/src/main/ets/pages/Index.ets 文件,这就是我们编写 Dashboard 页面的入口文件。
2.3 项目结构说明
一个标准的 HarmonyOS NEXT 应用项目结构如下:
ProjectRoot/
├── entry/ # 应用主模块
│ ├── src/main/ets/ # ArkTS 源代码目录
│ │ ├── entryability/ # Ability 生命周期管理
│ │ ├── pages/ # 页面文件(Index.ets 所在目录)
│ │ └── ...
│ ├── src/main/resources/ # 资源文件(字符串、颜色、图片等)
│ └── build-profile.json5 # 模块构建配置
├── oh_modules/ # ohpm 依赖包
├── build-profile.json5 # 项目级构建配置
└── hvigor/ # 构建工具配置
我们的 Dashboard 页面将全部写在 pages/Index.ets 中,通过 @Entry 装饰器将其注册为入口页面。
三、Dashboard 仪表盘布局的挑战与设计思路
管理后台的仪表盘页面通常面临以下几个布局痛点和设计挑战:
3.1 内容密度高,信息层级复杂
一个典型的 Dashboard 需要同时展示:顶部标题栏、若干核心指标卡片(如用户数、收入、订单量、活跃度)、趋势图表(柱状图或折线图)、最近动态列表、待办事项、消息通知、快捷操作入口等。这些内容的信息权重各不相同,有的需要突出显示(如核心 KPI),有的则作为辅助信息(如操作入口)。因此需要一种既灵活又有序的布局方案来承载不同权重的信息块。
3.2 卡片尺寸不统一,需要跨列/跨行
核心指标卡片通常是等宽等高的"小方块",但趋势图表和活动列表往往需要更大的显示区域------它们可能需要占用 2 列甚至 2 行。如果使用传统 LinearLayout(线性布局)或 Flex 布局,实现这种"不规则网格"会非常棘手,往往需要嵌套多层容器,导致布局臃肿、性能下降,代码也难以维护。
3.3 响应式适配要求
虽然本文的示例以固定列数演示,但 Dashboard 通常需要适配手机、平板、折叠屏等多种设备。Grid 的 columnsTemplate 使用 1fr 弹性单位,天然支持按比例伸缩,结合 MediaQuery 可以轻松实现响应式断点切换,无需为每种屏幕尺寸编写独立的布局文件。
3.4 鸿蒙 ArkTS 的布局哲学
在 ArkTS 中,一切皆是组件。布局不是写在 XML 或 JSON 中的静态描述,而是通过组件构造器链式调用 .属性() 动态构建的。这种风格与 SwiftUI 的 DSL 类似,强调代码即 UI,减少了上下文切换的认知负担。同时,ArkTS 在编译期会进行严格的类型检查,避免了很多运行时才能发现的问题。
3.5 设计思路总结
基于以上挑战,我们的 Dashboard 设计遵循以下原则:
- 网格化布局:使用 Grid 容器将页面划分为规整的网格,每个网格单元放置一张卡片
- 内容分区:将页面分为顶部标题区、核心指标区、图表与动态区、底部功能区四个垂直区块
- 视觉层次:通过卡片颜色、阴影深度、字体大小建立清晰的视觉层级
- 交互反馈:为操作型卡片提供点击反馈(Toast 提示),增强交互感知
- 数据驱动 :使用
@State管理卡片数据,便于后续接入真实 API
四、Grid 布局核心概念速览
在深入代码之前,我们先系统梳理 Grid 布局的核心 API,为后续实战打好基础。
4.1 Grid 容器
typescript
Grid() {
// GridItem 子组件
}
Grid 是一个容器组件,它的直接子组件必须是 GridItem。这一点与 Row 和 Column 有本质区别------Row 和 Column 的子组件可以是任意类型的组件(Text、Image、Button 等),但 Grid 要求显式使用 GridItem 将每个子项包裹起来。这个设计约束保证了 Grid 能够精确控制每个单元格的位置和跨度。
4.2 columnsTemplate / rowsTemplate
这两个属性是 Grid 的灵魂,它们定义了网格的列轨道 和行轨道,即每一列应该有多宽、每一行应该有多高:
typescript
// 4 列等宽------最常见的 Dashboard 布局
.columnsTemplate('1fr 1fr 1fr 1fr')
// 混合列宽:第一列 100vp 固定宽度,第二列自适应,第三列 2 倍弹性
.columnsTemplate('100vp 1fr 2fr')
// 2 行等高,适合双行卡片布局
.rowsTemplate('1fr 1fr')
// 固定像素和弹性单位的混合使用
.columnsTemplate('80vp 1fr 1fr 80vp')
1fr 是弹性单位(fraction unit),与 CSS Grid 的 1fr 含义完全一致------按比例分配容器中的剩余空间。三个 1fr 意味着三列各占 1/3 宽度;而 1fr 2fr 意味着第二列的宽度是第一列的 2 倍。这种弹性分配机制使得 Grid 天然具备响应式能力:无论容器宽度如何变化,各列的比例保持不变。
除了 1fr,还支持以下单位:
| 单位 | 含义 | 示例 |
|---|---|---|
1fr |
弹性比例单位 | '1fr 2fr' |
px / vp |
虚拟像素(逻辑像素) | '100vp 1fr' |
% |
百分比(相对于容器) | '25% 25% 25% 25%' |
auto |
自适应内容宽度 | 'auto 1fr' |
注意 :如果只设置 columnsTemplate 而不设置 rowsTemplate,Grid 会自动根据内容数量和容器高度计算行数。反之亦然。
4.3 columnsGap / rowsGap
控制网格线(即网格项之间的空隙)的宽度,相当于 CSS 中的 gap 属性:
typescript
.columnsGap(12) // 列间距 12vp,产生垂直的视觉分隔
.rowsGap(12) // 行间距 12vp,产生水平的视觉分隔
如果希望行列间距相同,也可以只设置一个,但 ArkTS 目前要求两个属性分别设置。12vp 是一个经过大量实践验证的"黄金间距",既能清晰区分不同的卡片,又不会浪费屏幕空间。
4.4 GridItem 与跨列/跨行
每个 GridItem 默认占据一个单元格(即网格中的一个"格子")。通过 .columnStart() / .columnEnd() 和 .rowStart() / .rowEnd(),可以让 GridItem 跨越多个网格单元,实现"大卡片"效果:
typescript
GridItem() {
// 占据 2 列宽的卡片内容
}
.columnStart(0) // 从第 0 列开始(列索引从 0 开始计数)
.columnEnd(1) // 到第 1 列结束(含结束列,0~1 共 2 列)
重要的细节 :
columnStart和columnEnd使用列索引 (从 0 开始),且columnEnd是包含边界 的。因此columnStart(0).columnEnd(1)跨越第 0 列和第 1 列,共计 2 列。如果需要跨越 3 列,则写为.columnStart(0).columnEnd(2)。
对于 rowStart / rowEnd,逻辑与列完全相同:
typescript
GridItem()
.columnStart(0).columnEnd(1) // 跨 2 列
.rowStart(0).rowEnd(1) // 跨 2 行
跨列和跨行可以同时使用,形成一个占据 2×2 网格的"大卡片"。
4.5 与 GridRow / GridCol 的区别
HarmonyOS 还提供了另一组网格相关组件:GridRow 和 GridCol。它们更接近 Bootstrap 的 12 列栅格系统------GridRow 定义一行,GridCol 通过 span 属性指定该列占几份宽度:
typescript
GridRow({ columns: 12 }) {
GridCol({ span: 6 }) { /* 占 6/12 = 50% 宽度 */ }
GridCol({ span: 6 }) { /* 占 6/12 = 50% 宽度 */ }
}
而 Grid + GridItem 则更接近 CSS Grid Layout,自由度更高,适合不规则网格布局。两者的选择建议如下:
| 场景 | 推荐组件 |
|---|---|
| 等分栅格、表单布局 | GridRow / GridCol |
| 不规则 Dashboard 卡片 | Grid / GridItem |
| 图片网格画廊 | Grid / GridItem |
| 多列文本列表 | GridRow / GridCol |
对于我们的 Dashboard 场景,推荐使用 Grid + GridItem,因为它可以精确控制每个卡片的跨列和跨行行为。
4.6 与其他布局组件的对比
为了帮助读者更好地理解 Grid 的定位,这里将其与 ArkTS 中的其他布局组件做简要对比:
| 组件 | 布局方向 | 适用场景 | 跨单元格能力 |
|---|---|---|---|
| Row | 水平单行 | 工具栏、按钮组、标签行 | 不支持 |
| Column | 垂直单列 | 表单、列表、内容流 | 不支持 |
| Flex | 单行/单列可换行 | 标签云、自适应排列 | 不支持 |
| Grid | 多行多列网格 | Dashboard、图片墙、卡片布局 | 支持跨列/跨行 |
| Stack | 层叠 | 叠加效果、徽标、全屏覆盖 | 不适用 |
| RelativeContainer | 相对定位 | 复杂自由布局 | 不支持网格对齐 |
五、实战:构建 Dashboard 仪表盘(代码详解)
现在让我们进入核心环节,逐块分析示例代码中的每个布局模块,理解每一行代码的作用和设计意图。
5.1 数据模型定义
在编写 UI 之前,我们首先定义了数据模型接口。这是良好的 ArkTS 编程习惯------先定义数据结构,再构建 UI:
typescript
interface StatCardData {
title: string; // 卡片标题
value: string; // 数值
unit: string; // 单位
trend: string; // 趋势描述
trendUp: boolean; // 趋势是否向好
icon: ResourceStr; // 图标 emoji
bgColor: string; // 卡片背景色
}
接口中的 trendUp: boolean 字段值得特别说明。这个布尔值直接决定了趋势文字的显示颜色:true 时显示绿色(表示增长向好),false 时显示红色(表示下降需要关注)。这是声明式 UI 中"数据驱动样式"的典型模式------UI 的呈现完全由数据决定,无需编写任何条件判断的 UI 操作代码。
5.2 整体架构
我们的 Dashboard 页面采用如下层次结构:
Scroll ← 可滚动容器
└─ Column (space: 16) ← 垂直排列各区块,间距 16vp
├─ HeaderSection ← @Builder: 顶部标题栏
├─ StatCardsGrid ← @Builder: Grid(4列) → 4 张核心指标卡片
├─ MiddleSectionGrid ← @Builder: Grid(4列) → 图表(跨2列) + 活动列表(跨2列)
└─ BottomCardsGrid ← @Builder: Grid(3列) → 三张小功能卡片
为什么用 Scroll 包裹? Dashboard 的内容高度通常超过一屏,Scroll 提供了自然的滚动能力,确保所有信息都可以被用户看到,同时避免了将页面高度写死的限制。
为什么 Column 设置 space: 16? 最外层 Column 的 space: 16 提供了各区块之间的垂直间距,这是一种简洁而有效的分隔方式------不需要为每个区块单独设置 margin 或 padding,一个属性就完成了区块间的"呼吸感"设计。
5.3 顶部标题栏(HeaderSection)
标题栏使用 Row + Column 组合实现左右布局:
typescript
@Builder
HeaderSection() {
Row() {
Column({ space: 4 }) {
Text('仪表盘').fontSize(24).fontWeight(FontWeight.Bold)
Text('欢迎回来,这里是您的管理概览').fontSize(14).fontColor('#8E8E93')
}
Blank() // ← 关键:占据中间剩余空间,实现左右对齐
Text(this.getCurrentDate())
.fontSize(14).backgroundColor('#FFFFFF').borderRadius(8)
.shadow({ radius: 2, color: 'rgba(0,0,0,0.06)' })
}
}
这里有两个设计要点:
第一 ,使用 Blank() 组件占据中间剩余空间,实现左右对齐的效果。这比设置 justifyContent(FlexAlign.SpaceBetween) 更灵活,因为当内容过长时,Blank() 会自动收缩,保证两侧内容不会被挤压到容器之外。
第二,右侧日期文字使用白色背景和轻微阴影的容器包裹,形成一种"标签"或"徽章"的视觉效果,增加了界面的精致感和细节品质。这种小细节往往能显著提升用户对产品的第一印象。
getCurrentDate() 工具方法使用了 JavaScript 原生的 Date 对象获取当前时间,并将其格式化为中文日期格式(如"2026年6月24日 星期三")。这个方法虽然简单,但展示了 ArkTS 中直接使用标准 JavaScript API 的能力。
5.4 核心指标卡片网格(StatCardsGrid)
这是最纯粹的 Grid 用法演示:
typescript
@Builder
StatCardsGrid() {
Grid() {
ForEach(this.statCards, (card: StatCardData) => {
GridItem() {
this.StatCard(card)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 4 列等宽
.rowsGap(12) // 行间距
.columnsGap(12) // 列间距
.height(140) // 固定高度
}
逐行要点分析:
第一行 .columnsTemplate('1fr 1fr 1fr 1fr'):定义了四列等宽的网格轨道。无论屏幕宽度是 360vp 的小屏手机还是 1000vp 的平板窗口,四列始终等分可用宽度。这意味着适配不同屏幕尺寸时,我们不需要修改任何布局代码------Grid 自动完成了"响应式"。
第二行 .rowsGap(12) 和第三行 .columnsGap(12):设置行列间距均为 12vp。这个间距值是 ArkUI 设计规范中推荐的常用值,它与后续卡片内部的 16vp padding 构成了"外疏内密"的双层间距体系------卡片之间的间距为 12vp,卡片内容与卡片边缘的间距为 16vp。
第四行 .height(140) :固定 Grid 容器的高度为 140vp。在 Grid 中,如果 rowsTemplate 没有显式指定,行高将由容器高度和行数共同决定。这里我们只需要一行,所以固定高度即可让卡片高度保持一致。如果使用 rowsTemplate('1fr') 效果也是等价的。
StatCard 内部布局使用了 Row(水平布局)将卡片分为左右两部分:
- 左侧:图标以圆形白色背景呈现,尺寸 52×52vp,圆角 26vp(即正圆形),带有阴影增加立体感
- 右侧:三行垂直排列------标题(小字灰色)、数值+单位(大字粗体)、趋势(带颜色的百分比)
typescript
@Builder
StatCard(card: StatCardData) {
Row() {
Text(card.icon).fontSize(28).width(52).height(52).borderRadius(26) // 左侧图标
Column({ space: 2 }) { // 右侧信息
Text(card.title).fontSize(13).fontColor('#8E8E93')
Text(card.value).fontSize(18).fontWeight(FontWeight.Bold)
Text(card.trend).fontColor(card.trendUp ? '#34C759' : '#FF3B30')
}.layoutWeight(1) // ← 关键:右侧占据剩余所有空间
}
.backgroundColor(card.bgColor) // ← 数据驱动的背景色
.borderRadius(16).shadow({...})
}
这里 .layoutWeight(1) 的作用是让右侧的 Column 占据 Row 中除图标之外的所有剩余空间,确保三行文字能够完整显示,不会被其他元素压缩。
四个卡片的背景色(bgColor)使用了四种柔和的浅色调:#E3F2FD(淡蓝)、#E8F5E9(淡绿)、#FFF3E0(淡橙)、#F3E5F5(淡紫)。这些颜色在视觉上区分了不同指标类别,同时保持了整体的轻盈感和亲和力,避免了 Dashboard 常见的"沉重感"。
5.5 中间区域:图表 + 活动列表(MiddleSectionGrid)
这是跨列布局的核心演示场景,也是本博客最重要的一段代码:
typescript
@Builder
MiddleSectionGrid() {
Grid() {
// 左半部分:柱状图卡片 --- 占 2 列
GridItem() { this.ChartCard() }
.columnStart(0)
.columnEnd(1) // 跨越第 0~1 列
// 右半部分:活动列表卡片 --- 占 2 列
GridItem() { this.ActivityCard() }
.columnStart(2)
.columnEnd(3) // 跨越第 2~3 列
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 依然 4 列模板
.rowsTemplate('1fr') // 1 行等高
.height(260)
}
设计思路 :Grid 容器定义了一个 4 列 1 行的网格,但只有两个 GridItem。第一个 GridItem 通过 .columnStart(0).columnEnd(1) 占据了第 0 和第 1 列(共 2 列),第二个 GridItem 通过 .columnStart(2).columnEnd(3) 占据了第 2 和第 3 列(共 2 列)。两个卡片各占 50% 宽度,实现了左右分屏。
如果业务需求变化,比如希望图表占 3 列、活动列表占 1 列,只需要简单调整 columnEnd 的值即可------这种灵活性是 Flex 或线性布局难以比拟的,因为 Flex 布局中实现"一个元素占 3/4、另一个占 1/4"需要精确计算 layoutWeight 的权重值。
柱状图卡片(ChartCard) 没有使用任何第三方图表库,而是纯用 Column + Row 组件手绘了简易柱状图。这一技巧展示了 ArkTS 布局组件的"图元化"能力------即使复杂的图表展示,也可以通过基础组件的组合来实现,完全不需要引入外部依赖:
typescript
// 柱状图核心代码片段
Row() {
ForEach(this.chartData, (value, index) => {
Column({ space: 4 }) {
Column() // 柱体
.width(24)
.height(value * 2) // 数据值 × 2 = 柱高
.backgroundColor('#007AFF')
.borderRadius({topLeft:4, topRight:4}) // 顶部圆角
Text(weekLabels[index]) // X 轴标签
.fontSize(11).fontColor('#8E8E93')
}.layoutWeight(1) // 每根柱子等宽
})
}
.alignItems(VerticalAlign.Bottom) // ← 关键:柱体从底部向上生长
三点实现技巧:
-
从底部生长 :外层 Row 的
.alignItems(VerticalAlign.Bottom)使所有柱体以底部为基准对齐,视觉上就像从地面向上生长------这比从顶部向下延伸的设计更符合柱状图的阅读习惯。 -
等比映射 :柱体高度由
value * 2计算得出。数据值 65~95 映射为 130~190vp 的高度,在 170vp 高的 Row 中恰好形成适中的视觉效果。如果数据范围变化,可以调整缩放系数。 -
顶部圆角 :
.borderRadius({topLeft:4, topRight:4})只给柱体顶部两个角设置圆角,底部保留直角。这样的设计模拟了现代 UI 风格中常见的"圆顶方底"柱状图样式,比全圆角柱体更显专业。
活动列表卡片(ActivityCard) 使用 ForEach 循环渲染活动条目,每个条目包含头像占位、用户名称、操作描述和时间:
typescript
ForEach(this.activities, (item, index) => {
Row({ space: 10 }) {
Text(item.avatar) // 头像 emoji
.width(36).height(36).borderRadius(18)
Column({ space: 2 }) {
Text(item.user + ' ' + item.action) // 用户名和操作
Text(item.time) // 时间戳
}.layoutWeight(1)
}
.border({ // 分割线
width: { bottom: index < this.activities.length - 1 ? 0.5 : 0 },
color: { bottom: '#E5E5EA' }
})
})
分割线的显示逻辑通过三元表达式 index < activities.length - 1 实现:只有当当前条目不是最后一条时,才在其底部绘制 0.5vp 的浅灰色分割线。这是一种常见的列表 UI 模式,在 ArkTS 中通过 .border() 链式调用优雅地实现。
5.6 底部卡片区(BottomCardsGrid)
底部使用 3 列 Grid 容纳三张功能卡片:
typescript
@Builder
BottomCardsGrid() {
Grid() {
GridItem() { this.TaskCard() } // 待办任务
GridItem() { this.MessageCard() } // 消息通知
GridItem() { this.QuickActionCard() } // 快捷操作
}
.columnsTemplate('1fr 1fr 1fr') // 3 列等宽
.columnsGap(12)
.rowsGap(12)
.height(160)
}
每张卡片都有差异化的交互设计:
- 待办任务卡片 :右上角显示红色角标数字"3",通过绝对定位(在 Row 内部使用 Blank + Text 右对齐)实现;绑定了
onClick点击事件 - 消息通知卡片:右上角显示蓝色角标数字"5",同样有点击事件
- 快捷操作卡片 :内部包含三个横向排列的操作按钮(新增、报表、设置),每个按钮通过
@Builder ActionBadge复用
ActionBadge 是一个接受 icon 和 label 两个参数的 Builder 方法,被调用了三次来创建三个功能按钮。这再次展示了 @Builder 的复用价值------相同的 UI 模式,只需编写一次定义、多次调用。
5.7 数据模型与 @Builder 封装的协同
整个页面使用了两个接口(StatCardData 和 ActivityItem)来结构化数据,配合 @State 装饰器实现响应式更新。当数据变化时,UI 会自动重新渲染受影响的部分,无需手动执行业务逻辑与 UI 的同步操作。
@Builder 是 ArkTS 中封装复用 UI 片段的利器。在本例中,我们将标题栏、统计卡片、图表卡片、活动卡片、底部卡片等每个独立区域都封装为 @Builder 方法,使 build() 方法结构清晰,就像在阅读页面的目录结构一样:
typescript
build() {
Scroll() {
Column({ space: 16 }) {
this.HeaderSection() // ← 每个 @Builder 就像一段"口语化"的标签
this.StatCardsGrid()
this.MiddleSectionGrid()
this.BottomCardsGrid()
}
}
}
与将 UI 提取为独立 @Component 相比,@Builder 的三个核心优势:
- 直接访问成员变量 :可以读取外层 struct 的
@State变量和普通成员,无需通过参数传递,减少了数据传递的样板代码 - 轻量级 :不需要额外定义 struct、不需要
build()方法、不需要@Component装饰器,代码量更少 - 内聚性强:同一文件中即可完成所有 UI 定义,对于中等复杂度的页面,避免了文件数量过多的问题
当然,如果某个卡片需要在多个页面复用,或者逻辑复杂到需要独立的管理状态,就应该将其抽取为独立的 @Component。
六、Grid 布局的进阶技巧与扩展
在掌握了基础用法之后,让我们探索一些更高级的 Grid 实践技巧。
6.1 动态列数与响应式适配
如果需要根据屏幕宽度动态调整列数,可以结合 MediaQuery 或通过 @StorageProp 获取窗口宽度:
typescript
@StorageProp('windowWidth') windowWidth: number = 360;
get columns(): number {
if (this.windowWidth >= 1200) return 6; // 大屏:6 列
if (this.windowWidth >= 840) return 4; // 中屏:4 列
if (this.windowWidth >= 600) return 3; // 小屏:3 列
return 2; // 手机竖屏:2 列
}
模板字符串也支持动态拼接,通过 String.prototype.repeat() 方法生成重复的 fr 单位:
typescript
.columnsTemplate('1fr '.repeat(this.columns).trim())
// 当 columns = 4 时,生成 '1fr 1fr 1fr 1fr'
这种方式可以灵活适应不同的屏幕尺寸,是实现响应式 Dashboard 的关键技术。
6.2 使用 rowSpan 实现跨行
除了跨列,GridItem 也可以跨行。例如让一张大图卡片占据 2 行 2 列,形成一个"特色展位":
typescript
GridItem()
.columnStart(0).columnEnd(1) // 跨 2 列(第 0~1 列)
.rowStart(0).rowEnd(1) // 跨 2 行(第 0~1 行)
这在图片墙、产品展示、运营推广位等场景中非常实用。跨列和跨行的组合使用可以创造出丰富的"不规则但有序"的视觉布局。
6.3 Grid 嵌套
Grid 可以嵌套使用------在一个 GridItem 内部再放一个 Grid 来实现子网格布局。例如,一个"团队业绩"卡片内部可能包含一个 2×2 的子网格来展示四个成员的指标数据:
typescript
GridItem() {
Grid() {
GridItem() { MemberCard({ name: '张三', score: 98 }) }
GridItem() { MemberCard({ name: '李四', score: 87 }) }
GridItem() { MemberCard({ name: '王五', score: 92 }) }
GridItem() { MemberCard({ name: '赵六', score: 78 }) }
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
}
但要注意避免过深的嵌套(超过 3 层),以免影响渲染性能。在 ArkUI 中,嵌套层数越深,布局计算的开销越大。
6.4 与 Swiper 结合实现轮播 Banner
Dashboard 顶部的运营 Banner 可以使用 Swiper 组件实现,将其放入一个 GridItem 中,与卡片网格共存:
typescript
Grid() {
GridItem() {
Swiper() {
// 轮播项
}
.autoPlay(true)
.interval(3000)
.indicator(true)
}
.columnStart(0).columnEnd(3) // 横跨整个 Grid 宽度
GridItem() { /* 其他卡片 */ }
// ...
}
6.5 性能优化建议
虽然 Dashboard 场景下的 GridItem 数量通常不多(一般不超过 20 个),但当数据量较大时,以下优化手段值得了解:
| 优化手段 | 说明 | 适用场景 |
|---|---|---|
cachedCount |
预缓存离屏 GridItem 数量 | 列表滚动场景 |
LazyForEach |
数据懒加载,只渲染可见区域的项 | 大规模数据列表 |
| 避免深层嵌套 | 布局层数不超过 3~4 层 | 所有场景 |
减少 @State 监听范围 |
将大对象拆分为独立的状态变量 | 频繁更新的复杂页面 |
6.6 列表卡片性能优化
在活动列表卡片中,如果活动条目数量很大(如 50+ 条),可以考虑将 ForEach 替换为 LazyForEach,后者只渲染当前可见区域的项,大幅减少组件树的节点数量:
typescript
LazyForEach(this.activityDataSource, (item: ActivityItem) => {
GridItem() { /* 活动条目 UI */ }
}, (item: ActivityItem) => item.user + item.time) // keyGenerator
但在 Dashboard 场景下,活动列表通常只显示最近 5~10 条,ForEach 完全够用。
七、卡片设计的最佳实践
好的 Dashboard 不仅是功能完整的,更应该是赏心悦目的。以下是一些经过验证的卡片设计原则。
7.1 卡片的高度一致性
在同一行 Grid 中的卡片应尽量保持高度一致。本文示例中,StatCardsGrid 固定高度 140vp,BottomCardsGrid 固定高度 160vp。如果不固定高度,Grid 的行高将由该行中最高的 GridItem 决定,可能导致其他卡片被不必要地拉伸,破坏视觉平衡。
对于内容较多的卡片(如我们的图表卡片高度为 260vp),可以单独分配一行或使用 rowSpan 跨越多行,而不影响其他卡片的显示。
7.2 色彩语言与信息层级
Dashboard 的卡片色彩应该传达信息层级和情感暗示:
| 卡片类型 | 推荐色彩方案 | 心理暗示 | 应用场景 |
|---|---|---|---|
| 核心 KPI | 品牌主色背景 + 白色文字 | 权威、清晰、信任 | 用户数、收入 |
| 趋势图表 | 蓝/青色系 | 专业、理性、可信 | 增长趋势、分析 |
| 警告/异常 | 橙/红色系 | 紧迫、需要关注 | 错误率、告警 |
| 成功/增长 | 绿色系 | 正面、积极、安全 | 完成率、增长率 |
| 中性信息 | 灰/浅色系 | 辅助、次要 | 消息、日志 |
| 操作入口 | 品牌色或渐变色 | 行动引导 | 新增、设置 |
7.3 间距与呼吸感
Grid 的 columnsGap 和 rowsGap 设置为 12vp 是一个经过大量鸿蒙应用验证的"黄金间距"。配合卡片内部的 16vp padding,形成了外部 12vp + 内部 16vp 的双层呼吸感结构。这种"外疏内密"的设计可以让用户的目光自然地聚焦在卡片内部的内容上,同时卡片之间有清晰的分隔。
为什么不是 8vp 或 20vp?8vp 的间距在卡片内容较多时会显得拥挤,缺乏区分度;而 20vp 在手机等小屏设备上会浪费宝贵的屏幕空间。12vp 恰好处于"刚刚好"的平衡点。
7.4 阴影层级与交互反馈
本文示例使用了统一的浅阴影:
typescript
.shadow({
radius: 6,
color: 'rgba(0, 0, 0, 0.06)',
offsetX: 0,
offsetY: 2
})
在实际项目中,可以根据卡片的交互状态设计不同的阴影层级:
- 默认态:浅阴影(radius: 6, color: rgba(0,0,0,0.06))
- 悬浮态(鼠标悬停或手指触摸):加深阴影(radius: 12, color: rgba(0,0,0,0.12))
- 点击态:内凹阴影或缩小效果,模拟按下
这种阴影层级的差异化设计可以传递卡片的"可交互性"暗示,提升用户的操作感知。
八、从 ArkTS 角度看声明式 UI 的优势
通过这个 Dashboard 项目,我们可以清晰地感受到 ArkTS 声明式 UI 与传统命令式 UI 开发之间的几个根本性差异。
8.1 UI = f(state):数据驱动 UI
@State 装饰的变量是 UI 的"单一数据源"。当数据变化时,框架自动重新渲染受影响的组件,开发者不再需要手动调用 setText()、setBackgroundColor()、notifyDataSetChanged() 等方法。这种"UI 是状态的函数"的范式,将开发者从繁琐的 UI 同步逻辑中解放出来,让他们能够专注于业务数据的处理。
8.2 链式 API 的可读性与可维护性
typescript
Text('Hello')
.fontSize(16)
.fontColor('#1A1A2E')
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
这种链式调用有着以下显著优势:
- 上下文集中:一个组件的所有属性设置集中在一处,无需在不同代码块之间跳转
- 天然的分组 :每个
.方法()独立一行,通过缩进可以清晰看到组件有哪些属性被设置 - 顺序无关:属性设置的顺序不影响最终结果,减少了排序相关的认知负担
- 编译期检查:属性名称和方法参数在编译时就会进行类型校验
8.3 类型安全:编译期发现错误
ArkTS 是 TypeScript 的超集,所有组件属性和事件都在编译期进行类型检查。例如,FontWeight 只接受预定义的枚举值(Bold、Medium、Regular 等),如果传入了错误的字符串,编译会直接报错,而不是等到运行时报错或默默忽略。这种编译期的安全保障在大型团队协作项目中极具价值。
8.4 装饰器体系
ArkTS 提供了丰富的装饰器(@Entry、@Component、@State、@Builder、@Prop、@Link、@Watch 等),每个装饰器都有明确的语义和职责:
| 装饰器 | 职责 |
|---|---|
@Entry |
标记页面入口 |
@Component |
定义可复用的组件单元 |
@State |
声明组件内部状态变量 |
@Builder |
封装 UI 片段为可复用方法 |
@Prop |
接收父组件传递的单向数据 |
@Link |
与父组件建立双向数据绑定 |
@Watch |
监听状态变量的变化并执行回调 |
这种装饰器驱动的编程模型,使得代码的语义非常清晰------看到 @State 就知道这是一个会引发 UI 重新渲染的响应式变量。
九、常见问题与调试技巧
在实际开发中,初学者使用 Grid 布局时常会遇到一些问题。这里整理了一些高频问题及其解决方案。
9.1 GridItem 不显示或位置错乱
原因:最常见的原因是忘记了 Grid 的直接子组件必须是 GridItem。如果在 Grid 中直接放置 Text、Row 等组件,这些组件不会被渲染。
解决方案:确保 Grid 的所有直接子组件都是 GridItem,内容放置在 GridItem 内部。
typescript
// ❌ 错误
Grid() {
Text('Hello') // 不会显示!
}
// ✅ 正确
Grid() {
GridItem() {
Text('Hello') // 正常显示
}
}
9.2 跨列/跨行设置不生效
原因 :columnEnd 的值设置不正确。常见错误是忘记 columnEnd 是包含结束索引的。
解决方案 :牢记公式------跨 N 列时,columnEnd = columnStart + N - 1。
typescript
// 跨 3 列:从第 0 列到第 2 列(包含)
.columnStart(0).columnEnd(2)
// 跨 2 列:从第 1 列到第 2 列(包含)
.columnStart(1).columnEnd(2)
9.3 Grid 高度与内容不匹配
原因 :Grid 的 rowsTemplate 和容器高度共同决定行高。如果 rowsTemplate 使用了 1fr 但没有设置容器高度,Grid 可能无法正确计算行高。
解决方案 :建议为 Grid 设置明确的高度值(如 .height(140)),或者在 rowsTemplate 中使用具体值(如 '100vp')。
9.4 卡片内容被截断
原因 :GridItem 内部的卡片内容超出了 GridItem 的边界。可能是因为卡片没有设置 .width('100%').height('100%'),导致内容尺寸超出了 GridItem 的约束。
解决方案 :为卡片容器设置 width('100%') 和 height('100%'),使其填满 GridItem 的分配空间。如果内容确实需要更多空间,考虑调整 Grid 的行高或使用 rowSpan。
9.5 编译错误:FontWeight.SemiBold 不存在
原因 :在某些 API 版本中,FontWeight 枚举不包含 SemiBold 值。
解决方案 :使用 FontWeight.Medium(500 字重)或 FontWeight.Bold(700 字重)替代。
typescript
// ❌ 部分版本不支持
.fontWeight(FontWeight.SemiBold)
// ✅ 通用写法
.fontWeight(FontWeight.Medium)
十、总结与展望
本文从 Dashboard 仪表盘的实际需求出发,完整演示了鸿蒙 ArkTS 中 Grid 布局的核心用法。从最基础的等分网格,到 columnStart/columnEnd 实现的跨列布局,再到 @Builder 封装的组件化实践,以及卡片设计的最佳实践,覆盖了 Grid 布局的方方面面。
关键要点回顾
- Grid + GridItem 是实现不规则网格布局的最佳选择,尤其适合管理后台 Dashboard 场景,其跨列/跨行能力是 Flex 和线性布局无法替代的
- columnsTemplate 使用
1fr弹性单位,让布局自适应不同屏幕尺寸,无需为每种分辨率编写独立布局 - 跨列/跨行 通过
columnStart/columnEnd/rowStart/rowEnd实现,精确控制每个 GridItem 的跨度 - @Builder 将 UI 片段封装为可复用的构建方法,保持
build()代码的清晰和可维护性 - 卡片设计遵循"外疏内密"的间距体系(12vp + 16vp),配合阴影层级和色彩语言,构建有层次感的 Dashboard
- @State 驱动的响应式数据流简化了 UI 更新的复杂度,开发者只需关注数据变化
未来展望
随着 HarmonyOS NEXT 生态的逐步成熟,Grid 布局在复杂管理后台、数据看板、多媒体展示等场景中的应用会越来越广泛。掌握 Grid 不仅仅是为了实现一个页面,更是理解鸿蒙 ArkUI 布局体系的关键一步。
在更高阶的应用中,Grid 还可以与以下组件和技术组合,构建功能更加丰富的大型数据看板:
LazyForEach:实现长列表的数据懒加载Swiper:轮播 Banner 与卡片网格共存Refresh:下拉刷新仪表盘数据@Animatable:为图表卡片添加入场动画和数值滚动动画- 分布式协同:利用鸿蒙的分布式能力,实现跨设备的 Dashboard 协同展示和数据同步
写在最后
鸿蒙生态正处于快速发展的黄金时期,作为开发者,我们有幸成为这个历史进程的参与者和建设者。ArkTS 声明式 UI 框架虽然在语法上需要一段适应期,但其设计理念和开发体验是现代化的、高效的。Grid 布局只是这场技术变革中的一个缩影------当我们掌握了它,就掌握了构建复杂鸿蒙应用的重要一块拼图。
希望本文能帮助读者快速上手 ArkTS Grid 布局,并在实际项目中灵活运用。如果你有任何问题或想法,欢迎在评论区交流讨论。让我们一起在鸿蒙的世界里,构建更美好的应用体验。
参考资料
- 华为开发者联盟官网 - ArkUI 开发文档
- HarmonyOS NEXT API 24 参考手册 - Grid 组件
- 《ArkTS 声明式开发范式》- 华为开发者社区
本文示例代码基于 HarmonyOS NEXT API 24 + DevEco Studio 5.0 配套 SDK,兼容 API 24 及以上版本。