HarmonyOS APP《画伴梦工厂》开发第37篇-GridRow-GridCol——响应式网格布局

第5.3篇:GridRow/GridCol------响应式网格布局

系列 :HarmonyOS 从入门到实践 · 画伴梦工厂实战

难度 :⭐⭐ 进阶

前置知识 :5.1 鸿蒙断点系统原理

涉及源文件features/responsiveLayout/src/main/ets/view/GridComponent.etsfeatures/responsiveLayout/src/main/ets/constants/CommonConstants.ets


在多设备时代,同一个应用需要在手机、平板、折叠屏和桌面端都能提供良好的视觉体验。传统的固定像素布局在屏幕尺寸变化时往往需要大量适配工作,而 HarmonyOS 提供了一整套响应式网格布局 方案------Grid + GridItem 组件配合断点系统,让布局自适应成为一件优雅的事。

本文将以"画伴梦工厂"项目中 responsiveLayout 特性模块为例,拆解如何用 Grid 容器实现从手机(2列)到平板(3列)再到桌面(4列)的自适应网格。

正式 API 命名中,GridGridItem 是鸿蒙网格布局的核心组件,而 GridRow/GridCol 是更上层的 12 列栅格系统。本文侧重点在 Grid + GridItem 组合,两种方案原理相通,可互为参考。


一、Grid + GridItem:鸿蒙网格容器基础

Grid 是 ArkUI 中用于创建网格布局的容器组件,GridItem 是其子项。与传统的 Row/Column 线性布局不同,Grid 允许元素沿水平和垂直两个方向排列,天然适合展示卡片列表、相册、商品货架等场景。

项目中的 Grid 声明

typescript 复制代码
// GridComponent.ets - 核心网格组件
@Component
export struct GridComponent {
  @StorageProp('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;
  @State colTemplate: string = commonConstants.initializeTemplate;
  private readonly data: number[] = [];

  build() {
    Grid() {
      ForEach(this.data, () => {
        GridItem() {
          // 每个网格项包含一个色块 + 文本标签
          Column() {
            Row()
              .width('100%')
              .aspectRatio(commonConstants.columnAspectRatio)
              .backgroundColor($r('sys.color.ohos_id_color_component_normal'))
            Row() {
              Text($r('app.string.grid_title_label'))
                .fontSize($r('app.float.grid_text_font_size'))
                .fontWeight(commonConstants.columnTextFontWeight)
            }
          }
        }
      })
    }
    .columnsTemplate(this.colTemplate)
    .columnsGap($r('app.float.grid_column_gap'))
    .rowsGap($r('app.float.grid_row_gap'))
  }
}

这段代码展示了最基础的 Grid 用法:

要素 说明
Grid 容器组件,负责整体网格布局
GridItem 子项组件,内部可嵌套任意内容
ForEach 循环渲染数据源,动态生成 GridItem
columnsTemplate 定义列数及每列宽度比例
columnsGap / rowsGap 控制列间距和行间距

Grid vs Row/Column 的选择

  • Row/Column:适合线性排列,一维布局
  • Grid:适合二维网格,子项等宽等高排列,自动换行
  • GridRow/GridCol:适合 12 列栅格系统,子项可跨列

项目使用 Grid 而非 GridRow,是因为卡片列表需要的是等宽等高的规则网格,而非自由跨列的复杂栅格。


二、columnsTemplate:用 fr 单位构建弹性列

columnsTemplate 是 Grid 最核心的属性,它定义了网格的列数每列宽度比例 。其语法类似 CSS Grid 的 grid-template-columns

fr 弹性单位

typescript 复制代码
// CommonConstants.ets - 各断点下的列模板
smColTemplate: '1fr 1fr',        // 手机:2列
mdColTemplate: '1fr 1fr 1fr',    // 平板:3列
lgColTemplate: '1fr 1fr 1fr 1fr', // 桌面:4列
xlColTemplate: '1fr 1fr 1fr 1fr', // 大桌面:4列

fr(fraction)是弹性比例单位,表示将可用空间按照指定份数分配:

