鸿蒙原生 ArkTS 布局之道:Grid 网格布局入门 — 行列式排列的核心概念


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

一、引言:为什么我们需要网格布局

在鸿蒙原生应用开发中,布局是构建用户界面的基石。HarmonyOS NEXT(API 24)的 ArkUI 框架提供了多种布局容器,每种布局都有其独特的设计哲学和适用场景。而 Grid(网格布局) 是其中最为强大、灵活且高效的布局方案之一,它完美地解决了二维空间中的组件排列问题。

1.1 从传统布局的痛点说起

在我们深入 Grid 布局之前,不妨先回顾一下:如果没有 Grid,我们要实现一个规则的多行多列布局,会遇到哪些困难?

第一种方式:Row + Column 多层嵌套

Column({ space: 8 }) {
  Row({ space: 8 }) {
    Text('1').width(80).height(80)
    Text('2').width(80).height(80)
    Text('3').width(80).height(80)
  }
  Row({ space: 8 }) {
    Text('4').width(80).height(80)
    Text('5').width(80).height(80)
    Text('6').width(80).height(80)
  }
  Row({ space: 8 }) {
    Text('7').width(80).height(80)
    Text('8').width(80).height(80)
    Text('9').width(80).height(80)
  }
}

这段代码的问题显而易见:代码极度冗余,每个 Row 都要重复写相同的结构;如果要修改列数,必须手动调整每一行;如果要实现跨列(比如第 1 个格子占 2 列),需要额外的嵌套和样式覆盖,实现方式非常笨拙。

第二种方式:Flex 弹性布局 + flexWrap

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
  // 假设有 9 个子项
}

Flex 确实可以自动换行,但它无法精确控制每行的高度对齐:当某个子项高度不一致时,行内对齐会变得混乱。更重要的是,Flex 没有"行"和"列"的概念,你无法让某个子项跨越两列或两行,也无法在不同行之间保持严格的列对齐。

第三种方式:绝对定位 + 手动计算

这是最不推荐的方式——每个子项的坐标都需要通过公式计算,一旦容器尺寸变化,所有坐标都要重新计算。这种方式完全违背了声明式 UI 的"描述你想看到的界面,而非如何达到它"的设计理念。

1.2 Grid 布局的提出

Grid 布局的理念源自 CSS Grid Layout,它引入了一个革命性的想法:将容器划分为由行列组成的网格系统,子组件只需要声明自己占据哪个区域,剩下的填充、对齐、间距全部由框架自动完成。

开发者只需要告诉 Grid 容器:

  • “我要 3 列,宽度均分”
  • “我要 2 行,高度均分”
  • “子项之间间距 8vp”
  • “第 1 个子项横跨整行”

剩下的,Grid 全部自动处理。这不仅大幅减少了代码量,还让布局意图变得更加清晰和可维护。

1.3 本文的目标读者和前置知识

本文适合以下读者:

  • 刚刚接触 HarmonyOS NEXT 开发,想要系统学习 ArkUI 布局机制的新手
  • 有过 Web 前端(CSS Grid / Flexbox)经验,正在迁移到鸿蒙开发的同学
  • 已经使用过 Column / Row / Flex,但尚未深入使用 Grid 的开发者

前置知识要求:了解 TypeScript / ArkTS 的基本语法,知道 @Component@Entry 装饰器的基本用法。不需要 Grid 相关经验,本文将从零开始讲解。


二、Grid 布局的核心架构

2.1 行、列、单元格

理解 Grid 布局的关键,在于理解三个层次的概念:

网格容器(Grid)
最外层的容器组件,负责定义网格的行列结构。它就像一张空白表格的框架——你需要先告诉它有多少行列,然后才能往里填充数据。

网格线
构成网格的虚拟分隔线。对于一个 N 列的网格,有 N+1 条垂直网格线(从 0 开始编号)。行同理。网格线是索引的基础,后续要使用的 columnStartcolumnEndrowStartrowEnd 都是基于网格线索引来定位的。

网格单元(Cell)
行列交叉形成的最小单位。一个 N 列 × M 行的网格,共有 N×M 个网格单元。每个 GridItem 占据一个或多个网格单元。

网格项(GridItem)
Grid 容器的直接子组件,是真正承载内容的实体。一个 GridItem 可以占据一个单元格(默认行为),也可以通过跨行跨列属性占据多个单元格。

理解这三层概念之后,你会发现 Grid 布局本质上就是一种"声明式的表格填充":你定义表格结构,然后把内容放入指定的单元格区域。

2.2 Grid 与 GridItem 的父子关系

Grid 和 GridItem 构成严格的父子关系。这意味着:

  1. GridItem 只能作为 Grid 的直接子组件出现。如果你把 GridItem 放在 Column 或 Row 里面,它将无法正常工作。

  2. 在 GridItem 内部,你可以放置任意数量的子组件,包括 Column、Row、Text、Image、Button,甚至另一个嵌套的 Grid。

  3. GridItem 之间不能存在非 GridItem 的兄弟节点。简单来说,Grid 容器中所有直接子节点都必须是 GridItem

// ✅ 正确写法
Grid() {
  GridItem() { Text('1') }
  GridItem() { Text('2') }
  GridItem() { Text('3') }
}

// ❌ 错误写法:Grid 中混入了非 GridItem 的直接子节点
Grid() {
  GridItem() { Text('1') }
  Text('错误!')    // 编译不通过或行为异常
}

2.3 网格的填充顺序

当一个 Grid 中包含了 M 个 GridItem,而网格有 N 列时,默认的填充顺序是先行后列(row-major order)

  • 第 1 个 GridItem 放入第 0 行第 0 列
  • 第 2 个 GridItem 放入第 0 行第 1 列
  • ……
  • 第 N 个 GridItem 放入第 0 行第 N-1 列
  • 第 N+1 个 GridItem 放入第 1 行第 0 列
  • 以此类推

这种填充顺序与英文的阅读顺序(从左到右、从上到下)一致,符合最自然的直觉。当 GridItem 跨列时,下一个 GridItem 会自动跳过被占用的单元格,继续在下一个空白单元放置。


三、深入理解 columnsTemplate 与 rowsTemplate

columnsTemplaterowsTemplate 是 Grid 布局中最重要的两个属性。理解了它们,就理解了 Grid 布局的 80%。

3.1 模板字符串的语法规则

这两个属性的值都是一个字符串,其中包含若干个用空格分隔的长度值。每个长度值定义一个列(或行)的尺寸。

.columnsTemplate('1fr 1fr 1fr')         // 3 列,每列宽度相等
.columnsTemplate('80px 1fr 2fr')         // 3 列,第 1 列固定,后两列弹性
.columnsTemplate('repeat(4, 1fr)')       // 4 列等宽(repeat 简写)

