网格布局是移动端最经典的排版方式之一。从照片墙到应用抽屉,从商品列表到图标菜单,等分网格承载了大量信息展示任务。HarmonyOS NEXT ArkUI 提供了 Grid 组件,通过 columnsTemplate 属性灵活定义列数与列宽,配合 GridItem 渲染子元素,轻松实现各种网格效果。

本文将深入讲解 Grid 组件的核心 API,并通过一个"动态照片画廊"实战案例,展示列数切换、选中高亮和多选管理等功能。

关键词:HarmonyOS、ArkUI、Grid、网格布局、照片画廊、动态列数

一、Grid 组件概览

Grid 是 ArkUI 容器组件家族中专门用于网格布局的成员。与 List(线性列表)、WaterFlow(瀑布流)和 Scroll(自由滚动)相比,Grid 的核心特征在于等分列——所有子元素按固定的列数排列,行高一致,视觉整齐统一。

组件 排列方式 典型场景
List 单列线性 消息列表、设置选项
Grid 多列等分 照片墙、应用抽屉、图标菜单
WaterFlow 多列不等高 图片流、商品发现
Scroll 自由排布 内容页面

Grid 的优势在于"规整"——当你的内容需要以 N 列的方式整齐排列时,Grid 是最合适的选择。

二、Grid 核心 API

2.1 构造函数与列模板

Grid 的列定义通过 columnsTemplate 属性控制,使用类似 CSS Grid 的语法:

Grid() {
  ForEach(this.items, (item) => {
    GridItem() { /* 内容 */ }
  })
}
.columnsTemplate('1fr 1fr 1fr')  // 3 等分列

columnsTemplate 支持以下格式:

  • '1fr 1fr' — 2 等分列(每列占 50% 宽度)
  • '1fr 1fr 1fr' — 3 等分列(每列占 33.3% 宽度)
  • '1fr 1fr 1fr 1fr' — 4 等分列(每列占 25% 宽度)
  • '120vp 1fr' — 混合模式(第一列固定 120vp,第二列占剩余宽度)

frfraction unit(分数单位)的缩写,1fr 表示"占 1 份可用空间"。如果有 3 个 1fr,则每列各占 1/3 宽度。

2.2 间距控制

Grid() { ... }
  .columnsGap(8)  // 列间距
  .rowsGap(8)     // 行间距

列间距和行间距独立控制,通常设为相同值以保持视觉统一。

2.3 动态构建列模板

在实际项目中,列数往往需要根据用户偏好动态调整。由于 columnsTemplate 接受字符串,我们可以动态构建:

buildTemplate(cols: number): string {
  let t = '1fr';
  for (let i = 1; i < cols; i++) {
    t = t.concat(' 1fr');
  }
  return t;
}

// 使用:
Grid() { ... }
  .columnsTemplate(this.buildTemplate(this.colCount))

colCount 变化时,columnsTemplate 字符串更新,Grid 自动重新排版。

2.4 GridItem

每个网格单元必须包裹在 GridItem() 中:

ForEach(this.photos, (photo: PhotoItem) => {
  GridItem() {
    Column() {
      Text(photo.emoji).fontSize(32)
      Text(photo.label).fontSize(13)
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(photo.color)
    .borderRadius(12)
  }
})

GridItem 提供了网格布局所需的尺寸测量能力,确保每列的子元素对齐。
在这里插入图片描述

三、实战:动态照片画廊

我们构建一个照片画廊,包含 18 张主题卡片(日出、海洋、森林、极光…),支持三种列数切换(2/3/4 列)和多选管理。

3.1 数据模型

每张"照片"用一个包含主题名、emoji 图标、背景色和文字色的对象表示:

interface PhotoItem {
  id: number;
  label: string;    // 主题名
  emoji: string;    // emoji 图标
  color: string;    // 背景色
  textColor: string; // 文字颜色(深色背景用白,浅色背景用黑)
}

aboutToAppear(): void {
  this.photos = [
    { id: 0,  label: '日出', emoji: '🌅', color: '#FF6B6B', textColor: '#FFFFFF' },
    { id: 1,  label: '海洋', emoji: '🌊', color: '#4ECDC4', textColor: '#FFFFFF' },
    { id: 2,  label: '森林', emoji: '🌲', color: '#2ECC71', textColor: '#FFFFFF' },
    { id: 3,  label: '晚霞', emoji: '🌇', color: '#FF8C42', textColor: '#FFFFFF' },
    { id: 4,  label: '星空', emoji: '✨', color: '#6C5CE7', textColor: '#FFFFFF' },
    // ... 共 18 张
  ];
}

每张卡片使用独立的颜色方案(18 色),确保网格在任何列数下都呈现出丰富的色彩对比。

3.2 列数切换

三个触摸按钮——2 列、3 列、4 列——点击时切换 colCount 并清除当前选择:

ForEach([2, 3, 4], (n: number) => {
  Text(n.toString().concat('列'))
    .fontSize(13)
    .fontColor(this.colCount === n ? '#FFFFFF' : '#666677')
    .fontWeight(this.colCount === n ? FontWeight.Bold : FontWeight.Normal)
    .padding({ top: 6, bottom: 6, left: 12, right: 12 })
    .borderRadius(12)
    .backgroundColor(this.colCount === n ? '#1677FF' : '#F2F3F5')
    .margin({ left: 6 })
    .onClick(() => {
      this.colCount = n;
      this.clearSelection();  // 切换列数时重置选择
    })
}, (n: number) => n.toString())

切换列数时清除选中状态是一个重要的 UX 细节——因为列数变化会导致卡片位置重新排列,保留旧选中状态会让用户困惑。

3.3 选中交互

点击卡片触发选中/取消,选中的卡片显示白色边框和扩展阴影:

toggleSelect(id: number): void {
  const found = this.selectedIds.indexOf(id);
  if (found === -1) {
    this.selectedIds.push(id);
  } else {
    this.selectedIds.splice(found, 1);
  }
}

每张卡片根据是否被选中动态调整样式:

.border({
  width: this.selectedIds.indexOf(photo.id) !== -1 ? 3 : 0,
  color: '#FFFFFF',
  style: BorderStyle.Solid
})
.shadow(
  this.selectedIds.indexOf(photo.id) !== -1
    ? { radius: 12, color: photo.color.concat('88'), offsetX: 0, offsetY: 4 }
    : { radius: 4, color: '#00000012', offsetX: 0, offsetY: 2 }
)

选中状态的双重视觉反馈:

  1. 白色边框(3vp 宽):清晰标识哪些卡片被选中
  2. 彩色扩展阴影(12vp 半径):使用卡片自身的颜色 + 半透明(88),产生"发光"效果

非选中状态的阴影则较轻(4vp 半径、浅黑半透明),保持卡片的层次感但不抢夺注意力。

3.4 选择计数与清除

在列数切换器右侧显示已选数量和清除入口:

if (this.selectedIds.length > 0) {
  Row() {
    Text('已选 '.concat(this.selectedIds.length.toString()))
      .fontSize(12).fontColor('#1677FF').fontWeight(FontWeight.Medium)
    Text(' | 清除')
      .fontSize(12).fontColor('#FF4D4F')
      .onClick(() => { this.clearSelection(); })
  }
}

这个信息区域仅在选中至少 1 张卡片后才显示,保持界面的简洁。清除按钮使用红色(#FF4D4F)暗示其"清空"的操作性质。
在这里插入图片描述

四、Grid vs List vs WaterFlow

选择哪个布局容器取决于内容特征:

特征 Grid List WaterFlow
内容高度 统一 可变 可变
列数 自定义 1 自定义
列对齐 强制对齐 紧凑排列
适用 图标菜单、相册 消息、设置 图片流

如果你的内容卡片的宽高比一致(如本例中的 18 张主题卡片),Grid 是天然的选择。如果内容高度差异大(如不同长度的文字卡片),WaterFlow 更合适。如果只需要单列,List 最简单。

五、完整交互流程

  1. 初始状态:进入页面,3 列网格展示 18 张彩色主题卡片,无任何选中
  2. 切换列数:点击"2 列"按钮,卡片重新排版为 2 列;点击"4 列",变为 4 列
  3. 选中卡片:点击"星空"卡片,出现白色边框 + 紫色发光阴影,右上角显示"已选 1 | 清除"
  4. 多选:继续点击"海洋"和"日出",已选计数变为 3,三张卡片同时高亮
  5. 取消选中:再次点击"星空",计数变为 2,该卡片恢复默认样式
  6. 清除选择:点击"清除",所有选中状态归零,计数信息消失

六、总结

Grid 是 ArkUI 中设计最简洁的布局组件之一。它的核心就是一条属性:columnsTemplate。掌握了这个属性的用法,就能在 2 列、3 列、4 列甚至混合列宽之间自由切换。

本文通过"照片画廊"这个实战案例,覆盖了:

  1. columnsTemplate 的动态构建与列数切换
  2. GridItem 的嵌套使用
  3. 选中状态的边框 + 阴影双重反馈
  4. 多选管理与计数清除
  5. Grid / List / WaterFlow 的选择原则

Grid 配合 List(列表)、WaterFlow(瀑布流)和 Scroll(滚动),构成了 ArkUI 布局体系的四大容器。根据内容特征选择合适的容器,是鸿蒙应用 UI 开发的基础能力。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