1. 项目背景与整体设计思路
1.1 什么场景需要固定行列的Grid
做HarmonyOS应用开发时,尤其在ArkTS声明式UI体系里,Grid布局是绕不开的一个组件。我最早接触它是因为一个多设备适配的卡片页需求:需要在手机和平板上保持一致的信息密度,九宫格形态的菜单入口,每行固定3列,总共2行,不管屏幕怎么变化,这个结构都不能乱。当时第一反应是用Flex配合wrap换行来实现,但很快发现换行后的对齐和间距控制非常费劲,于是转向了Grid的固定行列方案。
所谓固定行列,说直白一点,就是通过columnsTemplate和rowsTemplate两个属性,把网格的列结构和行结构预先定义死。比如3列2行,就是"1fr 1fr 1fr"和"1fr 1fr"。定义完之后,子组件按顺序填充进这些格子,超出部分会自动进入下一行或者触发滚动。这种写法的最大优势在于:布局的结构稳定、代码意图明确,不需要像Flex那样做各种间距补偿,而且在跨屏幕尺寸适配时,只要把fr比例调好,所有格子会等比缩放,视觉上非常整齐。
我后来在多个项目里反复用这个方案,包括相册宫格、首页金刚区图标、数据展示面板,甚至一些表单控件的排布。可以说,掌握了固定行列的Grid写法,很多经典UI场景都能一步到位。这篇文章会结合我在HarmonyOS 6上的实操经验,把这个组件的用法、踩坑点、以及和列表类组件的取舍一次性讲清楚。
1.2 方案选型:为什么不用Flex或List
很多初学者会问:Flex加wrap也可以做网格,为什么非要Grid?我自己的体会是,两者视觉结果相似,但背后的布局逻辑完全不同。
Flex本质是线性布局,通过flexWrap设置为Wrap后,子项在主轴方向上排满就换行。但它的换行行为不受控:一行到底放几个取决于子项宽度和容器宽度,如果子项宽度是固定值,那么不同屏宽下每行数量就会变化,无法保证"始终3列"。即使配合百分比宽度强制平分,也需要人为计算间距和边缘对齐,代码写起来非常啰嗦。
List则更适合线性滑动的长列表场景,它能处理无限滚动,但多列结构需要嵌套Row来实现,而且支撑不了"行数固定、列数固定、同时自动分配剩余空间"这种需求。Grid的存在恰好补齐了这个空白:它是专为二维网格设计的容器,既有固定行列的能力,也有滚动能力,还有行列跨度等高级特性。
从我实际开发的角度看,Grid的选型优先级应该是这样的:
- 需要固定行列、显示区域受限(如卡片、面板、弹窗)→ 选Grid,用固定行列模板
- 需要纵向滚动、但每行内部可能有复杂布局 → 选List嵌套Row,或者Grid单向滚动
- 需要瀑布流、不同格子大小不一 → 用Grid的rowsTemplate和跨度控制,或WaterFlow
- 只是简单横向一排 → Flex或者Row足够
这个取舍逻辑想清楚之后,后面写代码就清晰很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Grid组件核心细节解析
2.1 columnsTemplate与rowsTemplate的正确用法
Grid组件的两个核心模板属性,是我认为必须彻底搞懂的东西。columnsTemplate定义列的排列,rowsTemplate定义行的排列,它们的赋值形式是空格分隔的长度字符串。
来看一个最典型的例子:
typescript复制Grid() {
// 子项
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
这段代码定义了3列2行的网格。1fr表示等分剩余空间,三列各占三分之一的宽度,两行各占二分之一的整体高度(前提是Grid拿到了明确的高度)。如果写成'100 1fr',那就是第一列固定100vp,第二列占据剩余空间。
实际操作中,fr比例是控制网格形态最灵活的手段。比如我要做一个左侧固定宽度、右侧自适应两列的信息展示面板,可以这样写:
typescript复制.columnsTemplate('120px 1fr 1fr')
这里120px会固定第一列的宽度,后面两列均分剩余空间。注意vp单位在多数场景下比px更推荐,因为它能随屏幕密度自动适配。在HarmonyOS ArkTS中,直接写数字会被当作vp单位处理,所以我通常写'120 1fr 1fr'。
rowsTemplate的用法类似,但是有一个我现在特别提醒的点:在不设置rowsTemplate的情况下,Grid的行为是"无限行、按列填充"。也就是说,只有列模板,行会自动生成,子项超过一屏会滚动。而设置了固定rowsTemplate后,网格行数被锁死,多出来的子项不会撑高Grid,而是体现在Grid的可滚动区域里——如果你希望禁止滚动,就必须让所有子项正好填满定义的行列数。
2.2 GridItem的布局控制与跨度
Grid的子组件必须是GridItem,或者使用支持通用属性的组件并加上gridItem的布局约束。在实际编码中,直接用GridItem包裹内容是最稳妥的方式。GridItem有几个属性值得注意。
第一个是rowStart和rowEnd,控制纵向跨度。比如一个8宫格中,某个特殊的卡片需要占两个格子高度,可以这样:
typescript复制Grid() {
GridItem() {
Text('大图区域')
}
.rowStart(1)
.rowEnd(2)
// 其他普通GridItem
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
第二个是columnStart和columnEnd,控制横向跨度。这在跨列的大标题、广告位占据两列的场景特别有用。需要注意,跨度值的计算是从1开始的,比如第二列到第三列,就要写columnStart(2)、columnEnd(3),而不是0和2。
还有一点,GirdItem容器会默认拉伸填满对应的网格单元格,所以内部内容如果要做居中处理,需要在GridItem本身设置justifyContent和alignItems,或者在内部再包一层。这个细节很容易被忽略,结果就是内容贴边,视觉上很别扭。
2.3 与CSS Grid的思维映射
如果你有Web开发背景,学习ArkTS的Grid会非常快,因为这俩在思路上几乎同源。CSS Grid的grid-template-columns对应ArkTS的columnsTemplate,grid-template-rows对应rowsTemplate,grid-row-start/end对应rowStart/rowEnd,grid-column-start/end对应columnStart/columnEnd。甚至fr单位也直接照搬过来了。
我自己是从CSS Grid(尤其是阮一峰老师的Grid教程)入门再转到ArkTS的,这个迁移过程几乎没有成本。但有一个关键差异必须注意:CSS Grid的容器不限制子项数量,超出自动扩展行;ArkTS的Grid则表现得像"固定轨道的无限网格",子项超出后会进入滚动机制。在布局计算上,两者都是基于网格线来定位的,理解了网格线模型,就理解了Grid的精髓。
3. 实操过程:从零搭建固定行列网格
3.1 最基础的2行3列实现
先把我最常用的一个模板分享出来,它是一个标准的六宫格,每个格子展示图标加文字。这个结构在首页功能区、设置中心、工具页都非常常见。
typescript复制@Entry
@Component
struct FixedGridDemo {
@State menuData: Array<{ icon: string; label: string }> = [
{ icon: 'star', label: '收藏' },
{ icon: 'download', label: '下载' },
{ icon: 'history', label: '历史' },
{ icon: 'share', label: '分享' },
{ icon: 'setting', label: '设置' },
{ icon: 'about', label: '关于' }
]
build() {
Column() {
Grid() {
ForEach(this.menuData, (item: { icon: string; label: string }) => {
GridItem() {
Column({ space: 8 }) {
Text(item.icon)
.fontSize(28)
.fontColor('#333333')
Text(item.label)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}, (item: { icon: string; label: string }) => item.label)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding(16)
.width('100%')
.height(200)
}
.width('100%')
.height('100%')
}
}
这里有几个细节我说一下。columnsTemplate和rowsTemplate里的fr,意思是"比例分配"——3个1fr总分当前宽度,每列就是1/3宽度,这个宽度是自动计算的,不需要额外写尺寸。rowsGap和columnsGap控制间距,它们的值只会影响单元格之间的距离,不会让Grid本身变大或变小,这一点和Flex的gap逻辑类似。
高度方面,固定行列的Grid必须给一个明确的高度值,否则行模板不会生效。我上面的代码用.height(200)写死,这样2行正好100vp一个加上间距。如果你希望Grid高度自适应内容,比如高度等于两行内容加上间距,可以用.height('auto')吗?实测是不行的,固定行列模式下必须给确定的高度或比例。这也是我后面会反复强调的一个坑。
3.2 动态生成网格项与数据绑定
实际业务中,固定的6个菜单往往不满足需求。数据可能是后端返回的,可能是用户配置的,数量不确定。这时候需要动态生成GridItem。
ForEach就是标准的动态渲染方式。但有个关键问题:数据数量可能不是行数乘以列数的整数倍。比如固定2行3列,但数据只有5个,第6个格子就是空的,视觉上会缺失一块。处理方式有两种。
第一种是数据补位:在渲染前判断总数,不足的行位列数用空对象补齐。
typescript复制const rowCount = 2
const colCount = 3
const totalCount = rowCount * colCount
while (this.menuData.length < totalCount) {
this.menuData.push({ icon: '', label: '' })
}
然后渲染时遇到空数据就渲染一个占位GridItem:
typescript复制GridItem() {
if (item.label === '') {
Column() {
// 显示一个占位符,或者什么都不显示
}
.width('100%')
.height('100%')
} else {
// 正常内容
}
}
第二种是直接放弃固定行数,改为固定列数加动态行数的模式。这个模式更常用也更灵活:
typescript复制Grid() {
ForEach(...)
}
.columnsTemplate('1fr 1fr 1fr')
不设置rowsTemplate,行数自动扩展,数据多就往下延伸。加上.height和.scrollBar,超过区域就滚动。这种写法在商品列表、动态宫格中非常实用。
我通常在业务里会用第二种,除非UI稿明确要求"固定显示2行,多余的不展示"。如果你需要限制只显示2行,超出部分隐藏不占滚动位置,那就必须配合数据截断处理——在给Grid传数据之前先slice(0, 6),而不是去试图让Grid"吃掉"多余数据。
3.3 响应式适配:不同屏幕下的列数变化
固定行列虽然叫"固定",但并不是说所有屏幕都用同一个模板。HarmonyOS的应用要在手机、平板、折叠屏之间跑,列数以3列跑尺寸,不是不行,但在大屏上会拉得非常宽,视觉上很空旷。
我的做法是使用MediaQuery或者断点监听,动态切换columnsTemplate。ArkTS提供了@StorageLink配合媒体查询的方式,也可以直接用Modifier来判断,但最朴素的做法是:
typescript复制@State currentBreakpoint: string = 'sm'
aboutToAppear() {
this.updateBreakpoint()
}
updateBreakpoint() {
const displayInfo = display.getDefaultDisplaySync()
const width = displayInfo.width
if (width >= 840) {
this.currentBreakpoint = 'lg'
} else if (width >= 600) {
this.currentBreakpoint = 'md'
} else {
this.currentBreakpoint = 'sm'
}
}
然后在build里根据断点设置不同的模板:
typescript复制Grid() {
// 子项
}
.columnsTemplate(
this.currentBreakpoint === 'lg' ? '1fr 1fr 1fr 1fr' :
this.currentBreakpoint === 'md' ? '1fr 1fr 1fr' : '1fr 1fr'
)
.rowsTemplate(
this.currentBreakpoint === 'lg' ? '1fr 1fr 1fr' :
this.currentBreakpoint === 'md' ? '1fr 1fr 1fr' : '1fr 1fr 1fr'
)
注意,这样切换模板时,Grid会重新布局,原有的子项位置会dynamic重排,这个过程在UI上是瞬时完成的,不会造成明显闪烁。但如果你在切换时发现子项状态丢失(比如选中态),就需要为GridItem设置唯一key,确保状态绑定稳定。使用ForEach时,第三个参数就是key生成器:
typescript复制ForEach(
this.menuData,
(item) => { ... },
(item) => item.label
)
这个key值最好用数据的唯一ID而不是索引,因为索引在列表项增删时会变化,导致状态错乱。
3.4 性能优化:避免不必要的重渲染
Grid在数据量大的场景下,如果不做优化,会出现明显的卡顿。尤其是在快速滑动或者数据频繁更新的时候。
一个核心优化手段是懒加载——Grid和List一样支持按需渲染,默认情况下,只有进入可视区域的GridItem才会被创建和布局。这意味着如果你用ForEach传入100条数据,Grid只渲染当前一屏能看到的那几个。这个机制是自动的,无需额外配置。
但有一点要留意:如果GridItem内部包含图片资源或复杂组件,首次滑动到该位置时,可能有一瞬间的加载空白。解决方式包括:图片用占位图、组件复用、以及避免在GridItem里做耗时计算。有些开发者习惯在GridItem里直接调用网络请求,这是非常不推荐的做法——每次滑动到可见位置都可能触发请求,造成性能浪费。
更常见的一个性能问题是columnsTemplate和rowsTemplate的频繁变化。模板字符串每次变化都会触发整个Grid重新measure和layout。如果数据更新频率高,尽量避开频繁修改模板字符串。比如通过状态变量控制模板,确保只在真正的断点切换时才变化,而不是每次数据都赋值一个相同的字符串。
实测中我发现,ArkTS对Grid的优化已经做得不错;对于几百级别的数据量,正常写法基本不会出现卡顿。真正需要担心的是单Item内部布局的复杂程度——如果每个GridItem里堆了多层级嵌套、大量阴影和圆角裁剪,那么即使数据量不大,也可能在低端设备上掉帧。优化策略是让Item内部结构尽量扁平,能用绝对定位就不要套多层容器。
4. 常见问题与排查技巧实录
4.1 固定行列字符串设置无效的问题
我在多个技术群和论坛看到有人说,columnsTemplate和rowsTemplate设置了没效果。要排查这个,先分清楚情况。
如果Grid出现了滚动条且行数没限住,说明rowsTemplate没生效或者被覆盖。最常见的发生在:某个父组件对Grid设置了宽高约束,但Grid自身的宽度或高度是'auto',导致模板单位fr无法解析。fr需要明确的可分配空间,如果Grid的尺寸是自适应内容,那fr退化为auto,行列模板就会错乱。
解决方法:给Grid设置明确的宽度和高度。如果确实希望高度动态,可以把rowsTemplate改成固定值加fr混合,比如'80 80',这样即使高度不是固定值,也能按比例分配。
还有另一个容易忽略的点:如果Grid放在Scroll、Column等容器内,且没有给Grid设置flexShrink为0,在某些布局约束下Grid会被压缩,导致模板等比例缩小。这时给Grid加上constraintSize的minHeight和minWidth,可以避免被过度压缩。
4.2 GridItem内容不居中、被裁剪
内容不居中的根源是GridItem默认的布局对齐方式。GridItem本身是一个容器,默认对齐方式是左上角。你不给GridItem里的内容设置居中,它自然就在左上角。解决方式是显式设置GridItem或其子容器的justifyContent和alignItems。
裁剪问题则多见于固定行高但内容超高的情况。比如一个格子里放了一段长文本,行高只有60vp,文本直接显示不全甚至被裁切。有两种处理方案:
一是让文本自适应缩放,通过textOverflow和maxLines控制:
typescript复制Text(item.description)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
二是适当调整rowsTemplate,给行留出余量。比如'1fr 1fr'改成'1fr 1fr'其实还是均分,但如果你希望第一行高第二行矮,可以写'2fr 1fr',第一行会得到两倍于第二行的高度。
4.3 固定行列模式下滚动失效或意外滚动
Grid默认是支持滚动的。固定行列(设置rowsTemplate)时,如果数据超过了定义的行数,会产生滚动;如果数据正好等于行数,则没有滚动。有时候我想要"锁定网格不滚动",但发现Grid还是能手指拖动。
这种情况通常是因为用户在Grid上设置了scrollBar、edgeEffect等属性,但滚动行为仍然开启。要完全禁止,可以设置:
typescript复制.scrollBar(BarState.Off)
.nestedScroll({
scrollForward: NestedScrollMode.SELF_ONLY,
scrollBackward: NestedScrollMode.SELF_ONLY
})
不过严格来说,Grid并没有提供类似scrollEnabled(false)的开关。替代方案是:在数据层面控制数量,确保刚好填满。如果Grid父级是Scroll,则给Grid设置宽高使其自身不滚动,只借助父级滚动。我在实际的页面里就遇到过这种嵌套滚动冲突:外层Scroll想滚,内层Grid也在滚,体验非常糟糕。解决办法是使用NestedScroll机制,让子Grid的滚动事件上抛给父级Scroll,或者干脆让Grid高度自适应、完全交给外部滚动。
4.4 固定行列项数不够时出现空白格
前面提过,固定2行3列但只有5个数据,第6格空白。对于某些视觉需求,比如图标底部边框必须完整,空白格会非常难看。
我个人更推荐的是"动态行数"策略,也就是只设置columnsTemplate,不设置rowsTemplate。这样数据不足时Grid高度自动收缩,不会出现空白。只有当UI稿明确要求"固定高度矩形区域内显示两行"时,才需要补位数据。
4.5 ArkTS权限申请与Grid无关但常被问
网上有些关于"arkts权限申请"的热词,虽然和Grid布局本身没有直接关系,但涉及到GridItem里加载图片或文件列表时,就需要动态申请权限。HarmonyOS的权限模型分为系统权限和用户权限,像读取媒体文件这种需要在module.json5里声明ohos.permission.READ_IMAGEVIDEO,然后在代码里用abilityAccessCtrl动态申请。这个逻辑我建议在一开始就设计好,否则等Grid布局做好再补权限,会和UI逻辑耦合,改起来非常麻烦。
原理解释一下:权限申请本质是用户授权模型,和UI布局是解耦的。但在实际开发中,GridItem里如果渲染的是用户相册图片,没权限时拿到的是空路径,导致占位UI闪烁。我在处理这个问题时,习惯先获取权限,再加载图片数据,然后才把数据传给Grid渲染。这样Grid只需要关心数据即可。
4.6 如何判断用Grid还是WaterFlow
最后补充一个选型问题。ArkTS里还有WaterFlow组件,它也是网格,但更强调"瀑布流"效果:每行高度不一致,item高度取决于内容。Grid则要求行列轨道整齐划一。
我做过的几个内容流项目,早期为了省事直接用Grid做瀑布流,结果每行高度必须取当前行最高item的高度,导致其他item出现空白,视觉效果非常差。后来切到WaterFlow,用laneConstrain控制列宽,才解决。
选型口诀很简单:如果所有item高度一致或可固定,用Grid;如果item高度取决于内容且高度不一,用WaterFlow。
5. 实际案例复盘:从设计稿到Grid落地
5.1 一个典型的九宫格首页
我拿一个真实的首页金刚区案例来复盘。设计稿要求是这样的:8个功能图标,默认2行4列,每格包含图标和文字,图标尺寸固定,文字字号固定,间距为12vp,网格背景为白色卡片。
第一步是确定行列模板。8个功能,2行4列,模板写为:
typescript复制.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
第二步是处理每个GridItem。我用了Column加两个Text,分别显示图标(这里用SymbolGlyph)和标签。居中通过justifyContent和alignItems实现:
typescript复制GridItem() {
Column({ space: 6 }) {
SymbolGlyph(item.iconName)
.fontSize(24)
Text(item.label)
.fontSize(12)
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
第三步是计算Grid的高度。这里有个实际计算逻辑:2行,行高为Grid高度减去rowsGap后均分。如果Grid容器宽度是359vp,4列每列宽度就是(359 - 24 - 162) / 4约为75vp。行高要让每个格子既不太高也不太矮,我给Grid固定高度为140vp。这样每行高度为(140 - 12 - 162) / 2约为48vp,图标加文字加6vp间距,正好是48左右。
第四步,如果用户设备是平板,列数需要变成4列或者更多,走响应式模板切换逻辑。
这个案例的完整代码可以抽象成通用模板,我后来做了个组件封装,只需传入数据和配置即可复用在多个页面。
5.2 封装一个可复用的FixedGrid组件
看到这里,如果你已经决定在自己的项目里使用Grid固定行列,我建议封装成组件,避免每处都写模板和样式。封装思路:
typescript复制@Component
export struct FixedGrid {
@Prop columns: string = '1fr 1fr'
@Prop rows: string = ''
@Prop colGap: number | string = 0
@Prop rowGap: number | string = 0
@Builder
itemBuilder(item: object) {}
build() {
Grid() {
// 通过slot或Builder方式渲染子项
}
.columnsTemplate(this.columns)
.rowsTemplate(this.rows)
.columnsGap(this.colGap)
.rowsGap(this.rowGap)
}
}
使用方只需传入columns和rows模板以及数据,即可复用一个Grid布局。这里的@Builder回调机制可以保留布局灵活性,我在封装时发现,用@BuilderParam比直接传一个组件数组更灵活,因为它支持在GridItem外部包裹容器逻辑。
5.3 网格间隙与边距的计算技巧
很多人会困惑columnsGap和padding的配合。举个例子:容器宽度360,padding左右各16,那么网格内容区宽度就是328。如果4列、间距12,每列宽度为(328 - 12*3) / 4 = 73。这时候如果某个GridItem要做一个内部的分隔背景,宽度73看着很挤,需要知道实际数值才能准确设计。
我习惯先算好这些数值,再决定GridItem内部的对齐和边距。虽然fr是相对值,但配合计算可以保证视觉精确。
还有一个小技巧:如果希望在最后一行、列不满时,内容左对齐而不是拉伸填充,gridItem的宽度可以设置成固定值而不是100%。比如4列网格只有3个数据,可以让GridItem宽度为73而不是100%,配合justifyContent控制对齐方向。这个方法在实现"金牌榜单前三名"这类场景时特别好用。
6. 我个人在实际使用中的一些体会
Grid固定行列这个能力,看着简单,真正用到项目里才会发现它背后的坑也不少。我在写这篇文章时,又把自己的几个项目翻出来审视了一遍,发现一个好的实践模式是:数据层负责控制数量与内容、布局层只负责结构与间距、样式层负责视觉与状态,三者解耦,这样才能最大程度发挥Grid的优势。
还有一点想特别分享,就是Grid和ArkUI其他布局组件的搭配。比如在Tabs的某一个Tab页里,用Grid作为主体内容,同时配合Search组件和底部Button,Grid必须放在flex布局的中间区域并且设置flexGrow(1),而不是简单给个固定高度。这样在不同分辨率设备上,Grid能自动填满可用空间,又不挤压其他组件。
在HarmonyOS 6的开发环境里(DevEco Studio),Grid组件的实时预览效果已经很流畅了。我一般会把columnsTemplate先写成容易辨别比例的临时值,比如1fr 1fr改写成200px 1fr,快速验证列宽分配逻辑,确认后再改回正式值。这个调试技巧在遇到复杂嵌套布局时能省下大量时间。
最后再分享一个我在适配折叠屏时的做法:折叠屏展开态和折叠态的宽度差异很大,直接使用固定列数在展开态会显得很空。我给Grid的columnsTemplate绑定了一个计算属性,根据当前窗口宽度计算出安全列数:宽度大于720时用4列,宽度在500到720之间用3列,小于500用2列。这个逻辑用一行三元表达式或者一个getter就能完成,不需要引入复杂的框架。用下来体验比较稳定,也方便在不同设备上快速验证效果。
希望这篇文章能把Grid固定行列的用法讲透,尤其是那些文档里没细说、实际又会反复踩的地方。如果你正在做HarmonyOS 6的ArkTS页面开发,遇到网格布局相关的问题,可以直接参考这里的方案去对号入座。