语法规则总结如下:

  1. 模板字符串中的每个值对应一列(或一行)。
  2. 值的数量就是列(或行)的数量。
  3. 值之间用空格分隔,不能使用逗号或其他分隔符。
  4. 值的单位可以是 frpxvp%auto 之一。

3.2 fr 单位的详细计算机制

fr 是 Grid 布局中最独特也最重要的单位。它的名称源自 “fraction”(分数),代表剩余空间的份额。

计算规则:

每列的 fr 宽度 = (容器总宽度 − 所有固定宽度之和 − 所有列间距之和) × (该列的 fr 值 ÷ 所有 fr 值之和)

让我们通过一个具体示例来理解。假设容器宽度为 360vp,columnsTemplate 设为 '80px 1fr 2fr',列间距 columnsGap 为 8vp:

容器总宽度         = 360vp
减去固定列         = 360 − 80 = 280vp
减去列间距 (2个)   = 280 − 16 = 264vp
fr 总份数          = 1 + 2 = 3 份
每份宽度           = 264 ÷ 3 = 88vp
第 2 列 (1fr)     = 88vp
第 3 列 (2fr)     = 176vp

关键理解: fr 是在所有固定宽度和间距都计算完毕之后,才分配剩余空间的。因此,fr 的值只有在容器宽度变化时才会弹性变化,固定宽度的列不受影响。

3.3 px / vp 固定单位的区别

pxvp 是 ArkUI 中最常用的固定单位,它们的区别在于:

  • px(物理像素):直接对应屏幕上的物理像素点。在不同分辨率的屏幕上,相同 px 值对应的物理尺寸相同,但在逻辑上的"视觉大小"可能不同。
  • vp(虚拟像素):ArkUI 推荐的尺寸单位,会根据屏幕密度自动缩放。在标准密度屏幕上,1vp = 1px;在高密度屏幕上,1vp > 1px。

最佳实践:columnsTemplate 中推荐使用 vp 或直接写数字(如 80),系统默认以 vp 为单位。如果写 '80px',请确保你知道自己在做什么——在跨设备适配时,px 可能会导致在不同屏幕上的实际显示尺寸不一致。

3.4 auto 单位的自适应行为

auto 单位让列宽由该列中最宽的 GridItem 的内容决定。来看一个具体场景:

Grid() {
  GridItem() { Text('短文本') }
  GridItem() { Text('这是一段较长文本内容,它的宽度会撑开所在列') }
  GridItem() { Text('中等长度') }
}
.columnsTemplate('auto 1fr 1fr')

在这个示例中,第 1 列的宽度会由最宽的内容(“这是一段较长文本内容,它的宽度会撑开所在列”)决定,而第 2、3 列则按 1:1 的比例分配剩余空间。

auto vs 1fr 的核心区别:

特性 auto 1fr
宽度由谁决定 内容最宽的 GridItem 剩余空间按比例分配
容器变宽时 不变(除非内容变多) 按比例变宽
容器变窄时 可能换行或截断 按比例变窄
适用场景 标签列、固定操作列 内容列、自适应区域

3.5 % 百分比单位的使用

百分比单位使列宽占容器总宽度的百分比。需要注意:百分比的计算基准是 Grid 容器的总宽度,不包括间距

.columnsTemplate('30% 70%')

如果容器宽度是 360vp,第 1 列 = 360 × 30% = 108vp,第 2 列 = 360 × 70% = 252vp。

百分比和 fr 可以混合使用,但要注意它们各自的计算基准不同,混用时可能需要仔细计算实际比例。

3.6 repeat 简写的详细展开

// 基本用法:repeat(次数, 模式)
'repeat(4, 1fr)''1fr 1fr 1fr 1fr'
'repeat(3, auto)''auto auto auto'
'repeat(2, 100px 1fr)''100px 1fr 100px 1fr'
'repeat(2, 1fr 2fr)''1fr 2fr 1fr 2fr'

repeat 的第二个参数可以包含多个值,它们会作为一个整体重复。repeat(2, 100px 1fr) 等价于先将 100px 1fr 展开,再重复 2 次。

需要注意的是:repeat 暂不支持嵌套。repeat(2, repeat(2, 1fr)) 是无效语法。


四、间距控制:columnsGap 与 rowsGap

4.1 间距的定位

columnsGap 设置列与列之间的间距,rowsGap 设置行与行之间的间距。间距只作用于网格线上,不会在容器边缘增加空白。

可以这样理解:如果用 M 列、N 行的网格,那么:

  • 垂直方向有 M-1 条列间距线
  • 水平方向有 N-1 条行间距线

间距不会增加网格的总宽度以外部分,而是在内部"消耗"掉一部分空间。

4.2 间距对 fr 计算的影响

由于 fr 是在扣除所有固定宽度和间距之后才进行分配的,因此间距的大小会直接影响 fr 列的实际宽度:

容器宽度          = 360vp
列模板            = '1fr 1fr 1fr'
columnsGap = 0   → 每列宽度 = 360 ÷ 3 = 120vp
columnsGap = 8   → 每列宽度 = (360 - 2×8) ÷ 3 = 114.67vp
columnsGap = 16  → 每列宽度 = (360 - 2×16) ÷ 3 = 109.33vp

可以看到,间距越大,弹性列的可用空间就越小。

4.3 只设置一个方向间距

在某些场景下,你可能只需要纵向间距而不需要横向间距(或者反过来)。这完全没问题:

Grid() { /* ... */ }
  .columnsTemplate('1fr 1fr')
  .rowsTemplate('1fr 1fr')
  .columnsGap(0)     // 列之间无间距
  .rowsGap(12)       // 行之间有 12vp 间距

这种设置在"堆叠式卡片"布局中很常见——卡片之间只需要上下间距,左右紧贴容器边界。


五、跨行跨列实战:从对称到非对称

前面几个示例展示了等规格的网格排列,这些布局通过嵌套 Row/Column 也能勉强实现。但 Grid 的真正威力——跨行跨列能力——正是它区别于传统嵌套布局的根本所在。

5.1 理解网格线索引

要掌握跨行跨列,首先需要理解网格线的索引系统。对于一个 3 列 × 2 行的网格:

列索引:   0     1     2
行索引: ┌─────┬─────┬─────┐
   0    │     │     │     │
        ├─────┼─────┼─────┤
   1    │     │     │     │
        └─────┴─────┴─────┘

图中数字表示网格线的编号。对于一个 N 列的网格,网格线从 0 编号到 N。

