鸿蒙原生 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 设计遵循以下原则:

  1. 网格化布局:使用 Grid 容器将页面划分为规整的网格,每个网格单元放置一张卡片
  2. 内容分区:将页面分为顶部标题区、核心指标区、图表与动态区、底部功能区四个垂直区块
  3. 视觉层次:通过卡片颜色、阴影深度、字体大小建立清晰的视觉层级
  4. 交互反馈:为操作型卡片提供点击反馈(Toast 提示),增强交互感知
  5. 数据驱动:使用 @State 管理卡片数据,便于后续接入真实 API

四、Grid 布局核心概念速览

在深入代码之前,我们先系统梳理 Grid 布局的核心 API,为后续实战打好基础。

4.1 Grid 容器

Grid() {
  // GridItem 子组件
}

Grid 是一个容器组件,它的直接子组件必须是 GridItem。这一点与 Row 和 Column 有本质区别——Row 和 Column 的子组件可以是任意类型的组件(Text、Image、Button 等),但 Grid 要求显式使用 GridItem 将每个子项包裹起来。这个设计约束保证了 Grid 能够精确控制每个单元格的位置和跨度。

4.2 columnsTemplate / rowsTemplate

这两个属性是 Grid 的灵魂,它们定义了网格的列轨道行轨道,即每一列应该有多宽、每一行应该有多高:

// 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 属性:

.columnsGap(12)    // 列间距 12vp,产生垂直的视觉分隔
.rowsGap(12)       // 行间距 12vp,产生水平的视觉分隔

如果希望行列间距相同,也可以只设置一个,但 ArkTS 目前要求两个属性分别设置。12vp 是一个经过大量实践验证的"黄金间距",既能清晰区分不同的卡片,又不会浪费屏幕空间。

4.4 GridItem 与跨列/跨行

每个 GridItem 默认占据一个单元格(即网格中的一个"格子")。通过 .columnStart() / .columnEnd().rowStart() / .rowEnd(),可以让 GridItem 跨越多个网格单元,实现"大卡片"效果:

GridItem() {
  // 占据 2 列宽的卡片内容
}
.columnStart(0)    // 从第 0 列开始(列索引从 0 开始计数)
.columnEnd(1)      // 到第 1 列结束(含结束列,0~1 共 2 列)

重要的细节columnStartcolumnEnd 使用列索引(从 0 开始),且 columnEnd包含边界的。因此 columnStart(0).columnEnd(1) 跨越第 0 列和第 1 列,共计 2 列。如果需要跨越 3 列,则写为 .columnStart(0).columnEnd(2)

对于 rowStart / rowEnd,逻辑与列完全相同:

GridItem()
  .columnStart(0).columnEnd(1)   // 跨 2 列
  .rowStart(0).rowEnd(1)         // 跨 2 行

跨列和跨行可以同时使用,形成一个占据 2×2 网格的"大卡片"。

4.5 与 GridRow / GridCol 的区别

HarmonyOS 还提供了另一组网格相关组件:GridRowGridCol。它们更接近 Bootstrap 的 12 列栅格系统——GridRow 定义一行,GridCol 通过 span 属性指定该列占几份宽度:

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:

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 提供了各区块之间的垂直间距,这是一种简洁而有效的分隔方式——不需要为每个区块单独设置 marginpadding,一个属性就完成了区块间的"呼吸感"设计。

5.3 顶部标题栏(HeaderSection)

标题栏使用 Row + Column 组合实现左右布局:

@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 用法演示:

@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(即正圆形),带有阴影增加立体感
  • 右侧:三行垂直排列——标题(小字灰色)、数值+单位(大字粗体)、趋势(带颜色的百分比)
@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)

这是跨列布局的核心演示场景,也是本博客最重要的一段代码:

@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 布局组件的"图元化"能力——即使复杂的图表展示,也可以通过基础组件的组合来实现,完全不需要引入外部依赖:

// 柱状图核心代码片段
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)        // ← 关键:柱体从底部向上生长

三点实现技巧:

  1. 从底部生长:外层 Row 的 .alignItems(VerticalAlign.Bottom) 使所有柱体以底部为基准对齐,视觉上就像从地面向上生长——这比从顶部向下延伸的设计更符合柱状图的阅读习惯。

  2. 等比映射:柱体高度由 value * 2 计算得出。数据值 65~95 映射为 130~190vp 的高度,在 170vp 高的 Row 中恰好形成适中的视觉效果。如果数据范围变化,可以调整缩放系数。

  3. 顶部圆角.borderRadius({topLeft:4, topRight:4}) 只给柱体顶部两个角设置圆角,底部保留直角。这样的设计模拟了现代 UI 风格中常见的"圆顶方底"柱状图样式,比全圆角柱体更显专业。

活动列表卡片(ActivityCard) 使用 ForEach 循环渲染活动条目,每个条目包含头像占位、用户名称、操作描述和时间:

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 容纳三张功能卡片:

