做HarmonyOS NEXT开发这大半年,我用的最多的容器组件不是List,不是Flex,而是Grid。原因很简单——真机一跑,手机竖屏两列、横屏三列、折叠屏展开四列,这种响应式需求你拿Flex硬写试试?代码量能翻三倍。但Grid好用归好用,里面的坑是真的多,尤其是rowsTemplate和columnsTemplate同时设置导致不可滚动这个,我第一回踩的时候排查了整整一个下午。今天就把Grid布局相关的经验一股脑倒出来,包括fr单位的计算逻辑、跨行跨列的间距异常、响应式断点方案,以及Grid和List/WaterFlow/Flex的选型对比。

请添加图片描述

一、Grid的核心:columnsTemplate和rowsTemplate

Grid的灵魂就在两个属性——columnsTemplate定义列,rowsTemplate定义行。写法跟CSS Grid的grid-template-columns/grid-template-rows几乎一模一样,用空格分隔每一列(行)的尺寸。

最常用的就是fr单位,表示按比例分配剩余空间。比如1fr 1fr 1fr就是三等分,1fr 2fr就是1:2的比例。我在GridDemo里是这样写的:

private getColumnsTemplate(): string {
  if (this.currentColumns === 4) {
    return '1fr 1fr 1fr 1fr';
  } else if (this.currentColumns === 3) {
    return '1fr 1fr 1fr';
  }
  return '1fr 1fr';
}

三个档位,分别返回4列、3列、2列的模板字符串。简单粗暴,但够用。

fr单位的一个"隐蔽"计算细节

这里有个很多人没注意到的点:fr单位在计算可用宽度时,已经把gap扣掉了。什么意思?假设Grid总宽度360vp,columnsGap是8vp,两列,那不是每列(360-8)/2=176vp,而是框架先扣掉一个gap的8vp,剩余352vp再按fr比例分,每列176vp。看起来结果一样?三列就不一样了:两个gap共16vp,剩余344vp,每列约114.67vp。

这本来不是问题,但如果你给GridItem内部的子组件设了固定宽度,或者用百分比宽度的时候,就得留心这个差值。我之前在GridItem里放一个width('50%')的图片,结果在2列和3列下表现不一致,折腾半天才发现是fr计算宽度跟我预期有出入。结论:GridItem内部子组件用width('100%')最稳妥,让fr去控制列宽,内部自适应。

另外,fr之外你还能用固定值,比如100vp 1fr 1fr,第一列固定100vp,剩下两列平分。这种写法适合侧边栏+内容区的场景。但说实话,在移动端我几乎没遇到过需要固定列宽的需求,fr等分就够了。

二、onAreaChange:响应式断点的实现方案

HarmonyOS NEXT没有一个像Web端window.matchMedia那样的全局媒体查询API给组件用(@ohos.mediaquery倒是存在,但用法繁琐且坑多),我的做法是用onAreaChange监听容器宽度变化,手动切换断点。GridDemo里的核心代码:

@State currentColumns: number = 2;
@State currentBreakpoint: string = 'sm';

private updateBreakpoint(width: number): void {
  if (width >= 840) {
    this.currentColumns = 4;
    this.currentBreakpoint = 'lg';
  } else if (width >= 520) {
    this.currentColumns = 3;
    this.currentBreakpoint = 'md';
  } else {
    this.currentColumns = 2;
    this.currentBreakpoint = 'sm';
  }
}

然后在Grid上挂onAreaChange:

Grid() {
  ForEach(this.basicItems, (item: GridItemData) => {
    GridItem() {
      // ...卡片内容
    }
  }, (item: GridItemData) => item.id.toString())
}
.columnsTemplate(this.getColumnsTemplate())
.columnsGap(8)
.rowsGap(8)
.width('100%')
.onAreaChange((_oldArea: Area, newArea: Area) => {
  const w: number = Number(newArea.width);
  this.updateBreakpoint(w);
})