四个定位属性的含义如下:

  • columnStart:GridItem 的起始列线索引
  • columnEnd:GridItem 的结束列线索引(包含该线对应的列)
  • rowStart:GridItem 的行起始线索引
  • rowEnd:GridItem 的行结束线索引(包含该线对应的行)

重要:索引从 0 开始,且 End 是包含的。

5.2 跨列的场景分析

以我们的 Demo 中的跨列示例为例:

// 跨3列:从列线0到列线2 → 覆盖第0、1、2列
.columnStart(0).columnEnd(2)

// 跨1列:从列线0到列线0 → 只覆盖第0列
.columnStart(0).columnEnd(0)

// 跨2列:从列线1到列线2 → 覆盖第1、2列
.columnStart(1).columnEnd(2)

最常见的跨列场景包括:

场景一:顶部 Banner(通栏标题)
新闻列表的顶部通常有一个横跨全宽的头条图。用 Grid 实现就是第 0 行第 0 个 GridItem 跨所有列。

场景二:不对称的商品展示
电商首页中,"新品推荐"区域常采用"左边一个大图 + 右边两个小图"的非对称布局。这就是跨列的典型应用——左边 GridItem 跨 2 行,右边两个各占 1 行。

场景三:仪表盘 / Dashboard
监控仪表盘中,核心指标(CPU 使用率、内存占用)展示为小卡片,而趋势图占据更大的跨列区域。不同指标根据重要性配置不同的跨幅。

5.3 跨行的场景分析

跨行的语法与跨列完全对称,只是方向不同。

// 跨2行:从行线0到行线1 → 覆盖第0、1行
.rowStart(0).rowEnd(1)

跨行在以下场景中特别有用:

场景:侧边导航 + 内容区
左侧导航栏跨满整列的所有行,右侧内容区再细分为上下页眉和内容区域。

5.4 同时跨行跨列

最强大的情况是同时设置 columnStart/EndrowStart/End,让一个 GridItem 占据一个大的矩形区域。

GridItem() {
  // 占据左上角的 2列 × 2行 区域
}
.columnStart(0).columnEnd(1)
.rowStart(0).rowEnd(1)

这在"画报式"布局(如杂志封面、作品集展示)中非常常见——不同的区域有不同的大小,形成错落有致的视觉效果。


六、实战 Demo 逐行解析

现在,让我们逐行分析完整的 Demo 代码。这不是简单的展示,而是深入理解每处设计背后的思考。

6.1 颜色色板的设计

/**
 * 色板:给网格项上色,方便视觉区分每个格子
 * 使用十六进制字符串格式,兼容所有 HarmonyOS NEXT 版本
 * #AARRGGBB:AA=透明度(FF 不透明),RR/GGBB=红绿蓝分量
 */
const COLORS: string[] = [
  '#FF3F51B5',   // 靛蓝
  '#FFE91E63',   // 玫红
  '#FF009688',   // 青绿
  '#FFFF9800',   // 琥珀
  '#FF9C27B0',   // 紫色
  '#FF2196F3',   // 亮蓝
  '#FF4CAF50',   // 草绿
  '#FFF44336',   // 红色
  '#FF607D8B',   // 蓝灰
];

设计考量:

为什么选择这 9 种颜色?

首先,它们来自 Material Design 的 500 色板,每种颜色在色相上相差约 40°,确保相邻的网格单元具有强烈视觉对比,让网格边界清晰可辨。这对教学演示至关重要——如果使用相近的颜色,读者很难一眼看出网格的分隔。

其次,颜色的顺序按照"冷色 → 暖色 → 冷色"交替排列,避免连续出现相似色相。第 1 个格子是靛蓝(冷),第 2 个玫红(暖),第 3 个青绿(冷),第 4 个琥珀(暖)……这种交替排列使得即使不看数字编号,也能轻松区分相邻格子。

为什么使用 string 类型而不是 Color 枚举?在早期的 HarmonyOS API 版本中,Color.fromArgb() 的方法签名存在差异(有的版本是 fromArgb(alpha, red, green, blue),有的是 fromArgb(red, green, blue, alpha))。使用十六进制字符串 '#FF3F51B5' 作为 ResourceColor 类型,在所有 API 版本中行为一致,是跨版本兼容的最佳实践。

6.2 GridCell 组件详解

@Component
struct GridCell {
  /**
   * 格子编号(显示在文字中央)。
   * 注意:不能加 private 修饰符,否则无法通过构造函数传值初始化。
   */
  index: number = 0;

  /**
   * 背景色(从色板循环取色,使用十六进制字符串)。
   * 类型标注为 ResourceColor 可以同时接受 string、Color、number 和 Resource。
   */
  bgColor: ResourceColor = Color.Gray;

  build() {
    GridItem() {
      Text(`${this.index}`)       // 将数字转为字符串显示
        .fontSize(20)              // 字号 20vp
        .fontColor(Color.White)   // 白色字体,在深色背景上更清晰
        .fontWeight(FontWeight.Bold)  // 粗体增强可读性
        .textAlign(TextAlign.Center)  // 文字居中
    }
    .border({ width: 2, color: Color.White }) // 白色边框增强网格边界
    .borderRadius(8)                // 圆角 8vp,更美观
    .backgroundColor(this.bgColor) // 应用传入的背景色
    .width('100%')                  // 填满所在网格单元宽度
    .height('100%')                 // 填满所在网格单元高度
  }
}

关于 private 的关键知识点:

在 ArkTS 中,自定义组件的属性如果加了 private 修饰符,意味着该属性只能在组件内部访问,不能在父组件通过构造函数传值初始化。因此,如果写成 private index: number = 0,在父组件中写 GridCell({ index: 1 }) 就会触发编译警告。

正确的做法是:所有需要通过构造函数传入的属性,都不要加 private 修饰符。 只有那些组件内部自我管理、不需要外部传入的属性,才可以标记为 private

ResourceColor 类型:

ResourceColor 是一个联合类型,定义如下:

type ResourceColor = Color | string | number | Resource;

这意味着它可以接受四种不同类型的颜色值:

  • Color.Red — Color 枚举
  • '#FF3F51B5' — 十六进制字符串
  • 0xFF3F51B5 — 十六进制数字
  • $r('app.color.primary') — 资源引用

在 Demo 中,我们使用字符串,父组件传值 -> 子组件接收 -> 传递给 .backgroundColor(),类型完全兼容。

6.3 SectionTitle 和 CodeHint 辅助组件

@Component
struct SectionTitle {
  /** 演示区小标题,不加 private 以便通过构造函数传值 */
  title: string = '';

  build() {
    Text(this.title)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.Black)
      .margin({ top: 20, bottom: 8 })
      .textAlign(TextAlign.Start)
      .width('100%')
  }
}