  • '1fr 1fr':可用宽度分成 2 份,每列占 1 份(各 50%),即 2 列
  • '1fr 1fr 1fr':分成 3 份,每列占 1 份(各 33.33%),即 3 列
  • '1fr 1fr 1fr 1fr':分成 4 份,每列占 1 份(各 25%),即 4 列

混合单位对比

fr 可以与其他单位混合使用:

typescript 复制代码
// 混合写法示例(项目中未使用,但值得了解)
columnsTemplate: '200vp 1fr 1fr'  // 第一列固定200,后两列平分剩余空间
columnsTemplate: '1fr 2fr'         // 第二列宽度是第一列的2倍
columnsTemplate: 'repeat(3, 1fr)'  // 等价于 '1fr 1fr 1fr'

项目中采用清一色的 1fr 来构建均分列宽的网格,所有卡片等宽排列,视觉上整齐统一。

注意:鸿蒙的 columnsTemplate 目前不支持 repeat() 函数,需要手动写全每个 fr 值。

GridItem 的自动放置

当 GridItem 的数量超过列数 × 行数时,Grid 会自动折行。8 个数据项在 2 列模式下显示为 4 行,在 4 列模式下显示为 2 行------无需任何额外布局代码,这就是网格布局的便利之处。


三、断点驱动:aboutToAppear 中的模板初始化

Grid 的列数需要根据当前设备的断点(breakpoint)动态决定。项目在 aboutToAppear 生命周期中完成初始化。

生命周期内初始化

typescript 复制代码
aboutToAppear() {
  // 初始化数据源:生成 0~7 共 8 个索引
  for (let i = 0; i < commonConstants.gridSize; i++) {
    this.data.push(i);
  }
  // 根据当前断点选择初始列模板
  switch (this.currentBreakpoint) {
    case commonConstants.breakpointsSmName:    // 'sm'
      this.colTemplate = commonConstants.smColTemplate;  // '1fr 1fr'
      break;
    case commonConstants.breakpointsMdName:    // 'md'
      this.colTemplate = commonConstants.mdColTemplate;  // '1fr 1fr 1fr'
      break;
    case commonConstants.breakpointsLgName:    // 'lg'
      this.colTemplate = commonConstants.lgColTemplate;  // '1fr 1fr 1fr 1fr'
      break;
    case commonConstants.breakpointsXlName:    // 'xl'
      this.colTemplate = commonConstants.xlColTemplate;  // '1fr 1fr 1fr 1fr'
      break;
    default:
      this.colTemplate = commonConstants.smColTemplate;  // 默认 2 列
      break;
  }
}

这段逻辑的核心思路:

  1. 读取断点 :从 @StorageProp('mainBreakpoint') 获取当前设备断点名称
  2. switch 分发 :根据 sm/md/lg/xl 选择对应的列模板字符串
  3. 赋值 @State :将模板字符串赋给 @State colTemplate,触发 UI 渲染

@StorageProp 是 HarmonyOS 的全局存储装饰器,它与 AppStorage 中的 'mainBreakpoint' 键绑定。当断点系统检测到屏幕尺寸变化并更新 AppStorage 中的值时,所有通过 @StorageProp 订阅该键的组件都会自动收到新值。

数据源初始化与 gridSize

typescript 复制代码
// CommonConstants.ets
gridSize: 8,          // 固定 8 个网格项

gridSize 设置为 8,是一个精心选择的值:

  • 2 列模式下:4 行,刚好铺满一屏
  • 3 列模式下:3 行(最后一行 2 个),视觉均衡
  • 4 列模式下:2 行,内容紧凑

如果数据量更大的场景,可以配合 List + LazyForEach 实现虚拟列表加载。


四、onAreaChange + BreakPointType:运行时动态换模板

仅仅在 aboutToAppear 中初始化还不够------当用户旋转设备或从平板模式切换到桌面模式时,列数需要实时变化 。项目通过 Grid 的 onAreaChange 事件实现了这一能力。

事件监听与模板更替

typescript 复制代码
Grid() {
  // ... grid items
}
.columnsTemplate(this.colTemplate)
.onAreaChange(() => {
  // 每次网格区域尺寸变化时重新计算列模板
  this.colTemplate = new BreakPointType({
    sm: commonConstants.smColTemplate,   // '1fr 1fr'
    md: commonConstants.mdColTemplate,   // '1fr 1fr 1fr'
    lg: commonConstants.lgColTemplate,   // '1fr 1fr 1fr 1fr'
    xl: commonConstants.xlColTemplate    // '1fr 1fr 1fr 1fr'
  }).getValue(this.currentBreakpoint);
})

执行流程

复制代码
设备旋转 / 窗口缩放
        │
        ▼
  onAreaChange 触发
        │
        ▼
  new BreakPointType({...})
    .getValue(this.currentBreakpoint)  ← 读取当前断点
        │
        ▼
  返回对应的列模板字符串
        │
        ▼
  this.colTemplate = '1fr 1fr 1fr'    ← 更新 @State
        │
        ▼
  Grid 组件重新渲染,列数变化

BreakPointType 的角色

BreakPointType<T> 是 HarmonyOS 提供的一个工具泛型,它将一个对象映射为"根据断点名称获取对应值"的能力:

typescript 复制代码
// BreakPointType 的本质
class BreakPointType<T> {
  sm?: T;
  md?: T;
  lg?: T;
  xl?: T;