@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 是一个接受 iconlabel 两个参数的 Builder 方法,被调用了三次来创建三个功能按钮。这再次展示了 @Builder 的复用价值——相同的 UI 模式,只需编写一次定义、多次调用。

5.7 数据模型与 @Builder 封装的协同

整个页面使用了两个接口(StatCardDataActivityItem)来结构化数据,配合 @State 装饰器实现响应式更新。当数据变化时,UI 会自动重新渲染受影响的部分,无需手动执行业务逻辑与 UI 的同步操作。

@Builder 是 ArkTS 中封装复用 UI 片段的利器。在本例中,我们将标题栏、统计卡片、图表卡片、活动卡片、底部卡片等每个独立区域都封装为 @Builder 方法,使 build() 方法结构清晰,就像在阅读页面的目录结构一样:

build() {
  Scroll() {
    Column({ space: 16 }) {
      this.HeaderSection()       // ← 每个 @Builder 就像一段"口语化"的标签
      this.StatCardsGrid()
      this.MiddleSectionGrid()
      this.BottomCardsGrid()
    }
  }
}

与将 UI 提取为独立 @Component 相比,@Builder 的三个核心优势:

  1. 直接访问成员变量:可以读取外层 struct 的 @State 变量和普通成员,无需通过参数传递,减少了数据传递的样板代码
  2. 轻量级:不需要额外定义 struct、不需要 build() 方法、不需要 @Component 装饰器,代码量更少
  3. 内聚性强:同一文件中即可完成所有 UI 定义,对于中等复杂度的页面,避免了文件数量过多的问题

当然,如果某个卡片需要在多个页面复用,或者逻辑复杂到需要独立的管理状态,就应该将其抽取为独立的 @Component


六、Grid 布局的进阶技巧与扩展

在掌握了基础用法之后,让我们探索一些更高级的 Grid 实践技巧。

6.1 动态列数与响应式适配

如果需要根据屏幕宽度动态调整列数,可以结合 MediaQuery 或通过 @StorageProp 获取窗口宽度:

@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 单位:

.columnsTemplate('1fr '.repeat(this.columns).trim())
// 当 columns = 4 时,生成 '1fr 1fr 1fr 1fr'

这种方式可以灵活适应不同的屏幕尺寸,是实现响应式 Dashboard 的关键技术。

6.2 使用 rowSpan 实现跨行

除了跨列,GridItem 也可以跨行。例如让一张大图卡片占据 2 行 2 列,形成一个"特色展位":

GridItem()
  .columnStart(0).columnEnd(1)   // 跨 2 列(第 0~1 列)
  .rowStart(0).rowEnd(1)         // 跨 2 行(第 0~1 行)

这在图片墙、产品展示、运营推广位等场景中非常实用。跨列和跨行的组合使用可以创造出丰富的"不规则但有序"的视觉布局。

6.3 Grid 嵌套

Grid 可以嵌套使用——在一个 GridItem 内部再放一个 Grid 来实现子网格布局。例如,一个"团队业绩"卡片内部可能包含一个 2×2 的子网格来展示四个成员的指标数据:

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 中,与卡片网格共存:

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,后者只渲染当前可见区域的项,大幅减少组件树的节点数量:

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 的 columnsGaprowsGap 设置为 12vp 是一个经过大量鸿蒙应用验证的"黄金间距"。配合卡片内部的 16vp padding,形成了外部 12vp + 内部 16vp 的双层呼吸感结构。这种"外疏内密"的设计可以让用户的目光自然地聚焦在卡片内部的内容上,同时卡片之间有清晰的分隔。

为什么不是 8vp 或 20vp?8vp 的间距在卡片内容较多时会显得拥挤,缺乏区分度;而 20vp 在手机等小屏设备上会浪费宝贵的屏幕空间。12vp 恰好处于"刚刚好"的平衡点。

7.4 阴影层级与交互反馈

本文示例使用了统一的浅阴影:

.shadow({
  radius: 6,
  color: 'rgba(0, 0, 0, 0.06)',
  offsetX: 0,
  offsetY: 2
})

在实际项目中,可以根据卡片的交互状态设计不同的阴影层级:

  1. 默认态:浅阴影(radius: 6, color: rgba(0,0,0,0.06))
  2. 悬浮态(鼠标悬停或手指触摸):加深阴影(radius: 12, color: rgba(0,0,0,0.12))
  3. 点击态:内凹阴影或缩小效果,模拟按下

这种阴影层级的差异化设计可以传递卡片的"可交互性"暗示,提升用户的操作感知。


八、从 ArkTS 角度看声明式 UI 的优势

通过这个 Dashboard 项目,我们可以清晰地感受到 ArkTS 声明式 UI 与传统命令式 UI 开发之间的几个根本性差异。

8.1 UI = f(state):数据驱动 UI