@Component
struct CodeHint {
  /** 每节底部的代码注释说明框,不加 private 以便通过构造函数传值 */
  hint: string = '';

  build() {
    Text(this.hint)
      .fontSize(12)
      .fontColor(Color.Gray)
      .backgroundColor('#1E000000')   // 约 12% 透明度的黑色背景
      .borderRadius(6)
      .padding(8)
      .margin({ top: 4, bottom: 4 })
      .width('100%')
  }
}

这两个辅助组件的设计理念是职责分离——把"展示小标题"和"展示代码提示"这两个重复出现的 UI 模式抽象为独立组件,大幅减少了主页面中的重复代码。

如果不使用这两个组件,主页面中每个示例都需要重复写:

Text('① 三列均分')
  .fontSize(16)
  .fontWeight(FontWeight.Bold)
  .fontColor(Color.Black)
  .margin({ top: 20, bottom: 8 })

有了 SectionTitle,就简化为一行:

SectionTitle({ title: '① 三列均分' })

这就是组件化开发的核心思想——通过抽象和复用,提升代码的可读性和可维护性。

6.4 主页面 GridDemo 的结构设计

@Entry
@Component
struct GridDemo {
  build() {
    Scroll() {                    // 外层滚动容器,方便查看所有示例
      Column({ space: 8 }) {      // 垂直排列所有示例
        // 页面标题
        // ...
        // 示例一 ~ 示例四
        // ...
        // 核心概念总结
        // ...
      }
      .padding({ left: 16, right: 16 })
      .width('100%')
    }
    .backgroundColor(Color.White)
    .height('100%')
    .width('100%')
  }
}

设计决策:为什么最外层用 Scroll 而不是直接使用 Column

因为在大多数手机屏幕上,4 个 Grid 示例 + 标题 + 总结的文字内容合计高度超过屏幕可见区域。如果不使用 Scroll,后面的内容会被屏幕底部截断,用户无法看到完整的示例四和总结。

Scroll 提供了纵向滚动能力,让用户可以通过滑动查看所有内容。这是"演示型页面"的标准做法——优先展示全部内容,让用户可以自由浏览。

为什么 Scroll 内部用 Column 而不是 List?

Column 更适合"已知数量、固定结构"的垂直排列,而 List 更适合"大量数据、动态增删"的长列表。我们只有 4 个固定的 Grid 示例,用 Column 足够了。List 的开销比 Column 大(因为 List 需要管理子项的回收和复用),对于固定数量的子项,Column 更轻量、更简单。

6.5 示例一的逐行解析

// ───────────────────────────────────────────────
// 示例一:固定列数 —— 3 列均分("1fr 1fr 1fr")
//   fr = fraction = 分数单位,每个格子占 1 份
//   等价于将容器宽水平均分成 3 份
// ───────────────────────────────────────────────
SectionTitle({ title: '① 三列均分 —— columnsTemplate: "1fr 1fr 1fr"' })

CodeHint({
  hint: '⭐ "1fr 1fr 1fr" = 把容器宽度分成 3 等份,每列占 1 份'
})

Grid() {
  // 使用 ForEach 循环生成 9 个格子的数据驱动写法
  ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (i: number) => {
    GridCell({
      index: i,
      bgColor: COLORS[(i - 1) % COLORS.length]  // 循环取色
    })
  })
}
.columnsTemplate('1fr 1fr 1fr')   // ← 关键属性:3 列,每列宽度比例 1:1:1
.rowsTemplate('1fr 1fr 1fr')      // ← 关键属性:3 行,每行高度比例 1:1:1
.columnsGap(8)                     // 列间距 8vp
.rowsGap(8)                        // 行间距 8vp
.height(240)
.width('100%')
.padding(8)
.backgroundColor('#14000000')      // 约 8% 透明度的黑色,突出网格区域
.borderRadius(12)

逐行分析:

  1. SectionTitleCodeHint:作为每个示例的"前导说明",告诉读者即将看到什么、核心语法是什么。

  2. ForEach([1, 2, ..., 9], ...):这是 ArkTS 中循环渲染的写法。第一个参数是数据源数组,第二个参数是一个闭包,对数组中的每个元素生成一个子组件。这里数字 1-9 表示 9 个网格项。

  3. (i - 1) % COLORS.length:因为 i 从 1 开始,而数组索引从 0 开始,所以用 i - 1 做映射。% COLORS.length 确保当 i 超过颜色数量时,循环回到数组开头。COLORS 有 9 种颜色,刚好对应 9 个格子,每个格子颜色不同。

  4. .columnsTemplate('1fr 1fr 1fr'):如前所述,3 个 1fr 意味着 3 列等宽。这是 Grid 布局最经典的用法。

  5. .rowsTemplate('1fr 1fr 1fr'):3 个 1fr 意味着 3 行等高。行列都是 1fr 时,每个网格单元都是正方形(假设容器宽高比合适)。

  6. .columnsGap(8).rowsGap(8):在格子之间加入 8vp 的间距,避免格子紧贴在一起,使网格结构更加清晰。

  7. .height(240):固定 Grid 容器的高度。注意这里必须显式设置高度,因为 Grid 默认高度为 0(由内容撑开)。如果不设置高度,rowsTemplate 将无法生效。

  8. .backgroundColor('#14000000'):半透明的黑色背景,大致相当于 CSS 中的 rgba(0, 0, 0, 0.08),让网格区域与白色页面背景区分开。

6.6 示例二的逐行解析

// ───────────────────────────────────────────────
// 示例二:混合列宽 —— "80px 1fr 2fr"
//   第 1 列固定 80vp     → 不随容器宽度缩放
//   第 2 列占 1 份剩余   → 弹性
//   第 3 列占 2 份剩余   → 第 3 列宽度是第 2 列的 2 倍
// ───────────────────────────────────────────────
SectionTitle({ title: '② 混合列宽 —— columnsTemplate: "80px 1fr 2fr"' })

CodeHint({
  hint: '⭐ "80px 1fr 2fr" = 第1列固定80vp,剩余宽度按 1:2 分给后两列'
})

Grid() {
  ForEach([1, 2, 3, 4, 5, 6], (i: number) => {
    GridCell({ index: i, bgColor: COLORS[(i + 3) % COLORS.length] })
  })
}
.columnsTemplate('80px 1fr 2fr')   // ← 关键:混合单位!px 固定,fr 弹性
.rowsTemplate('1fr 1fr')           // 2 行均分高度
.columnsGap(8)
.rowsGap(8)
.height(160)
.width('100%')
.padding(8)
.backgroundColor('#14000000')
.borderRadius(12)

