【共创季稿事节】鸿蒙原生 ArkTS 布局精讲:Grid 行列间距控制 —— rowsGap / columnsGap 完全指南
鸿蒙原生 ArkTS 布局精讲:Grid 行列间距控制 —— rowsGap / columnsGap 完全指南



一、引言
在鸿蒙原生应用开发中,布局是构建用户界面的基石。HarmonyOS NEXT 提供了多种声明式布局容器,其中 Grid(网格布局) 是最强大、最灵活的布局方式之一。它将容器划分为行(Row)和列(Column),子组件按顺序填入网格单元,天然适合相册、商品列表、仪表盘、宫格菜单等场景。
在实际开发中,仅仅将元素排列成网格往往不够——我们还需要精确控制网格单元之间的间距。例如:
- 商品卡片之间需要 12vp 的左右间距和 16vp 的上下间距;
- 照片墙需要紧密排列(间距为 0),但每个照片块本身带内边距;
- 仪表盘指标卡片之间需要更大的垂直间距来分层。
ArkTS 的 Grid 组件为此提供了两个专用 API:rowsGap 和 columnsGap。它们分别控制行与行、列与列之间的间距,互不干扰,可以分别设置不同的值。
本文通过一个完整的交互式示例,深入讲解 rowsGap / columnsGap 的用法、原理和最佳实践。
二、Grid 布局基础回顾
2.1 什么是 Grid?
Grid 是 ArkUI 提供的网格布局容器,核心思路是将可用空间划分为**行(rows)和列(columns)**的二维矩阵,子组件 GridItem 按从左到右、从上到下的顺序依次填充到网格单元中。
2.2 核心属性一览
| 属性 | 类型 | 作用 | 示例 |
|---|---|---|---|
rowsTemplate |
string | 定义行高模板 | '60px 60px 60px' 或 '1fr 2fr 1fr' |
columnsTemplate |
string | 定义列宽模板 | '1fr 1fr 1fr' 或 '100px auto 100px' |
rowsGap |
Length | 行与行之间的垂直间距 | .rowsGap(16) |
columnsGap |
Length | 列与列之间的水平间距 | .columnsGap(12) |
editable |
boolean | 是否允许拖拽调整行列大小 | — |
rowsTemplate 和 columnsTemplate 决定网格的骨架结构,而 rowsGap 和 columnsGap 控制的是骨架单元之间的缝隙,两者分工明确。
2.3 为什么需要分别控制行列间距?
试想几个真实场景:
- 商品列表:商品卡片之间,左右间距 12vp 就足够,但上下间距可能需要 20vp 以容纳价格、标题两行文字,视觉上需要更多呼吸空间。
- 日历组件:日期格子之间的左右间距通常较小(便于密集排列),但行间距可以稍大以区分周。
- 表单布局:标签和输入框在一行内紧挨,但不同字段之间需要更大的垂直间距。
如果只有一个统一的 gap 属性(如 Flex 布局),上述场景就无法实现。rowsGap 和 columnsGap 的分离设计正是为了满足这种差异化需求。
三、rowsGap 和 columnsGap 详解
3.1 基本用法
在 ArkTS 中,rowsGap 和 columnsGap 是 Grid 组件的链式方法,接受 Length 类型参数(单位默认为 vp——虚拟像素):
Grid() {
// GridItem 子组件...
}
.rowsTemplate('60px 60px 60px')
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(16) // ← 行间距 16vp
.columnsGap(12) // ← 列间距 12vp
3.2 取值类型
| 写法 | 含义 | 示例 |
|---|---|---|
| 纯数字 | 按 vp 单位 | .rowsGap(16) → 16vp |
| 字符串 + px | 按物理像素 | .rowsGap('16px') |
| 字符串 + vp | 按虚拟像素 | .rowsGap('16vp') |
| 字符串 + % | 按容器尺寸百分比 | .rowsGap('2%') |
| Resource 对象 | 引用资源文件 | .rowsGap($r('app.float.grid_gap')) |
| 0 或 ‘0px’ | 无间距 | 紧贴排列 |
最佳实践:推荐用纯数字或 vp 单位——HarmonyOS 的 vp 会自动适配不同屏幕密度。
3.3 间距计算模型
Grid 的行列间距计算遵循以下公式:
Grid 总高度 = 所有行高之和 + rowsGap × (行数 - 1)
Grid 总宽度 = 所有列宽之和 + columnsGap × (列数 - 1)
示例:一个 3 行 3 列的 Grid,每行高 60px,rowsGap = 16px,则:
- 总高度 = 60 + 16 + 60 + 16 + 60 = 212px
- 如果 columnsGap = 12px,每列宽 100px,则总宽度 = 100 + 12 + 100 + 12 + 100 = 324px
理解这个模型可避免内容截断或留白过多。
四、实战演示:交互式间距控制应用
为了让读者直观感受 rowsGap 和 columnsGap 的效果,我们用 ArkTS 构建了一个演示应用,核心思路:
- 三个对比场景:场景一(等间距)、场景二(行大列小)、场景三(行小列大);
- 实时交互调节:每个场景通过 Slider 滑动条动态修改 rowsGap 和 columnsGap;
- 总览对比:底部将三个配置并排展示,一眼看出差异;
- 色彩编码:每个网格单元用不同背景色,行列位置一目了然。
4.1 完整代码
/**
* Grid 布局 —— rowsGap / columnsGap 行列间距控制演示
*
* 关键技术点:
* - rowsGap: 行与行之间的垂直间距
* - columnsGap: 列与列之间的水平间距
* - @Builder: 封装可复用的 UI 片段
* - @State: 绑定滑动条,实时响应
*/
@Entry
@Component
struct Index {
@State columnsGap1: number = 8 // 场景一:列间距
@State rowsGap1: number = 8 // 场景一:行间距
@State columnsGap2: number = 8 // 场景二:列间距(小)
@State rowsGap2: number = 24 // 场景二:行间距(大)
@State columnsGap3: number = 24 // 场景三:列间距(大)
@State rowsGap3: number = 8 // 场景三:行间距(小)
build() {
Scroll() {
Column({ space: 24 }) {
// 标题区域
Text('Grid 行列间距控制')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 16 })
Text('rowsGap / columnsGap 可分别控制行与行、列与列的间距')
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ bottom: 8 })
// ── 场景一:行列等间距 ──
this.demoSection(
'场景一:行列等间距 rowsGap=8 columnsGap=8',
this.rowsGap1,
this.columnsGap1,
(r: number, c: number) => { this.rowsGap1 = r; this.columnsGap1 = c },
196
)
// ── 场景二:行间距大、列间距小 ──
this.demoSection(
'场景二:行间距大、列间距小 rowsGap=24 columnsGap=8',
this.rowsGap2,
this.columnsGap2,
(r: number, c: number) => { this.rowsGap2 = r; this.columnsGap2 = c },
244
)
// ── 场景三:行间距小、列间距大 ──
this.demoSection(
'场景三:行间距小、列间距大 rowsGap=8 columnsGap=24',
this.rowsGap3,
this.columnsGap3,
(r: number, c: number) => { this.rowsGap3 = r; this.columnsGap3 = c },
196
)
// ── 底部总览对比 ──
Column({ space: 8 }) {
Text('快速对比总览').fontSize(16).fontWeight(FontWeight.Medium)
Row({ space: 12 }) {
Column({ space: 4 }) {
Text('等距 8·8')
this.buildMiniGrid(8, 8)
}.layoutWeight(1)
Column({ space: 4 }) {
Text('行大 24·8')
this.buildMiniGrid(24, 8)
}.layoutWeight(1)
Column({ space: 4 }) {
Text('列大 8·24')
this.buildMiniGrid(8, 24)
}.layoutWeight(1)
}.width('100%')
}
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.width('100%')
}
.padding(16)
}
.width('100%')
.height('100%')
}
@Builder
demoSection(
title: string,
rowsGap: number,
colsGap: number,
onChange: (r: number, c: number) => void,
gridHeight: number
) {
Column({ space: 8 }) {
Text(title).fontSize(16).fontWeight(FontWeight.Medium)
Grid() {
ForEach([0, 1, 2], (row: number) => {
ForEach([0, 1, 2], (col: number) => {
this.gridItem(row, col)
})
})
}
.rowsTemplate('60px 60px 60px')
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(rowsGap) // ← 核心:行间距
.columnsGap(colsGap) // ← 核心:列间距
.width('100%')
.height(gridHeight)
// rowsGap 滑块
Row({ space: 8 }) {
Text('rowsGap')
Slider({ value: rowsGap, min: 0, max: 48, step: 1 })
.onChange((v: number) => { onChange(v, colsGap) })
.layoutWeight(1)
Text(`${rowsGap} vp`).width(44)
}.width('100%')
// columnsGap 滑块
Row({ space: 8 }) {
Text('columnsGap')
Slider({ value: colsGap, min: 0, max: 48, step: 1 })
.onChange((v: number) => { onChange(rowsGap, v) })
.layoutWeight(1)
Text(`${colsGap} vp`).width(44)
}.width('100%')
}
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.width('100%')
}
@Builder
gridItem(row: number, col: number) {
GridItem() {
Text(`R${row}C${col}`)
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('100%')
.height('100%')
}
.backgroundColor(COLORS[(row * 3 + col) % COLORS.length])
.borderRadius(8)
.width('100%')
.height(60)
}
@Builder
buildMiniGrid(rowGap: number, colGap: number) {
Grid() {
ForEach([0, 1, 2], (row: number) => {
ForEach([0, 1, 2], (col: number) => {
GridItem() {
Text(`R${row}C${col}`)
.fontSize(7)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
}
.backgroundColor(COLORS[(row * 3 + col) % COLORS.length])
.borderRadius(3)
.height(28)
})
})
}
.rowsTemplate('28px 28px 28px')
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(rowGap)
.columnsGap(colGap)
.width('100%')
.height(28 * 3 + rowGap * 2)
}
}
const COLORS: string[] = [
'#FF5B8F', '#FF8A65', '#FFC107',
'#4CAF50', '#2196F3', '#9C27B0',
]
4.2 代码要点解析
4.2.1 关于 @Builder
ArkTS 中 UI 片段不能用普通函数返回,必须用 @Builder 装饰器标记:
// ❌ 错误:普通函数返回 GridItem 不被允许
function buildGridItem(row: number, col: number): GridItem { ... }
// ✅ 正确:@Builder 方法
@Builder
gridItem(row: number, col: number) { ... }
4.2.2 关于 @State 与响应式更新
@State 装饰的变量改变时,依赖它的 UI 会自动重新渲染。这是 Slider 拖动时网格间距实时更新的机制:
用户拖动 Slider
→ onValueChange 回调触发
→ 修改 @State columnsGap1 / rowsGap1 的值
→ Grid 组件检测到 rowsGap / columnsGap 属性变化
→ 重新布局,绘制新的间距
4.2.3 关于模板字符串
rowsTemplate 和 columnsTemplate 使用空格分隔的字符串来定义多个轨道:
'60px 60px 60px'→ 3 行,每行高度固定为 60px'1fr 1fr 1fr'→ 3 列,每列等分剩余空间(fr是分数单位,类似 CSS Grid 的 fr)
如果需要不对称分配,也可以写 '2fr 1fr'(第一列占 2/3,第二列占 1/3)。
五、间距控制的高级技巧
5.1 间距为 0 的场景
某些场景需要网格紧贴排列,例如:
Grid() { /* ... */ }
.rowsGap(0)
.columnsGap(0)
此时网格块之间没有缝隙,看起来像一张完整的图片或地图瓦片拼接。
5.2 结合 padding 实现内外间距分离
有时你希望网格块之间没有间距(rowsGap/columnsGap = 0),但整个 Grid 容器与外部元素有间距——这时用 padding 来实现:
Grid() { /* ... */ }
.rowsGap(0)
.columnsGap(0)
.padding(16) // 容器内边距,不参与网格计算
5.3 使用 fraction 单位与间距的联动
当列宽使用 fr 单位时,间距会影响每列的实际内容宽度。假设 Grid 容器宽度为 360px,columnsTemplate('1fr 1fr 1fr'),columnsGap = 12px:
可用宽度 = 360px
间距占用的总宽度 = 12px × 2(3 列之间有 2 个间隙)= 24px
每列内容宽度 = (360 - 24) ÷ 3 = 112px
间距越大,每列的内容宽度越小。设计时需要考虑这个联动效应,尤其是在窄屏设备上。
5.4 使用 Resource 对象统一管理间距值
在大型项目中,建议将间距值抽离到资源文件中统一管理:
// resources/base/element/float.json
{
"float": [
{ "name": "grid_rows_gap", "value": "12vp" },
{ "name": "grid_columns_gap", "value": "8vp" }
]
}
Grid() { /* ... */ }
.rowsGap($r('app.float.grid_rows_gap'))
.columnsGap($r('app.float.grid_columns_gap'))
这样做的好处是:修改间距只需改一处资源文件,全局生效,避免硬编码散落在各个页面中。
六、常见问题与避坑指南
6.1 「Grid 内容显示不全」
现象:Grid 中部分 GridItem 被截断或没有显示。
根因:Grid 容器的高度没有给够。记得总高度 = 行高之和 + rowsGap × (行数 - 1)。
解决办法:计算 Grid 容器高度时加上间距,或设置 .height('auto') 让 Grid 自适应内容高度。
6.2 「滑动条不生效」
现象:拖动 Slider,但网格间距不变。
根因:常见的两种原因——
- 滑动条绑定的不是
@State变量; onChange中修改了变量但没有重新赋值(数组/对象的引用未变)。
解决办法:确保声明了@State,并在onChange中直接赋新值。
6.3 「rowsGap 和 columnsGap 效果看起来一样」
可能性:网格只有一行或一列。当只有一行时,rowsGap 不产生任何效果(因为没有行间缝隙);只有一列时同理,columnsGap 不生效。
6.4 性能注意
当 Grid 中的 @State 变化时,整个 Grid 会重新布局。如果 GridItem 数量巨大(几百上千个),频繁拖动 Slider 可能引起掉帧。优化建议:
- 使用
LazyForEach替代ForEach实现按需加载; - 配合
.cachedCount()预缓存前后若干项; - 在拖动结束后再触发间距更新(利用 Slider 的
onChangeEnd回调)。
七、扩展思考
7.1 与其他布局的横向对比
| 布局 | 间距 API | 特点 |
|---|---|---|
| Grid | rowsGap + columnsGap |
二维矩阵,行列间距独立控制 |
| Column | space |
一维垂直排列,单一间距值 |
| Row | space |
一维水平排列,单一间距值 |
| Flex | space |
弹性布局,单一间距值 |
| Stack | — | 层叠布局,无间距概念 |
| List | space |
列表项间距,单一值 |
横向对比可见,Grid 是唯一支持行/列间距分别控制的布局,这正是它的独特优势。
7.2 与 CSS Grid 的类比
如果你有 Web 开发背景,可以这样类比:
| ArkTS Grid | CSS Grid | 说明 |
|---|---|---|
rowsTemplate |
grid-template-rows |
行高定义 |
columnsTemplate |
grid-template-columns |
列宽定义 |
rowsGap |
row-gap |
行间距 |
columnsGap |
column-gap |
列间距 |
fr 单位 |
fr 单位 |
分数分配剩余空间 |
这种设计语言的相似性降低了跨平台开发者的学习成本。
7.3 间距与无障碍设计
间距不仅影响美观,还关系到无障碍体验。合理的行列间距可以:
- 减少误触:按钮等交互元素之间有足够的间距(建议 ≥ 8vp),降低用户点错概率;
- 增强可读性:行间距拉开后,每一行的内容更容易被视觉追踪;
- 适配大字体:当系统字体放大时,足够的间距保证文字不重叠。
八、总结
本文围绕 ArkTS Grid 布局的 rowsGap 和 columnsGap 进行了深入讲解,核心要点总结如下:
rowsGap控制行间距,columnsGap控制列间距,两者互不干扰,可分别设置零值、正值或资源引用;- Grid 总尺寸 = 行列尺寸之和 + 间距之和,在容器定高时务必考虑间距带来的额外空间;
@Builder是封装 UI 片段的正确方式,普通函数不能返回 UI 节点;@State+ Slider 组合可实现动态调试,直观观察不同间距值的视觉差异;- 间距设计是用户体验的一部分,合理的行/列间距能显著提升界面的可读性和操作友好度。
希望本文能帮助你掌握 Grid 间距控制,在鸿蒙原生开发中更加得心应手。
附录:快速调试技巧
如果你正在调试 Grid 的间距问题,可借助 DevEco Studio 的 Inspector 工具实时查看布局边界:
- 打开 DevEco Studio;
- 在 Previewer 中预览页面;
- 点击工具栏的「Show Layout Bounds」按钮;
- 每个组件的 padding、margin、实际内容区域会以不同颜色的边框显示。
配合 Slider 调整 rowsGap / columnsGap,你可以在 Inspector 中直观看到间距变化对布局边界的影响。
版权声明:本文为 HarmonyOS NEXT 开发技术分享,基于 API 24 编写,可在 DevEco Studio NEXT 中直接运行。
更多推荐



所有评论(0)