@State 装饰的变量是 UI 的"单一数据源"。当数据变化时,框架自动重新渲染受影响的组件,开发者不再需要手动调用 setText()setBackgroundColor()notifyDataSetChanged() 等方法。这种"UI 是状态的函数"的范式,将开发者从繁琐的 UI 同步逻辑中解放出来,让他们能够专注于业务数据的处理。

8.2 链式 API 的可读性与可维护性

Text('Hello')
  .fontSize(16)
  .fontColor('#1A1A2E')
  .fontWeight(FontWeight.Bold)
  .margin({ top: 8 })

这种链式调用有着以下显著优势:

  • 上下文集中:一个组件的所有属性设置集中在一处,无需在不同代码块之间跳转
  • 天然的分组:每个 .方法() 独立一行,通过缩进可以清晰看到组件有哪些属性被设置
  • 顺序无关:属性设置的顺序不影响最终结果,减少了排序相关的认知负担
  • 编译期检查:属性名称和方法参数在编译时就会进行类型校验

8.3 类型安全:编译期发现错误

ArkTS 是 TypeScript 的超集,所有组件属性和事件都在编译期进行类型检查。例如,FontWeight 只接受预定义的枚举值(BoldMediumRegular 等),如果传入了错误的字符串,编译会直接报错,而不是等到运行时报错或默默忽略。这种编译期的安全保障在大型团队协作项目中极具价值。

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 内部。

// ❌ 错误
Grid() {
  Text('Hello')  // 不会显示!
}

// ✅ 正确
Grid() {
  GridItem() {
    Text('Hello')  // 正常显示
  }
}

9.2 跨列/跨行设置不生效

原因columnEnd 的值设置不正确。常见错误是忘记 columnEnd包含结束索引的。

解决方案:牢记公式——跨 N 列时,columnEnd = columnStart + N - 1

// 跨 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 字重)替代。

// ❌ 部分版本不支持
.fontWeight(FontWeight.SemiBold)

// ✅ 通用写法
.fontWeight(FontWeight.Medium)

十、总结与展望

本文从 Dashboard 仪表盘的实际需求出发,完整演示了鸿蒙 ArkTS 中 Grid 布局的核心用法。从最基础的等分网格,到 columnStart/columnEnd 实现的跨列布局,再到 @Builder 封装的组件化实践,以及卡片设计的最佳实践,覆盖了 Grid 布局的方方面面。

关键要点回顾

  1. Grid + GridItem 是实现不规则网格布局的最佳选择,尤其适合管理后台 Dashboard 场景,其跨列/跨行能力是 Flex 和线性布局无法替代的
  2. columnsTemplate 使用 1fr 弹性单位,让布局自适应不同屏幕尺寸,无需为每种分辨率编写独立布局
  3. 跨列/跨行通过 columnStart/columnEnd/rowStart/rowEnd 实现,精确控制每个 GridItem 的跨度
  4. @Builder 将 UI 片段封装为可复用的构建方法,保持 build() 代码的清晰和可维护性
  5. 卡片设计遵循"外疏内密"的间距体系(12vp + 16vp),配合阴影层级和色彩语言,构建有层次感的 Dashboard
  6. @State 驱动的响应式数据流简化了 UI 更新的复杂度,开发者只需关注数据变化

未来展望

随着 HarmonyOS NEXT 生态的逐步成熟,Grid 布局在复杂管理后台、数据看板、多媒体展示等场景中的应用会越来越广泛。掌握 Grid 不仅仅是为了实现一个页面,更是理解鸿蒙 ArkUI 布局体系的关键一步。

在更高阶的应用中,Grid 还可以与以下组件和技术组合,构建功能更加丰富的大型数据看板:

  • LazyForEach:实现长列表的数据懒加载
  • Swiper:轮播 Banner 与卡片网格共存
  • Refresh:下拉刷新仪表盘数据
  • @Animatable:为图表卡片添加入场动画和数值滚动动画
  • 分布式协同:利用鸿蒙的分布式能力,实现跨设备的 Dashboard 协同展示和数据同步

写在最后

鸿蒙生态正处于快速发展的黄金时期,作为开发者,我们有幸成为这个历史进程的参与者和建设者。ArkTS 声明式 UI 框架虽然在语法上需要一段适应期,但其设计理念和开发体验是现代化的、高效的。Grid 布局只是这场技术变革中的一个缩影——当我们掌握了它,就掌握了构建复杂鸿蒙应用的重要一块拼图。

希望本文能帮助读者快速上手 ArkTS Grid 布局,并在实际项目中灵活运用。如果你有任何问题或想法,欢迎在评论区交流讨论。让我们一起在鸿蒙的世界里,构建更美好的应用体验。


参考资料

  1. 华为开发者联盟官网 - ArkUI 开发文档
  2. HarmonyOS NEXT API 24 参考手册 - Grid 组件
  3. 《ArkTS 声明式开发范式》- 华为开发者社区

本文示例代码基于 HarmonyOS NEXT API 24 + DevEco Studio 5.0 配套 SDK,兼容 API 24 及以上版本。

Logo

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

更多推荐