关键知识点:

'80px 1fr 2fr' 展示了 Grid 布局中最实用的"固定 + 弹性"混合模式。这种模式在实际项目中的应用非常广泛:

  • 用户列表:头像(固定 48vp)+ 用户名(1fr)+ 操作按钮(auto)
  • 表单布局:标签(固定 80vp)+ 输入框(1fr)+ 校验状态(固定 24vp)
  • 商品卡片:缩略图(固定 72vp)+ 标题(2fr)+ 价格(1fr)

这里的 COLORS[(i + 3) % COLORS.length] 使用了不同的偏移量(i + 3),使得示例二的起始颜色与示例一不同,视觉上更容易区分两个独立的 demo。

为什么这里只用 6 个 GridItem?因为 '80px 1fr 2fr' 是 3 列,'1fr 1fr' 是 2 行,总计 3×2 = 6 个单元格。刚好 6 个 GridItem 填满网格,不多不少。

6.7 示例三的逐行解析

// ───────────────────────────────────────────────
// 示例三:使用 repeat 语法 —— "repeat(4, 1fr)"
//   等价于 "1fr 1fr 1fr 1fr",更简洁
//   当列数很多时尤其方便
// ───────────────────────────────────────────────
SectionTitle({ title: '③ repeat 语法 —— columnsTemplate: "repeat(4, 1fr)"' })

CodeHint({
  hint: '⭐ "repeat(4, 1fr)" = 快速创建 4 列等宽布局,和 "1fr 1fr 1fr 1fr" 等价'
})

Grid() {
  ForEach([1, 2, 3, 4, 5, 6, 7, 8], (i: number) => {
    GridCell({ index: i, bgColor: COLORS[(i + 6) % COLORS.length] })
  })
}
.columnsTemplate('repeat(4, 1fr)')  // ← 关键:repeat 语法,4 列各 1fr
.rowsTemplate('1fr 1fr')            // 2 行
.columnsGap(8)
.rowsGap(8)
.height(160)
.width('100%')
.padding(8)
.backgroundColor('#14000000')
.borderRadius(12)

repeat 的高级用法:

这里的 'repeat(4, 1fr)' 等价于 '1fr 1fr 1fr 1fr'。当列数从 4 增加到 8 或 12 时,repeat 的简洁性优势会越来越明显。

想象一下,如果你需要一个 12 列的网格:

// 不用 repeat:
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')

// 用 repeat:
.columnsTemplate('repeat(12, 1fr)')

repeat 还支持更复杂的模式重复:

// 模式重复:等价于 '80px 1fr 80px 1fr 80px 1fr'
.columnsTemplate('repeat(3, 80px 1fr)')

这在实现"等间隔的固定 - 弹性交替排列"时非常有用。

为什么这个示例使用 8 个 GridItem 而不是 8 个单元格(4 列 × 2 行 = 8 个单元格)?目的就是让读者看到:GridItem 数量刚好等于网格单元总数时,网格被完全填满,没有空隙。

6.8 示例四的逐行解析

// ───────────────────────────────────────────────
// 示例四:行列间距与跨行列(GridItem 跨列)
//   columnsGap / rowsGap 控制网格间距
//   通过 GridItem.columnStart / .columnEnd 实现跨列
// ───────────────────────────────────────────────
SectionTitle({ title: '④ 跨列布局 —— GridItem 跨多列' })

CodeHint({
  hint: '⭐ 使用 columnStart/columnEnd 让某个 GridItem 跨越多个列'
})

Grid() {
  // GridItem 1:第 0 行,跨 3 列(从第 0 列线到第 2 列线)
  GridItem() {
    Text('跨3列')
      .fontSize(18)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
      .textAlign(TextAlign.Center)
  }
  .columnStart(0)     // 起始列线索引(从 0 开始)
  .columnEnd(2)       // 结束列线索引(包含第 2 列线)
  .rowStart(0)        // 起始行线
  .rowEnd(0)          // 结束行线
  .backgroundColor('#FF3F51B5')  // 靛蓝
  .borderRadius(8)
  .border({ width: 2, color: Color.White })
  .width('100%')
  .height('100%')

  // GridItem 2:第 1 行第 0 列(普通单格)
  GridItem() {
    Text('A')
      .fontSize(18)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
      .textAlign(TextAlign.Center)
  }
  .columnStart(0).columnEnd(0)    // 只占第 0 列
  .rowStart(1).rowEnd(1)          // 只占第 1 行
  .backgroundColor('#FFE91E63')   // 玫红
  .borderRadius(8)
  .border({ width: 2, color: Color.White })
  .width('100%')
  .height('100%')

  // GridItem 3:第 1 行第 1~2 列(跨 2 列)
  GridItem() {
    Text('跨2列')
      .fontSize(18)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
      .textAlign(TextAlign.Center)
  }
  .columnStart(1).columnEnd(2)    // 跨越第 1、2 列
  .rowStart(1).rowEnd(1)          // 只占第 1 行
  .backgroundColor('#FF009688')   // 青绿
  .borderRadius(8)
  .border({ width: 2, color: Color.White })
  .width('100%')
  .height('100%')
}
.columnsTemplate('1fr 1fr 1fr')     // 底层仍是 3 列网格
.rowsTemplate('1fr 1fr')            // 2 行
.columnsGap(8)
.rowsGap(8)
.height(180)
.width('100%')
.padding(8)
.backgroundColor('#14000000')
.borderRadius(12)

布局的展开过程:

步骤一:先看 columnsTemplate('1fr 1fr 1fr')rowsTemplate('1fr 1fr'),确定了这是一个 3 列 × 2 行的网格。视觉上可以想象成 6 个空白格子的表格。

步骤二:第 1 个 GridItem 设置了 columnStart(0).columnEnd(2)rowStart(0).rowEnd(0)。它占据了从列线 0 到列线 2(即第 0、1、2 列)和行线 0 到行线 0(即第 0 行)的区域。也就是说,它横跨了第 0 行的全部 3 列,形成了一行通栏。

步骤三:第 2 个 GridItem 设置了 columnStart(0).columnEnd(0)rowStart(1).rowEnd(1)。它占据第 1 行第 0 列——一个普通的单格。

步骤四:第 3 个 GridItem 设置了 columnStart(1).columnEnd(2)rowStart(1).rowEnd(1)。它占据第 1 行的第 1 列和第 2 列,跨越了 2 列。

最终效果:

┌──────────────────────────┐
│       跨3列              │  ← 第 0 行,跨 3 列
├────────┬─────────────────┤
│   A    │     跨2列        │  ← 第 1 行,左侧单格 + 右侧跨 2 列
└────────┴─────────────────┘