  getValue(currentBreakpoint: string): T {
    // 根据 currentBreakpoint 返回对应的值
  }
}

在项目中,BreakPointType 被反复用于各种断点差异化配置------不仅限于列模板,还用于 Tab 方向、间距、内边距等。这种模式让断点相关的逻辑高度集中、易于维护。

为什么不直接用 @StorageProp 的监听?

有人可能会问:既然 currentBreakpoint 是响应式的(@StorageProp),为什么不直接监听它的变化来自动更新 colTemplate

原因在于:Grid 的重新布局需要触发 onAreaChange 事件来获得正确的区域尺寸 。单纯更新 colTemplate 变量虽然会触发 UI 重绘,但 onAreaChange 提供了额外的一层保障------确保 Grid 容器在尺寸变化稳定后才更新模板,避免在动画帧过程中频繁切换列数导致的闪烁。


五、columnsGap / rowsGap:间距控制与视觉节奏

间距是网格布局中影响视觉品质的关键因素。项目中通过资源文件统一管理间距值:

typescript 复制代码
Grid()
  .columnsGap($r('app.float.grid_column_gap'))
  .rowsGap($r('app.float.grid_row_gap'))

列间距 vs 行间距

属性 作用 典型值
columnsGap 水平方向相邻列的间距 grid_column_gap(资源文件定义)
rowsGap 垂直方向相邻行的间距 grid_row_gap(资源文件定义)

通过资源引用($r())而非硬编码数值,间距值可以在不同设备上差异化配置------手机端间距小、桌面端间距大,进一步提升响应式体验。

间距与 fr 的配合

间距会在 fr 分配之前从可用宽度中扣除。例如:

复制代码
总宽度 = 1000vp
columnsGap = 16vp
3 列 → 两列之间各 16vp 间距 → 有效宽度 = 1000 - 16×2 = 968vp
每列宽度 = 968 ÷ 3 ≈ 322.67vp

这个计算由框架自动完成,开发者只需声明列数和间距。


六、aspectRatio:响应式卡片比例控制

为了让网格卡片在列宽变化时保持合理的视觉比例,项目使用了 aspectRatio 属性:

typescript 复制代码
// CommonConstants.ets
columnAspectRatio: 16 / 9,

// GridComponent.ets
Row()
  .width('100%')
  .aspectRatio(commonConstants.columnAspectRatio)  // 宽高比 16:9

aspectRatio 的工作原理

aspectRatio 定义组件的宽高比(width / height)。当宽度固定时,高度自动计算:

复制代码
aspectRatio = 16 / 9 ≈ 1.778
width = 100%(父容器宽度)
height = width / 1.778

在不同列数下,卡片宽度不同,但比例一致:

断点 列数 卡片宽度(估算) 卡片高度(16:9)
sm 2 约 50% 容器宽度 约 28%
md 3 约 33% 容器宽度 约 18.8%
lg/xl 4 约 25% 容器宽度 约 14%

这样就实现了比例恒定、大小自适应的卡片效果------无论屏幕多宽,卡片始终是规整的 16:9 矩形。

常用宽高比参考

比值 适用场景
1 / 1 正方形头像、正方形卡片
16 / 9 视频缩略图、横屏内容
4 / 3 照片、传统显示比例
3 / 4 竖版卡片、人物照片
2 / 3 商品列表、竖版内容

七、响应式列数策略:2→3→4 的断点设计

项目的网格列数策略可以总结为一张表:

复制代码
┌────────┬────────────┬──────────┬──────────────┐
│  断点   │  设备典型   │  列数    │  columnsTemplate │
├────────┼────────────┼──────────┼──────────────┤
│  sm    │  手机竖屏   │   2列    │  '1fr 1fr'       │
│  md    │  平板竖屏   │   3列    │  '1fr 1fr 1fr'   │
│  lg    │  桌面 / 横屏 │   4列    │  '1fr 1fr 1fr 1fr' │
│  xl    │  大屏桌面   │   4列    │  '1fr 1fr 1fr 1fr' │
└────────┴────────────┴──────────┴──────────────┘

设计考量