断点阈值我选的是520vp和840vp,这是HarmonyOS官方推荐的多设备断点值,对应关系如下:

断点 宽度范围 典型设备 列数
sm <520vp 手机竖屏 2列
md 520-840vp 手机横屏/小平板 3列
lg >840vp 平板/折叠屏展开 4列

为什么不用mediaquery?

试过。@ohos.mediaquery需要先mediaquery.matchMediaSync创建监听器,再注册回调,组件销毁时还得手动解绑。一个页面如果多处响应,每个都要创建一遍,代码又臭又长。而且它监听的是屏幕宽度,不是容器宽度——如果你的Grid不是全屏的(比如侧边栏+主内容区),屏幕宽度根本不是你想要的值。onAreaChange监听的是组件自身尺寸,精确且省事。

当然onAreaChange也有小问题:初次渲染时会触发一次,且频繁旋转设备可能短时间触发多次。我实际项目中会加个防抖(500ms足够),不过GridDemo是Demo就没加,生产代码别忘。

columnsTemplate动态切换的渲染表现

这里有个让我惊喜的点:columnsTemplate是状态驱动的,this.currentColumns一变,Grid会重新布局,而且是带动画过渡的。你旋转设备的时候,卡片会平滑地从2列变3列再变4列,不是生硬地跳变。这比手动计算每个item的margin/width去模拟响应式强太多了。

但要注意,ForEach的key如果设置不当(比如用index做key),切换列数时可能出现item闪烁或错位。务必用唯一id做key,像Demo里这样:(item: GridItemData) => item.id.toString()

三、跨行跨列:columnStart/columnEnd/rowStart/rowEnd

Grid最强大的能力之一就是让GridItem跨越多行多列。在GridDemo的"跨行跨列"区域,我手写了一个4列3行的网格,每个GridItem都用columnStart/columnEnd/rowStart/rowEnd精确定位:

Grid() {
  GridItem() {
    Column() {
      Text('1x1').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
    }
    .width('100%').height('100%').backgroundColor('#1a73e8')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(0).rowEnd(0).columnStart(0).columnEnd(0)   // 占1格

  GridItem() {
    Column() {
      Text('2x1').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
      Text('跨2列').fontSize(10).fontColor('#ffffffcc')
    }
    .width('100%').height('100%').backgroundColor('#e65100')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(0).rowEnd(0).columnStart(1).columnEnd(2)   // 跨2列

  GridItem() {
    Column() {
      Text('1x1').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
    }
    .width('100%').height('100%').backgroundColor('#34a853')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(0).rowEnd(0).columnStart(3).columnEnd(3)

  GridItem() {
    Column() {
      Text('1x2').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
      Text('跨2行').fontSize(10).fontColor('#ffffffcc')
    }
    .width('100%').height('100%').backgroundColor('#9c27b0')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(1).rowEnd(2).columnStart(0).columnEnd(0)   // 跨2行

  GridItem() {
    Column() {
      Text('1x1').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
    }
    .width('100%').height('100%').backgroundColor('#d32f2f')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(1).rowEnd(1).columnStart(1).columnEnd(1)

  GridItem() {
    Column() {
      Text('2x2').fontSize(13).fontColor('#ffffff').fontWeight(FontWeight.Medium)
      Text('跨2行2列').fontSize(10).fontColor('#ffffffcc')
    }
    .width('100%').height('100%').backgroundColor('#00897b')
    .borderRadius(8).justifyContent(FlexAlign.Center)
  }
  .rowStart(1).rowEnd(2).columnStart(2).columnEnd(3)   // 跨2行2列
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.width('100%')
.height(260)

columnStart和columnEnd是列索引,从0开始。columnStart(1).columnEnd(2)表示从第2列到第3列,跨2列。同理rowStart(1).rowEnd(2)跨2行。
请添加图片描述

跨行跨列的间距异常坑

这是我踩过最恶心的坑之一。当有跨行跨列的GridItem时,gap可能出现"消失"或"翻倍"的现象。具体来说,一个2x2的item跨过了中间的gap线,按理说这个位置的gap应该被item覆盖掉,但实际渲染中,有时候相邻item的间距会跟预期不一致,尤其是跨列item和非跨列item紧邻时。

我目前的workaround是:跨行跨列的GridItem内部自己加padding来"模拟"gap,而不是完全依赖columnsGap/rowsGap。比如在2x2的item里,Column加个padding(4),视觉上就和谐了。不优雅,但管用。

另外,跨行跨列的写法要求你必须手写每个GridItem的位置,不能用ForEach自动布局。这就意味着数据量大的场景根本没法用。如果你的需求是"大部分item 1x1,偶尔有个大item跨2列",建议用两个Grid叠加——一个放普通item,一个用绝对定位放大item,覆盖在对应位置上。虽然hacky,但至少能自动布局。

四、Grid滚动的那些坑

坑1:rowsTemplate和columnsTemplate同时设置=不可滚动

这是最关键的一条:当Grid同时设置了rowsTemplate和columnsTemplate时,Grid的行列数就固定了,不会自动扩展,因此不可滚动。 只有只设置columnsTemplate(不设rowsTemplate)时,行数才能根据内容自动增长,Grid才能纵向滚动。

我在GridDemo里专门设计了两个区域来对比:

  • "响应式Grid"区域:只设columnsTemplate,不设rowsTemplate,8个卡片自动排成多行,内容超出时可滚动。
  • "跨行跨列"区域:同时设了columnsTemplate('1fr 1fr 1fr 1fr')rowsTemplate('1fr 1fr 1fr'),Grid固定4列3行,不可滚动,需要给外层包Scroll。

``typescript
// 可滚动的Grid——只设columnsTemplate
Grid() {
ForEach(this.basicItems, …)
}
.columnsTemplate(this.getColumnsTemplate()) // 只设列模板
.columnsGap(8).rowsGap(8)

// 不可滚动的Grid——同时设了行和列
Grid() {
// 手动定位的GridItem
}
.columnsTemplate(‘1fr 1fr 1fr 1fr’) // 设了列模板
.rowsTemplate(‘1fr 1fr 1fr’) // 又设了行模板,Grid固定3行4列,不可滚动
.height(260) // 必须给固定高度,否则显示不出来
``

我在第一回用Grid的时候,照着CSS Grid的习惯,把rowsTemplate和columnsTemplate都设了,然后纳闷为什么内容超出不滚动……这跟CSS Grid的行为完全不同,Web端设了grid-template-rows只是定义了行高,内容多了照样往下排。HarmonyOS的Grid是:行模板=行数固定。这个设计逻辑我至今觉得反直觉,但就是这么规定的,只能适应。

坑2:GridItem固定高度阻止滚动

第二个坑:如果Grid只设了columnsTemplate(理论上可滚动),但GridItem都设了固定高度,且所有item的总高度不超过Grid自身高度,那Grid也不会滚动。 这听起来是正常行为,但实际开发中容易踩的情况是——你给Grid设了height(200),8个item每个80vp,2列4行,总高度480+38(rowsGap)=336vp > 200vp,能滚动。但如果你后来改了item高度为40vp,总高度440+38=184vp < 200vp,突然就不滚动了,而你可能还在纳闷"为什么最后几个item没出现"。

我的建议:需要滚动的Grid,不要给Grid设固定高度,用layoutWeight让它撑满剩余空间。 固定高度的Grid只用于"跨行跨列"那种确定行列数的静态网格。

坑3:滑动方向只有纵向

只设columnsTemplate的Grid只支持纵向滚动。想要横向滚动?你得只设rowsTemplate,不设columnsTemplate。但横向滚动的Grid在实际中极少使用(水平滑动用Swiper或者Scroll+Row更合适),所以这条知道就行。

五、cachedCount:性能优化利器

Grid的cachedCount属性控制预渲染的屏幕外item数量。默认值是0,意味着只在可见区域内渲染item。设成3,就是在可见区域上下各多渲染3行item。

GridDemo里的滑动Grid设置了cachedCount(3):

typescript Grid() { ForEach(Array.from({ length: 30 }, (_v: Object, i: number) => i), (i: number) => { GridItem() { Column() { Text('项 ' + (i + 1).toString()) .fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium) Text('数据内容') .fontSize(11).fontColor('#888888').margin({ top: 2 }) } .width('100%').height(80).backgroundColor('#f0f0f0') .borderRadius(8).padding(8) } }, (i: number) => i.toString()) } .columnsTemplate('1fr 1fr 1fr') .columnsGap(8).rowsGap(8) .cachedCount(3) .width('90%') .height(200)

30个item,3列排列就是10行。屏幕可见大概2-3行,cachedCount=3意味着实际渲染5-6行的item,其余的在滚动到附近时才创建。

cachedCount设多少合适? 我的经验是2-5之间。太低的话快速滑动时会出现白块(item还没渲染出来),太高的话内存占用上升。item内容越复杂(图片多、组件嵌套深),白块越容易出现,cachedCount就要适当调大。如果你的item只有几个Text,cachedCount=1都够。

还有一点要注意:cachedCount预渲染的item是创建但不可见的,它的生命周期跟可见item不同。如果你的item有onAppear/onDisappear回调(比如曝光打点),cachedCount的item不会触发onAppear,只有真正进入可见区域才会触发。这个行为是正确的,但第一次用的人可能以为onAppear没生效。

六、Grid vs List vs WaterFlow vs Flex

这个对比我写了不少于五遍,每次团队新人来都要讲一遍。干脆写在这里。

Grid——行列网格

Grid是唯一能同时控制行和列的容器。数据量大时自动滚动(只设columnsTemplate),需要跨行跨列时精确布局(同时设行和列模板)。适合:相册九宫格、设置页图标列表、商品网格、仪表盘。

核心优势:columnsTemplate动态切换就能实现响应式,一个Grid组件搞定多设备适配。List要实现同样效果得在外层套一堆判断。

List——单列/多列列表

List默认单列纵向滚动,通过lanes属性可以设多列,但lanes的控制粒度远不如columnsTemplate——你只能指定列数,不能控制列宽比例。List的强项是分组(ListItemGroup)、粘性头部、侧滑删除这些交互能力,Grid没有这些。

适合:聊天列表、新闻Feed、联系人列表、设置项列表。这些场景的共同特点是——单列为主,偶尔双列,不需要复杂的跨行跨列。

WaterFlow——瀑布流

WaterFlow是HarmonyOS NEXT新增的瀑布流组件,item高度可以不同,自动排到最短的那一列。这解决了Grid所有item必须等高的限制。

适合:商品展示、图片Pinterest流。item高度不固定的场景,WaterFlow是唯一选择。但WaterFlow不支持跨行跨列,也没有rowsTemplate/columnsTemplate这种精确控制能力。

我个人的选型偏好:如果item高度统一,优先Grid(响应式更方便);item高度参差不齐,用WaterFlow。List只在需要ListItemGroup等特殊交互时用。

Flex——弹性布局

Flex不是滚动容器!这是新人最容易犯的错。Flex是用来做一行/一列内的弹性排列的,比如水平居中、两端对齐、按比例分配空间。它不会自动换行(除非设了flexWrap(FlexWrap.Wrap)),也不会滚动。如果你的内容超出屏幕,Flex不会帮你处理,只会被截断或者撑爆布局。

适合:工具栏、标签栏、底部按钮排列、表单行内label+input。任何不需要滚动的局部布局都该用Flex,而不是Grid。Grid是"大容器",Flex是"小工具"。

七、完整Demo代码解析

GridDemo这个页面我分了两个Tab——“实战演示"和"对比指南”。实战演示里放了三个Grid变体,对比指南里放了对选型和属性的文字说明。

完整的GridDemo结构

``typescript
import { router } from ‘@kit.ArkUI’;

interface GridItemData {
id: number;
title: string;
subtitle: string;
color: string;
}

@Entry
@Component
struct GridDemo {
@State currentColumns: number = 2;
@State currentBreakpoint: string = ‘sm’;
@State activeTab: number = 0;

private basicItems: GridItemData[] = [
{ id: 0, title: ‘卡片A’, subtitle: ‘基础布局’, color: ‘#1a73e8’ },
{ id: 1, title: ‘卡片B’, subtitle: ‘等分列’, color: ‘#e65100’ },
{ id: 2, title: ‘卡片C’, subtitle: ‘响应式’, color: ‘#34a853’ },
{ id: 3, title: ‘卡片D’, subtitle: ‘自适应’, color: ‘#9c27b0’ },
{ id: 4, title: ‘卡片E’, subtitle: ‘断点切换’, color: ‘#d32f2f’ },
{ id: 5, title: ‘卡片F’, subtitle: ‘多设备’, color: ‘#00897b’ },
{ id: 6, title: ‘卡片G’, subtitle: ‘fr单位’, color: ‘#558b2f’ },
{ id: 7, title: ‘卡片H’, subtitle: ‘间距控制’, color: ‘#0277bd’ }
];
// … (其余代码见GridDemo.ets)
}
``

第一个Tab"实战演示"里,最上面的响应式Grid区域就是本文的核心——通过onAreaChange感知宽度,动态切换columnsTemplate。中间是跨行跨列的静态Grid,固定4列3行,用手动定位。最下面是带cachedCount的可滚动Grid,30个item纵向滚动。

第二个Tab"对比指南"是纯UI展示,包括Grid/List/WaterFlow/Flex的对比表格、Grid关键属性说明、响应式断点建议,以及常见坑的红色警告区。这些内容我在前文已经展开讲了,不再赘述。

响应式卡片区域的完整代码

这是Demo最核心的部分,我单独拎出来再过一遍:

typescript Column() { Grid() { ForEach(this.basicItems, (item: GridItemData) => { GridItem() { Column() { Text(item.title) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor('#ffffff') Text(item.subtitle) .fontSize(11) .fontColor('#ffffffcc') .margin({ top: 4 }) } .width('100%') .height(80) .borderRadius(8) .backgroundColor(item.color) .padding(12) .justifyContent(FlexAlign.Center) } }, (item: GridItemData) => item.id.toString()) } .columnsTemplate(this.getColumnsTemplate()) .columnsGap(8) .rowsGap(8) .width('100%') .onAreaChange((_oldArea: Area, newArea: Area) => { const w: number = Number(newArea.width); this.updateBreakpoint(w); }) } .width('90%') .padding(12) .borderRadius(8) .backgroundColor('#f8f8f8')

注意几个关键点:

  1. Grid外层包了一个灰色背景的Column,宽度90%,Grid本身width(‘100%’)填满这个Column。这样onAreaChange拿到的是实际内容区宽度,不是屏幕宽度。
  2. GridItem内部用Column+width(‘100%’)+height(80),高度固定80vp,宽度跟随fr分配。这样在不同列数下item高度一致,视觉整齐。
  3. 断点信息展示在Grid上方:'断点: ' + this.currentBreakpoint + ' | ' + this.currentColumns.toString() + '列',方便调试。

八、实战经验总结

写到这里,我把Grid相关的经验提炼成几条硬规则:

规则1:需要滚动的Grid只设columnsTemplate,绝不设rowsTemplate。 这是铁律,没有例外。如果你想固定行高,可以通过GridItem的height控制,而不是设rowsTemplate。

规则2:响应式断点用onAreaChange+@State驱动columnsTemplate。 不要用mediaquery,不要手动计算屏幕宽度。onAreaChange给你的是容器真实宽度,最精确。

规则3:GridItem内部宽度一律用100%,不要用固定值或百分比。 fr单位已经帮你算好了列宽,GridItem填满格子就行。想控制内部元素大小,在GridItem的子组件上设。

规则4:跨行跨列时gap可能异常,用padding补丁。 暂时没有完美的解决方案,等官方修复。在此之前,手动padding是唯一的出路。

规则5:cachedCount根据item复杂度设2-5。 简单item用2,复杂item(含图片、自定义组件)用5。别设太大,内存会涨。

规则6:跨行跨列的Grid只用于少量固定的item。 如果数据是动态的、数量大的,别想跨行跨列,老老实实用等分网格。

规则7:ForEach的key用唯一id,别用index。 响应式切换列数时,index做key会导致item闪烁和错位。这在任何列表组件里都是一样的道理,但Grid因为列数切换频繁,问题更明显。

九、一个容易被忽略的细节:columnsGap/rowsGap的取值

最后说一个不起眼但可能让你debug到怀疑人生的小问题。columnsGap和rowsGap的值是vp,不是px。这本身没问题,但如果你习惯性地写个整数比如8,在有些设备上看起来间距很小,有些设备上又看起来间距很大——因为不同设备的vp-to-px比例不同。

不过这个问题在HarmonyOS NEXT上其实不太严重,因为系统统一用vp做布局单位,视觉一致性比Android好得多。真正出问题的是你同时用了vp和px混搭的场景,比如Grid的gap用8(vp),但GridItem内部某个元素用了.margin(8)(也是vp,没问题)或者用了.width(100)(vp,也没问题)。只要全用vp,就不会有单位混乱的问题。

我的习惯是:所有布局相关的数值都用vp(ArkUI默认就是vp),只在需要精确像素控制的地方(比如画Canvas)才用px。 Grid的columnsGap、rowsGap、item的height、padding,一概用vp。

十、结语

Grid是HarmonyOS NEXT里我最喜欢的布局组件,没有之一。它的columnsTemplate动态切换方案,是目前我找到的实现响应式多设备适配最优雅的方式。虽然坑不少——rowsTemplate+columnsTemplate同时设就不可滚动、跨行跨列的gap异常、GridItem固定高度影响滚动——但只要掌握了这些规则,用起来比List+条件判断模拟多列要舒服得多。

如果你也在做HarmonyOS NEXT的多设备适配,我的建议是:把Grid作为你的主力网格容器,用onAreaChange做响应式,把List留给需要分组和侧滑的场景,把WaterFlow留给不等高的瀑布流场景,把Flex留给不需要滚动的局部布局。 各司其职,别让一个组件干所有活。

完整代码见项目中的GridDemo.ets,跑起来旋转设备就能看到断点切换效果。有问题欢迎讨论。

补充篇:生产项目中的Grid实战案例

光讲理论和Demo不够,说两个我在实际项目中用Grid遇到的真实场景和解决方案。

场景一:电商首页的频道入口区

电商首页顶部一般有一排圆形图标入口——充值、充值卡、优惠券、会员中心之类的,通常8-10个,手机竖屏两行4列,横屏一行8列,折叠屏展开可能一行10列。这种场景Grid简直是量身定做的。

我最初用Flex+Wrap实现,结果发现:FlexWrap.Wrap虽然能自动换行,但每一行的高度是按行内最高元素算的,圆形图标行和文字行高度不同,换行后间距不均匀。换成Grid后,columnsTemplate精确控制列数,rowsGap统一行间距,所有格子等高等宽,视觉上整齐得多。

但这个场景有个坑:频道入口的数据是运营后台配置的,数量不固定,可能8个也可能12个。如果用固定列数的columnsTemplate,8个入口2行4列正好,12个入口就变3行了。我的做法是根据数据量动态计算列数——如果入口数能被4整除就用4列,能被3整除就用3列,否则取最小公约数。代码大概是:

typescript private calcColumns(count: number, maxCols: number): number { for (let i = maxCols; i >= 2; i--) { if (count % i === 0) return i; } return 2; }

这个函数确保Grid不会出现最后一行只有一个item的情况——那种半行空着的效果在视觉上很难看。如果数据量实在没法整除,就在最后补一个"更多"入口占位。

另外,频道入口区的item高度不能太高(否则首屏被入口区占满),我一般设60-70vp,图标40vp+文字12vp+间距8vp刚好。这个高度在2列和4列下都合适,不需要响应式调整。

场景二:设置页的开关项列表

设置页的经典布局:左边图标+中间标题描述+右边开关。这种布局很多人用List做,我一开始也是。但遇到的需求是——某些设置项要"跨列"显示,比如"智能省电"这个大开关要占满整行,而其他开关是两列并排的。

用List?List的lanes只能统一设列数,没法让某一项跨列。用Flex?数据量超过10个就得滚动,Flex不支持滚动。用Grid?可以,但rowsTemplate和columnsTemplate同时设就不能滚动了……

最终我的方案是Grid只设columnsTemplate,跨列的item用columnStart/columnEnd控制。等等,前面不是说只设columnsTemplate时不能用columnStart/columnEnd吗?这里有个微妙之处:只设columnsTemplate的Grid,columnStart/columnEnd仍然可以设置,只是效果跟同时设rowsTemplate时不同。在可滚动Grid中,columnStart/columnEnd会让item跨指定列数的格子,但rowStart/rowEnd不生效(因为行数是动态的)。

实际代码中,普通设置项不设columnStart/columnEnd,自动占1列;跨全行的设置项设.columnStart(0).columnEnd(this.currentColumns - 1),横跨所有列。这样就实现了"大部分两列,偶尔全行"的混合布局,而且Grid依然可以滚动。

这个技巧我搜遍了官方文档和社区都没找到明确的说明,是自己反复试验发现的。官方文档对columnStart/columnEnd的描述是"仅在rowsTemplate和columnsTemplate同时设置时生效",但实际在只设columnsTemplate时,columnStart/columnEnd对跨列是生效的,只是跨行(rowStart/rowEnd)不生效。这个行为属于"能用但没文档"的灰色地带,生产代码慎用,我目前只在设置页这种低频页面用了。

场景三:图片选择器的九宫格

相册选择器是最经典的Grid场景——3列或4列等高网格,每个格子一张图片,点击选中加勾号。这个用Grid做最自然,而且有个额外好处:Grid的columnsGap/rowsGap形成的网格线,正好是图片之间的分隔线,不需要额外画border。

我在做图片选择器时遇到的坑是:图片加载用LazyForEach,而Grid的LazyForEach跟List的LazyForEach行为不完全一样。List的LazyForEach在item滑出视口后可能会被回收(取决于cachedCount),但Grid的LazyForEach回收策略更激进——如果columnsGap或rowsGap设得很大,导致可见区域能容纳的item变少,cachedCount的"上下N行"计算可能不准确,出现快速滑动时的白块。我的解决方案是把gap控制在4-8vp,不要设太大。

这段代码的坑在.height('100%')——如果Grid同时设了rowsTemplate,height(‘100%’)表示填满格子高度,没问题。但如果Grid只设columnsTemplate,格子高度由GridItem内容撑开,Image的height(‘100%’)会变成0(因为父容器没有明确高度)。解决方案:给GridItem设固定高度,比如.height(120),Image用height(‘100%’)填满。 这个坑我在GridDemo的响应式区域也踩过,最终给每个卡片设了.height(80)
把Grid封装成ResponsiveGrid自定义组件,接收columns参数,内部不自己监听宽度。这样一个页面只有一个onAreaChange,4个Grid同时响应状态变化,布局效率高得多。

这个封装思路我强烈推荐——把"宽度监听+断点计算"和"Grid布局渲染"分离,前者只做一次,后者纯展示。这也是为什么GridDemo里updateBreakpoint和getColumnsTemplate是两个独立方法的原因,虽然在Demo里它们在同一个组件里,但在真实项目中,断点计算应该上提到页面级或全局级,Grid只负责根据传入的列数渲染。

请添加图片描述

Logo

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

更多推荐