这种"顶部通栏 + 底部两栏"的布局结构,在真实应用中随处可见。典型的场景包括:

  • 文章详情页:顶部是大标题图片(通栏跨列),下方左侧是作者头像(小格),右侧是文章摘要(大格)
  • 商品详情页:顶部是商品主图轮播(通栏),下方分别是价格、规格选择、购买按钮
  • 个人主页:顶部是封面图和头像(通栏),下方左侧是粉丝数等统计信息,右侧是动态列表

七、将 Grid 应用于实际项目

掌握了 Grid 布局的基本概念之后,让我们来看几个真实项目中的典型应用场景。

7.1 场景一:相册网格

@Component
struct PhotoGrid {
  private photos: string[] = [];  // 图片路径数组

  build() {
    Grid() {
      ForEach(this.photos, (src: string) => {
        GridItem() {
          Image(src)
            .objectFit(ImageFit.Cover)
            .width('100%')
            .height('100%')
        }
      })
    }
    .columnsTemplate('repeat(3, 1fr)')   // 3 列等宽
    .rowsTemplate('repeat(3, 1fr)')      // 3 行等高
    .columnsGap(2)                        // 微小间距模拟"缝隙"
    .rowsGap(2)
    .height(360)
  }
}

这是 Grid 布局最常见的应用场景。相册网格要求图片缩略图均匀排列,3 列等宽是标准方案。使用 Grid,你只需要定义列数和间距,剩下的对齐工作全部自动完成。

需要注意的是,当图片数量较大(超过 50 张)时,应该使用 LazyForEach 替代 ForEach 以优化性能,这一点在后面的性能章节会详细讨论。

7.2 场景二:商品陈列

.columnsTemplate('1fr 1fr')     // 2 列均衡
.rowsTemplate('auto')           // 行高自适应内容

电商应用中的商品列表通常采用 2 列布局,因为 2 列在手机屏幕上既能充分展示商品细节,又不会让单个商品卡片过小。

这里 rowsTemplate 设置为 'auto' 而不是 '1fr',是因为商品卡片的内容高度可能不同(有些商品描述较长),auto 让每一行的高度根据该行中最高的商品卡片自适应,不会出现内容被截断的问题。

7.3 场景三:仪表盘看板

.columnsTemplate('1fr 1fr')        // 2 列
.rowsTemplate('100px 100px 200px') // 前两行固定高度,第三行更高

监控仪表盘通常包含多种尺寸的卡片。简单指标(CPU 使用率、内存占用)用小卡片,复杂图表(趋势图、饼图)用大卡片。

通过 Grid 的跨行跨列能力,可以轻松实现"小卡占 1×1,大卡占 2×2"的布局模式,让不同重要性的指标拥有不同的视觉权重。

7.4 场景四:控制面板

.columnsTemplate('repeat(4, 1fr)')  // 4 列等宽
.rowsTemplate('repeat(3, 1fr)')     // 3 行等高

智能家居的控制面板通常采用 4×3 或 4×4 的网格,每个 GridItem 对应一个设备控制按钮(灯光开关、温度调节、窗帘控制等)。Grid 布局让这些按钮按照规则矩阵排列,整齐划一。


八、GridView 的高级用法

除了前文介绍的基础用法,Grid 布局还提供了一些高级功能,适用于更复杂的场景。

8.1 Grid 的可滚动模式

Grid 默认具有纵向滚动能力——当 GridItem 的总尺寸超过 Grid 容器的尺寸时,容器会变为可滚动。

Grid() {
  // 大量 GridItem...
}
.columnsTemplate('repeat(3, 1fr)')
.rowsTemplate('repeat(3, 1fr)')
.scrollable(ScrollDirection.Vertical)    // 默认值,可纵向滚动

如果你不希望 Grid 滚动(比如在 Scroll 内部嵌套 Grid,或者 Grid 的高度是固定的),可以禁用滚动:

.scrollable(ScrollDirection.None)        // 禁用滚动

这样可以避免与外部 Scroll 的滚动冲突。

8.2 Grid 的编辑模式

Grid 支持编辑模式,允许用户拖拽调整 GridItem 的位置。通过 editMode 属性和相关事件回调实现:

Grid() {
  // GridItem...
}
.editMode(true)
.onDragStart((event: ItemDragInfo) => {
  // 拖拽开始
})
.onDrop((event: ItemDragInfo) => {
  // 拖拽放下
})

这在"首页图标编辑"、"自定义仪表盘布局"等场景中非常实用。

8.3 GridItem 的样式自定义

GridItem 除了 columnStart/EndrowStart/End 属性外,还支持常见的通用样式属性:

GridItem()
  .border({ width: 1, color: '#E0E0E0' })   // 边框
  .borderRadius(12)                          // 圆角
  .shadow({ radius: 4, color: '#40000000' }) // 阴影
  .padding(8)                                // 内边距
  .margin(4)                                 // 外边距(注意:margin 会影响网格布局计算)

需要注意的是,margin 会影响 GridItem 在网格中的实际占位尺寸,可能导致与其他 GridItem 的间距不一致。建议优先使用 columnsGaprowsGap 控制间距,而 margin 仅用于特定情况下的微调。

8.4 网格线的可视化

在调试阶段,可以通过为 GridItem 添加临时边框来可视化网格边界:

GridItem()
  .border({
    width: 1,
    color: '#FF0000',    // 红色边框,方便调试
    style: BorderStyle.Dashed
  })

或者为 Grid 容器整体添加一个半透明的背景色,来观察网格的总体区域:

Grid()
  .backgroundColor('#1E000000')  // 半透明黑色背景

8.5 使用 @State 动态更新网格属性

Grid 的 columnsTemplate 可以绑定到 @State 变量,从而实现动态调整列数:

@Component
struct DynamicGrid {
  @State columnCount: number = 3;

  build() {
    Column() {
      // 控制按钮
      Row({ space: 8 }) {
        Button('2列').onClick(() => { this.columnCount = 2; })
        Button('3列').onClick(() => { this.columnCount = 3; })
        Button('4列').onClick(() => { this.columnCount = 4; })
      }
      .padding(8)

      // 动态网格
      Grid() {
        ForEach([1, 2, 3, 4, 5, 6, 7, 8], (i: number) => {
          GridItem() {
            Text(`${i}`).fontSize(20)
          }
          .backgroundColor('#FF3F51B5')
          .height(80)
        })
      }
      .columnsTemplate(`repeat(${this.columnCount}, 1fr)`)
      .columnsGap(8)
      .rowsGap(8)
      .padding(8)
    }
  }
}