  1. 手机(sm)2 列:手机屏幕宽度有限,2 列保证了卡片有足够大的点击区域和可视化面积,避免内容过于拥挤。

  2. 平板(md)3 列:平板屏幕宽度约 600~800vp,3 列能在不牺牲卡片可读性的前提下展示更多内容。

  3. 桌面(lg/xl)4 列:桌面宽度充足,4 列充分利用宽屏空间。lg 和 xl 复用同一模板,因为再增加列数会导致卡片过窄,反而降低可读性。

这种"阶梯式"列数增长是一种经过验证的响应式策略------在保证内容可读性的前提下,用最少的断点覆盖最多的设备。

与其他响应式策略对比

策略 特点 适用场景
阶梯式列数(本项目) 断点切换时列数跳跃变化 内容型卡片、媒体展示
流体网格 列数固定,列宽随容器伸缩 文本为主的布局
混合网格 部分列固定、部分列弹性 带侧边栏的复杂布局
12列栅格系统 子项自由跨列组合 页面级大布局

八、@StorageProp + 断点系统:全局响应式联动

GridComponent 通过 @StorageProp 与全局断点系统建立连接,这是整个响应式机制的数据基础。

数据流链路

复制代码
BreakpointSystem(监听屏幕尺寸变化)
       │
       ▼
  AppStorage.set('mainBreakpoint', 'sm'|'md'|'lg'|'xl')
       │
       ▼
  @StorageProp('mainBreakpoint') currentBreakpoint
       │
       ▼
  GridComponent 内部消费 currentBreakpoint
       │
       ├── aboutToAppear → switch → colTemplate 初始值
       └── onAreaChange → BreakPointType.getValue → colTemplate 更新

@StorageProp 详解

typescript 复制代码
@StorageProp('mainBreakpoint') currentBreakpoint: string
  • @StorageProp :从 AppStorage 中读取属性,并建立单向绑定。当 AppStorage'mainBreakpoint' 的值变化时,currentBreakpoint 自动更新。
  • 单向绑定 :组件可以读取和消费该值,但不能修改它------断点值的修改权在 BreakpointSystem 中,保证了数据流的单向性。
  • 初始化值commonConstants.breakpointsInitializeName 值为 'md',在断点系统尚未完成初始化时作为保底值。
复制代码
┌─────────────┬─────────────────────┬────────────────────┐
│   装饰器     │       方向          │        用途         │
├─────────────┼─────────────────────┼────────────────────┤
│ @StorageProp │ AppStorage → 组件    │  只读消费全局状态    │
│ @StorageLink │ AppStorage ↔ 组件    │  双向读写全局状态    │
└─────────────┴─────────────────────┴────────────────────┘

在 ResponsiveLayout.ets(父页面)中使用了 @StorageLink,因为它需要主动修改当前索引等状态;而在 GridComponent 中只需读取断点值,因此使用 @StorageProp------语义更清晰、权限更受限。


九、与 CSS Grid 的对比(开发经验迁移)

对于有 Web 开发背景的读者,理解鸿蒙 Grid 与 CSS Grid 的异同可以加速上手:

维度 CSS Grid(Web) HarmonyOS Grid(ArkUI)
容器 display: grid <Grid> 组件
子项 直接子元素 <GridItem> 包裹
列定义 grid-template-columns: 1fr 1fr 1fr .columnsTemplate('1fr 1fr 1fr')
行定义 grid-template-rows .rowsTemplate()
间距 gap / row-gap / column-gap .columnsGap() / .rowsGap()
跨列 grid-column: span 2 需手动调整 GridItem 位置
自动折行 auto-fill / auto-fit 自动折行,无需额外属性
响应式 @media (min-width: 768px) BreakPointType + onAreaChange
fr 单位 fr(CSS spec) fr(ArkUI 属性值字符串)
repeat() 支持 repeat(3, 1fr) 暂不支持,需手动展开

鸿蒙 Grid 的独特优势