在这个示例中,点击"2列"按钮,columnCount 变为 2,columnsTemplate 变为 'repeat(2, 1fr)',网格立即从 3 列切换为 2 列。这种动态控制的能力在"自适应布局"场景中非常有用。


九、常见陷阱与最佳实践

9.1 陷阱一:忘记设置 Grid 的高度

这是最常见的错误。

// ❌ 错误:没有设置 height,rowsTemplate 不会生效
Grid() {
  // GridItem...
}
.columnsTemplate('repeat(3, 1fr)')
.rowsTemplate('1fr 1fr 1fr')   // 不会生效!

// ✅ 正确:显式设置 height
Grid() { /* ... */ }
.rowsTemplate('1fr 1fr 1fr')
.height(300)

Grid 的默认高度为 0,如果不显式设置 heightrowsTemplate 中的所有单位(无论是 1fr 还是 100px)都不会生效,因为容器高度为 0。这个 bug 在开发中非常容易被忽略,因为 Column 和 Row 等容器在没有显式设置高度时是"由内容撑开"的,但 Grid 不同——它严格按照 rowsTemplate 决定行高。

9.2 陷阱二:GridItem 内容未填满单元格

// ❌ 错误:GridItem 内部组件未填满
GridItem() {
  Text('1')          // Text 默认宽度由内容决定
}
.backgroundColor('#FF3F51B5')   // 背景色只覆盖 Text 区域

// ✅ 正确:GridItem 内部组件填满
GridItem() {
  Text('1')
    .textAlign(TextAlign.Center)
    .width('100%')
    .height('100%')
}
.backgroundColor('#FF3F51B5')

没有 width('100%')height('100%') 时,Text 组件只占据文字本身所需的区域,背景色也只作用于这个区域。这会导致网格中看起来有"缝隙"——实际的 GridItem 尺寸是满格的,但视觉上背景色没有覆盖满格。解决方案:在 GridItem 的子组件上显式设置 width('100%')height('100%')

9.3 陷阱三:GridItem 数量与网格单元不匹配

如果 GridItem 数量少于网格单元总数:

Grid() {
  // 只有 5 个 GridItem
  ForEach([1, 2, 3, 4, 5], (i: number) => { /* ... */ })
}
.columnsTemplate('1fr 1fr 1fr')   // 3 列
.rowsTemplate('1fr 1fr')          // 2 行 → 共 6 个单元

结果:第 6 个单元(第 1 行第 2 列)是空的。这在视觉上可能表现为一个"空洞"。

如果 GridItem 数量多于网格单元总数:

Grid() {
  // 有 10 个 GridItem
  ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (i: number) => { /* ... */ })
}
.columnsTemplate('1fr 1fr 1fr')   // 3 列
.rowsTemplate('1fr 1fr')          // 2 行 → 共 6 个单元

结果:第 7-10 个 GridItem 会超出网格范围,但 Grid 默认可以滚动,用户可以通过滚动查看超出的部分。

9.4 最佳实践一:优先使用 LazyForEach

当 GridItem 数量超过 20 个时,使用 LazyForEach 代替 ForEachLazyForEach 会按需创建和销毁不可见的 GridItem,大幅降低内存占用和初始渲染时间。

// 数据源类需要实现 IDatableSource 接口
class MyDataSource implements IDataSource {
  private data: number[] = [];

  totalCount(): number {
    return this.data.length;
  }

  getData(index: number): number {
    return this.data[index];
  }

  // 其他必要接口方法...
}

Grid() {
  LazyForEach(this.dataSource, (item: number) => {
    GridItem() {
      Text(`${item}`)
    }
  }, (item: number) => item.toString())
}
.columnsTemplate('repeat(3, 1fr)')

9.5 最佳实践二:避免深层嵌套

GridItem 内部的组件树深度会影响布局计算和渲染性能。建议 GridItem 内部的组件层级控制在 3~5 层以内。

// ✅ 推荐:组件树深度适中
GridItem() {
  Column({ space: 4 }) {
    Image($r('app.media.thumb'))
      .width('100%').height(120)
      .objectFit(ImageFit.Cover)
    Text('标题').fontSize(14)
    Text('描述').fontSize(12).fontColor(Color.Gray)
  }
  .padding(8)
}

// ❌ 不推荐:组件树过深
GridItem() {
  Column() {
    Row() {
      Column() {
        Row() {
          Text('过于嵌套').fontSize(10)
        }
      }
    }
  }
}

过深的组件嵌套不仅影响性能,还会让代码难以阅读和维护。

9.6 最佳实践三:合理使用跨行跨列的度

跨行跨列虽然强大,但过度使用会导致布局难以理解和维护。建议:

  • 在同一个 Grid 中,跨行跨列的 GridItem 数量不要超过总数的 30%
  • 避免一个 GridItem 同时跨 4 行以上和 3 列以上的大区域
  • 跨行跨列的 GridItem 尽量放在网格的上方或左侧(遵循阅读顺序)

十、Grid 与 ArkUI 其他布局的对比

ArkUI 提供了多种布局容器,了解它们的各自定位有助于在项目中选择正确的工具。

布局容器 定位描述 适用场景 不适用场景
Grid 二维规则网格 相册、商品列表、仪表盘、宫格菜单 单行排列、不规则高度
Column 一维垂直排列 表单、列表项、垂直菜单 水平排列、多列布局
Row 一维水平排列 导航栏、水平按钮组、标签栏 垂直排列、多行布局
Flex 一维弹性排列 + 换行 标签云、不确定性数量的子项 严格的二维对齐
Stack 层叠排列 悬浮按钮、遮罩层、分层 UI 自动排列多个子项
RelativeContainer 锚点相对定位 复杂动态布局、自适应界面 大量子项的自动排列
List 高性能长列表 聊天记录、新闻列表、设置页 二维网格排列
WaterFlow 不规则瀑布流 图片瀑布流、动态卡片 需要严格行列对齐

10.1 Grid vs List

很多初学者会混淆 Grid 和 List。两者的核心区别在于:

  • List一维的,子项以列表形式在垂直或水平方向排列。它优化了长列表的滚动性能,支持子项回收复用。
  • Grid二维的,子项在行列网格中排列。它不擅长大量数据的虚拟化(虽然可以通过 LazyForEach 优化),但擅长实现复杂的格子布局。

选择建议:如果你的布局逻辑上是"一个接一个地排列"(即使它看起来像网格,比如用 Row 实现的伪网格),用 List。如果布局逻辑上是"在行列矩阵中精确放置",用 Grid。

10.2 Grid vs WaterFlow(瀑布流)

  • Grid 要求所有行严格对齐,同一行的所有 GridItem 高度相同。
  • WaterFlow 允许每个子项有不同的高度,子项会"挤入"上一行留下的空隙。

选择建议:如果你想实现"格子整齐划一"的效果(图库、商品列表),用 Grid。如果你想实现"错落有致的卡片墙"效果(Pinterest 风格的图片瀑布流),用 WaterFlow。


十一、调试技巧与工具

11.1 使用 DevEco Studio 的 Inspector 工具

DevEco Studio 内置的 Inspector 工具是排查布局问题的最佳利器。使用方法:

  1. 在模拟器或真机上运行应用
  2. 在 DevEco Studio 中打开 Inspector 标签页
  3. 点击页面上的 Grid 区域,Inspector 会自动定位到对应的 Grid 节点
  4. 在右侧属性面板中查看 Grid 的 columnsTemplaterowsTemplatecolumnsGaprowsGap 等属性的实际计算值

Inspector 还可以显示组件的盒模型(border-box),让你直观地看到每个 GridItem 的边距、边框、内边距和内容区域。

11.2 使用网格线辅助显示

在调试阶段,可以开启网格线辅助显示:

Grid()
  .columnsTemplate('1fr 1fr 1fr')
  .rowsTemplate('1fr 1fr 1fr')
  // 通过背景色和边框凸显网格结构
  .backgroundColor('#1E000000')    // 半透明背景

每个 GridItem 内部添加临时边框:

GridItem()
  .border({
    width: 1,
    color: '#FF0000',
    style: BorderStyle.Dashed
  })

如果发现某个 GridItem 的边框没有对齐,说明网格行或列的尺寸计算有问题。

11.3 常见问题快速排查表

问题现象 可能原因 解决方案
Grid 高度为 0 未设置 height 添加 .height(值)
GridItem 背景色只覆盖部分区域 子组件未设置 width('100%').height('100%') 在子组件上添加填充属性
网格中有意外的间隙 GridItem 的 margin 或子组件的 padding 移除 margin,使用 columnsGap/rowsGap
行高不一致 使用了 auto 但内容不同 改用 1fr 统一行高
滚动不生效 父容器拦截了滚动事件 检查父容器是否也设置了滚动
Grid 不滚动 设置了 scrollable(ScrollDirection.None) 移除或改为 ScrollDirection.Vertical
repeat 不生效 参数格式错误 检查 repeat(次数, 模式) 的语法
跨列后布局错乱 columnEnd 索引超出网格线数 确保 end ≤ 总列数 - 1

十二、综合问答

Q:Grid 和 CSS Grid 有什么区别?

A:两者在概念上非常相似(列模板、行模板、跨列、跨行、fr 单位),但 ArkUI 的 Grid 更侧重于移动端布局,默认支持滚动,且与 ArkUI 的组件体系深度集成。CSS Grid 的某些高级特性(如 grid-template-areasgrid-auto-flow)在 ArkUI 中暂不支持。

Q:可以在一个页面中使用多个 Grid 吗?

A:完全没问题。每个 Grid 是独立的容器,互不影响。在复杂的页面中,你可能会有多个 Grid 布局不同的区域(相册区、推荐区、排行榜区),每个 Grid 有自己的 columnsTemplaterowsTemplate

Q:Grid 支持横向滚动吗?

A:支持。通过设置 .scrollable(ScrollDirection.Horizontal) 可以让 Grid 在水平方向滚动。这在"横向滑动的分类导航"等场景中很有用。

Q:columnsTemplaterowsTemplate 可以动态改变吗?

A:可以。将 columnsTemplate 绑定到 @State 变量,通过状态更新即可动态改变网格的列数和列宽。这在自适应布局(平板 vs 手机)中非常有用。

Q:GridItem 的宽高比例可以控制吗?

A:可以通过 Grid 容器的 aspectRatio 属性控制所有 GridItem 的宽高比。如果需要对单个 GridItem 单独控制,可以在 GridItem 内部通过 aspectRatio 属性实现。

Q:GridItem 支持点击事件吗?

A:支持。GridItem 继承自基组件,原生支持 .onClick().onTouch().onLongPress() 等手势事件。

GridItem() {
  Text('可点击的格子')
}
.onClick(() => {
  console.info('格子被点击了')
})

Q:如何实现 Grid 中某个格子的选中态高亮?

A:在 GridItem 内部使用 @State 控制背景色,点击时切换颜色值:

@Component
struct SelectableGridItem {
  @State isSelected: boolean = false;

  build() {
    GridItem() {
      Text('可选中的格子')
    }
    .backgroundColor(this.isSelected ? '#FF2196F3' : '#FF9E9E9E')
    .onClick(() => {
      this.isSelected = !this.isSelected;
    })
  }
}

Q:fr% 可以混合使用吗?

A:可以,但需要理解它们的计算顺序不同:% 基于容器总宽度计算,fr 基于扣除固定值和间距后的剩余宽度计算。混合使用时,两者的基准不同,建议仔细计算预期结果。

Q:多个 GridItem 可以叠加在同一个网格单元上吗?

A:不可以。Grid 设计的前提是一个网格单元由一个 GridItem 占据。如果有重叠需求,应该在 GridItem 内部使用 Stack 来层叠子组件。


十三、总结与下期预告

本文从 Grid 布局的核心概念出发,深入讲解了 columnsTemplaterowsTemplatefr 单位、repeat 语法、跨行跨列等关键知识点,并通过一个完整的实战 Demo 逐行展示了每种用法的实现方法。

回顾四个关键知识点:

  1. 模板字符串是 Grid 布局的"骨架"——通过 columnsTemplaterowsTemplate 定义网格的行列结构。
  2. fr 单位是弹性分配的核心——在扣除固定宽度和间距后,按比例分配剩余空间。
  3. 跨行跨列是 Grid 超越嵌套布局的杀手级能力——让 GridItem 跨越多个网格单元,实现非对称、不规则的复杂布局。
  4. 间距控制是精细化调整的关键——通过 columnsGaprowsGap 控制网格线之间的距离,让布局疏密有致。

掌握了这些核心概念,你就能够灵活运用 Grid 布局来构建从简单九宫格到复杂仪表盘的各种 UI 结构。

Grid 布局还有更多值得探索的特性。下一篇文章我们将深入讲解 Grid 自适应列数 的实现原理——如何通过监听容器尺寸变化动态调整网格结构,实现从手机到平板的完美自适应。同时,我们还将介绍 LazyForEach 与 Grid 的结合使用,展示如何构建高性能的商品列表和图片画廊。


本文代码基于 HarmonyOS NEXT API 24,使用 ArkTS 语言编写。完整 Demo 代码位于 entry/src/main/ets/pages/Index.ets,可直接在 DevEco Studio 中运行体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