  1. 断点系统原生集成BreakPointType + @StorageProp 是框架级别的能力,无需额外引入媒体查询库
  2. 性能优化 :GridItem 配合 LazyForEach 支持按需加载,长列表场景下性能优于 Web 端手动实现
  3. 类型安全:模板字符串在编译时检查,避免 CSS 中拼写错误导致的静默失败
  4. 资源响应式$r('app.float.xxx') 资源引用可与断点联动,间距、字号等同步适配

十、完整布局结构图

将本文涉及的所有知识点整合为 GridComponent 的整体布局架构:

复制代码
┌─────────────────────────────────────────────────┐
│  Grid                                            │
│  columnsTemplate={colTemplate}                    │
│  columnsGap / rowsGap                             │
│                                                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │ GridItem  │  │ GridItem  │  │ GridItem  │        │
│  │ ┌──────┐  │  │ ┌──────┐  │  │ ┌──────┐  │        │
│  │ │ Row  │  │  │ │ Row  │  │  │ │ Row  │  │        │
│  │ │16:9  │  │  │ │16:9  │  │  │ │16:9  │  │        │
│  │ └──────┘  │  │ └──────┘  │  │ └──────┘  │        │
│  │  Text     │  │  Text     │  │  Text     │        │
│  └──────────┘  └──────────┘  └──────────┘        │
│                                                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │ GridItem  │  │ GridItem  │  │ GridItem  │        │
│  │  ...      │  │  ...      │  │  ...      │        │
│  └──────────┘  └──────────┘  └──────────┘        │
│                                                   │
│  ← onAreaChange → BreakPointType → colTemplate → │
└─────────────────────────────────────────────────┘

总结

本文通过"画伴梦工厂"中的 GridComponent,完整拆解了 HarmonyOS 响应式网格布局的实现方案:

知识点 核心实现
Grid + GridItem 容器 二维网格布局,自动折行排列子项
columnsTemplate + fr 单位 定义列数和弹性比例,'1fr 1fr' 表示 2 列等宽
断点初始化 aboutToAppear + switch 根据 currentBreakpoint 设置初始模板
动态换模板 onAreaChange + BreakPointType 实时响应尺寸变化
间距控制 columnsGap / rowsGap 统一管理水平和垂直间距
卡片比例 aspectRatio(16/9) 保证卡片比例稳定
列数策略 2 列(手机)→ 3 列(平板)→ 4 列(桌面)阶梯式递增
全局响应式联动 @StorageProp('mainBreakpoint') 连接断点系统与 Grid 组件

响应式网格布局的精髓在于:用最少的规则应对最多的设备。通过断点、模板、比例三要素的组合,一套 Grid 代码即可完美适配手机、平板和桌面------这正是 HarmonyOS 响应式设计框架的核心价值所在。


参考源码

本文所有代码均来自项目文件:

  • features/responsiveLayout/src/main/ets/view/GridComponent.ets --- 核心 Grid 网格组件,包含初始化、断点切换、模板更新完整逻辑
  • features/responsiveLayout/src/main/ets/constants/CommonConstants.ets --- 断点名称、列模板、间距等所有常量配置
  • features/responsiveLayout/src/main/ets/pages/ResponsiveLayout.ets --- 父页面,展示 BreakPointType 在 Tab 布局中的更多应用
相关推荐
痕忆丶1 小时前
openharmony开发基础之5.0.1版本文件管理器复制粘贴框架调用流程
harmonyos
国服第二切图仔2 小时前
HarmonyOS APP《画伴梦工厂》开发第31篇-语音识别实战——SpeechRecognitionEngine+AudioCapturer
语音识别·xcode·harmonyos
TrisighT4 小时前
Electron 鸿蒙 PC 上点外链唤醒应用,我试了 6 种写法只有 1 种能跑
前端·electron·harmonyos
TrisighT5 小时前
Electron 跑鸿蒙 PC 上,这 4 个 API 的行为跟 Windows 完全不一样——但文档一行都没写
windows·electron·harmonyos
蓝速科技7 小时前
蓝速科技 RISC-V 鸿蒙信创工控终端深度评测
科技·harmonyos·risc-v
TrisighT1 天前
DevEco Code 写鸿蒙 ArkTS 确实快,但我试了三天后把默认引擎换成了 Cursor
ai编程·harmonyos·cursor
liz7up1 天前
鸿蒙原生流程图 & 审批流组件 hmflowkit
harmonyos
网易云信2 天前
全框架覆盖!网易智企IM鸿蒙生态适配再进一步
人工智能·aigc·harmonyos
TrisighT2 天前
我用 AI 逆向了 ArkTS @Builder 的编译产物,看完再也不敢乱写嵌套了
ai编程·harmonyos·arkts