鸿蒙原生 ArkTS 布局方式之 Grid 实现照片墙布局


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

在移动应用开发中,照片墙(Photo Wall / Image Grid)是最常见的 UI 模式之一。从 Instagram 的个人主页到 Pinterest 的瀑布流,从手机相册到电商商品列表,网格布局几乎无处不在。在鸿蒙原生生态中,Grid 组件是实现此类布局的首选方案。

本文将从一个完整的实战项目出发,深入剖析如何使用鸿蒙 NEXT(API 24)的 Grid 组件构建 Instagram 风格的照片墙。我们将覆盖从数据建模、组件拆分、布局编排到性能优化的全链路知识,帮助你彻底掌握 Grid 在复杂 UI 场景中的应用。


二、项目背景与目标

2.1 为什么选择 Grid 而非其他布局?

鸿蒙 ArkTS 提供了多种布局容器:

布局容器 适用场景 局限性
Column / Row 单一方向的线性排列 无法实现多列自动换行
Flex 弹性盒模型布局 等分排列不够灵活
RelativeContainer 相对定位布局 不适合列表型内容
List 垂直/水平滚动列表 单列,难以实现网格
Grid 多行多列网格
WaterFlow 瀑布流(不等高) API 24 新增,复杂度略高

Grid 的优势在于:原生支持行/列数定义跨行跨列自动换行以及可滚动,这些特性正是照片墙所需要的。

2.2 设计目标

我们的照片墙应包含以下功能模块:

┌─────────────────────────────────────┐
│  📷 照片墙                🔔  ☰    │  ← 顶部导航栏
├─────────────────────────────────────┤
│  [头像]   12   1.2k   368           │  ← 用户资料栏
│           帖子   粉丝   关注         │
│  📸 旅行摄影 | 美食探店 | 生活记录   │
│  [编辑个人资料]                      │
│  ✈️ 🍜 🌊 📚 🎬                    │  ← 故事高亮圈
├─────────────────────────────────────┤
│  全部  旅行  美食  建筑  人像       │  ← 分类标签栏
├─────────────────────────────────────┤
│  ┌───┬───┬─────────┐               │
│  │ 🏔️ │ 🌅 │    🌊   │               │  ← Grid 照片墙
│  ├───┴───┼───┬─────┤               │     核心区域
│  │   🌿  │ 🌸 │ 🍜 │               │
│  ├───┬───┼───┴─────┤               │
│  │ 🏛️ │ 🦋 │   🍃   │               │
│  ├───┼───┼───┬─────┤               │
│  │ ☕ │   🍰    │   │               │
│  ├───┴───┼───┴─────┤               │
│  │  🌻   │   🏕️    │               │
│  └───────┴─────────┘               │
└─────────────────────────────────────┘

三、核心概念:Grid 组件深度解析

3.1 Grid 的基本属性

Grid 是鸿蒙提供的网格布局容器,其核心属性包括:

Grid() {
  // GridItem 子组件
}
.columnsTemplate('1fr 1fr 1fr')  // 列模板:3列等比
.rowsTemplate('1fr 1fr 1fr')     // 行模板(可选,不设则自动)
.columnsGap(8)                    // 列间距
.rowsGap(8)                       // 行间距
.editable(false)                  // 是否可编辑
.layoutWeight(1)                  // 权重
.width('100%')
.height('100%')

columnsTemplate 的值语法(同样适用于 rowsTemplate):

语法 含义 示例
'1fr 1fr 1fr' 三列等比例 每列占 1/3
'1fr 2fr 1fr' 三列不等比 中间列是两侧的 2 倍
'100px 1fr 2fr' 混合单位 首列固定 100px,其余等比
'repeat(3, 1fr)' repeat 函数 同上,等比例 3 列
'auto-fit 150px' 自适应列宽 每列至少 150px,自动填充

3.2 GridItem 的关键特性

GridItemGrid 的直接子组件,它支持的关键属性:

  • columnStart:起始列索引(从 0 开始)
  • columnEnd:结束列索引(不包含,即独占第 0 列时 columnEnd = 1
  • rowStart:起始行索引
  • rowEnd:结束行索引(不包含

跨列原理:当 columnEnd - columnStart > 1 时,该 GridItem 跨越多个列。

3.3 流式排列 vs 显式定位

GridItem 有两种排列方式:

方式 使用场景 设置方法
流式排列 普通等宽项 不设 columnStart/columnEndGrid 按顺序自动填充
显式定位 跨列/跨行/特殊位置 设置 columnStart+columnEnd,手工指定位置

💡 关键原则:对于普通 1 列宽的项,不要设置 columnStart/columnEnd,让 Grid 自动流式排列。只对需要跨列(span > 1)的项手工定位。如果所有项都设了 columnStart(0),它们会全部重叠在第 0 列——这是初学者最容易踩的坑。


四、位置预计算:解决跨列重叠问题

4.1 问题分析

Instagram 风格的照片墙有一个典型特征:某些精选照片会占据 2 列宽度,形成视觉节奏的变化。在 Grid 中实现跨列并不难,难的是让跨列项与普通项在流式排列中不重叠

普通 Grid 流式排列(所有项 span=1):
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
└───┴───┴───┘

目标排列(混合 span):
┌───┬───┬───────┐
│ 1 │ 2 │   3   │  ← #3 跨列
├───┴───┼───┬───┤
│   4   │ 5 │ 6 │  ← #4 跨列
├───┬───┼───┴───┤
│ 7 │ 8 │   9   │
├───┼───┼───┬───┤
│10 │   11   │   │  ← #11 跨列
└───┴───────┴───┘

4.2 解决方案:游标算法

我们在 PhotoData.ets 中实现了一个 buildPhotoDataSource() 函数,在数据层预先计算每个 GridItemcolStartcolEnd

function buildPhotoDataSource(): PhotoItem[] {
  let colCursor = 0; // 当前行已占列数 (0, 1, 2)
  const result: PhotoItem[] = [];

  for (const raw of rawPhotos) {
    // 如果当前项跨列数 > 本行剩余列数,则换行
    if (raw.span + colCursor > 3) {
      colCursor = 0;
    }

    result.push({
      ...raw,
      colStart: colCursor,
      colEnd: colCursor + raw.span
    });

    colCursor += raw.span;
    if (colCursor >= 3) colCursor = 0; // 本行已满
  }
  return result;
}

算法原理

  1. colCursor 表示当前行已被占用的列数(从 0 到 3)
  2. 遍历每个照片项,检查 span + colCursor <= 3 是否成立
  3. 如果不成立,说明本行放不下了,换行(colCursor = 0
  4. 设置 colStart = colCursorcolEnd = colCursor + span
  5. 更新 colCursor += span,如果达 3 则归零

4.3 在 Grid 中应用

Grid() {
  ForEach(this.photos, (item: PhotoItem) => {
    if (item.span > 1) {
      // 跨列大图:手工指定起止列
      GridItem() {
        PhotoCard({ photoItem: item, colSpan: item.span })
      }
      .columnStart(item.colStart)  // 从预计算的列开始
      .columnEnd(item.colEnd)      // 到预计算的列结束
    } else {
      // 普通小图:不设定位属性,自动流式排列
      GridItem() {
        PhotoCard({ photoItem: item, colSpan: 1 })
      }
    }
  },
  (item: PhotoItem): string => item.id.toString())
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(3)
.rowsGap(3)

五、组件化设计:将照片墙拆分为可复用组件

5.1 组件树

PhotoWall (@Entry)
 ├── 顶部导航栏 (Row inline)
 ├── Scroll
 │    ├── ProfileHeader
 │    │    ├── 头像 Circle + @Builder overlay
 │    │    ├── 统计数据 statItem (@Builder)
 │    │    └── 故事高亮圈 Circle + @Builder overlay
 │    ├── CategoryTabs (Scroll > Row > ForEach > Text)
 │    └── Grid
 │         ├── GridItem > PhotoCard (span=1)
 │         ├── GridItem > PhotoCard (span=2, with columnStart/End)
 │         └── ...

5.2 关键组件解析

PhotoCard — 最小照片卡片单元
@Component
struct PhotoCard {
  photoItem: PhotoItem;
  colSpan: number;

  build() {
    Stack() {
      // 渐变色模拟照片(可替换为 Image 组件)
      Rect()
        .linearGradient({
          direction: GradientDirection.Right,
          colors: this.getGradColors(this.photoItem.color)
        })
      
      // Emoji 模拟照片内容
      Text(this.photoItem.emoji).fontSize(this.colSpan > 1 ? 48 : 36)
      
      // 底部标签
      Text(this.photoItem.label)
        .position({ x: 12, y: '80%' })
        .shadow({ radius: 4, color: '#80000000' })
    }
    .clip(true)
  }
}

设计要点

  • 通过 colSpan 属性控制 UI 表现(大图显示更大字体、更多装饰)
  • 使用 Stack 实现层叠布局,方便叠加文字和装饰元素
  • 实际项目将 Rect + linearGradient 替换为 Image + objectFit(ImageFit.Cover) 即可加载真实图片
ProfileHeader — Instagram 风格个人信息栏

ProfileHeader 组件通过 @Builder 装饰器解决了 ArkTS 中 overlay() 必须传入 Builder 的限制:

@Component
struct ProfileHeader {
  @Builder
  avatarOverlay() {
    Text('📷').fontSize(30)
  }

  build() {
    Circle()
      .width(72).height(72)
      .linearGradient({ ... })
      .overlay(this.avatarOverlay)  // ✅ 传入 @Builder 函数
  }
}

⚠️ ArkTS 重要约束overlay() 方法的参数必须是 string | CustomBuilder | ComponentContent<Object> 类型。不能直接传入 Text(...) 这样的组件表达式,必须用 @Builder 包装。


六、完整代码详解

6.1 数据模型层(PhotoData.ets)

/** 照片数据接口 */
export interface PhotoItem {
  id: number;
  color: string;
  emoji: string;
  label: string;
  span: number;
  colStart: number;  // ⭐ 预计算起始列
  colEnd: number;    // ⭐ 预计算结束列
}

数据模型是整个应用的基础。我们将 布局信息(colStart/colEnd) 放在数据结构中,而非在 UI 层硬编码,使得:

  • 数据驱动布局:只需修改数据,布局自动变化
  • 解耦:UI 组件不关心位置计算逻辑
  • 可测试:位置计算逻辑可以独立单元测试

6.2 组件层(PhotoWall.ets)

整个页面约 293 行,结构清晰:

行号范围 组件 职责
14-78 PhotoCard 单张照片卡片的视觉呈现
81-173 ProfileHeader 用户资料栏(头像/统计/故事)
168-195 CategoryTabs 分类标签水平滚动栏
199-293 PhotoWall 主入口,组合所有组件 + Grid 布局

七、性能考量与最佳实践

7.1 使用 ForEach 而非 LazyForEach

在数据量较小(13 项)时,ForEach 足够高效。对于数百张照片的大规模场景,应使用 LazyForEach 实现按需加载:

// 大数据量时推荐
LazyForEach(this.dataSource, (item: PhotoItem) => {
  GridItem() { ... }
}, (item: PhotoItem) => item.id.toString())

7.2 keyGenerator 的重要性

ForEachLazyForEach 的第三个参数 keyGenerator 用于唯一标识每个项,帮助框架高效复用组件:

(item: PhotoItem): string => item.id.toString()

必须保证 key 唯一且稳定。推荐使用数据库 ID、UUID 等持久化唯一标识。

7.3 GridItem 的复用策略

鸿蒙的 Grid 组件内部会回收不可见的 GridItem。为了最大化复用效率:

  • 保持 GridItem 的子组件结构一致(不要在一个分支用 Stack,另一个分支用 Column
  • 尽量减少 if 条件分支对组件树结构的改变
  • 使用 visibility 而非 if 来控制显隐(如果性能敏感)

7.4 避免不必要的 Grid 属性变化

columnsTemplaterowsTemplate 等属性变化会触发布局重排。如果需要在运行时改变列数,建议:

// 使用 @State 动态控制
@State private columnCount: number = 3;

build() {
  Grid() { ... }
  .columnsTemplate(`1fr ${'1fr '.repeat(this.columnCount - 1).trim()}`)
}

八、踩坑记录与常见问题

8.1 编译错误汇总

错误信息 原因 解决方案
Module has no exported member 'router' 导入方式错误 import router from '@ohos.router'(默认导出)
Type is missing properties colStart, colEnd 默认值未同步更新 补全所有必填字段的默认值
Property 'BottomRight' does not exist on type 'typeof GradientDirection' 枚举值不存在 查阅 API 文档,使用 Right / Top 等标准值
TextAttribute is not assignable to parameter of type 'string | CustomBuilder' overlay() 传参错误 @Builder 函数包装组件
Property 'xxx' is private and can not be initialized through the component constructor ArkTS 限制 去掉 private 修饰符

8.2 布局常见陷阱

  1. 所有 GridItem 都设置 columnStart(0) → 全部重叠在第 0 列
  2. Grid 没有设置 width(‘100%’) → Grid 宽度为 0,内容不显示
  3. Grid 嵌套在未设高度的 Column 中 → Grid 高度坍塌
  4. Scroll 未设置 layoutWeight(1) → 内容区域不能滚动

九、扩展与进阶

9.1 替换为真实图片

当前示例使用渐变色 + Emoji 模拟照片。接入真实图片只需替换 PhotoCard 中的 Rect

// 替换前:渐变色占位
Rect()
  .linearGradient({ ... })

// 替换后:真实图片
Image($r('app.media.photo_' + this.photoItem.id))
  .objectFit(ImageFit.Cover)  // 覆盖裁剪,保持正方形
  .width('100%')
  .height('100%')

9.2 添加点击预览

为照片添加点击放大预览效果:

GridItem() {
  PhotoCard({ ... })
}
.onClick(() => {
  // 弹出图片预览
  this.showPreview = true;
  this.previewIndex = index;
})

配合 @State 控制预览弹层。

9.3 从 WaterFlow 到瀑布流

如果需要实现图片不等高的 Pinterest 风格瀑布流,可使用 WaterFlow 组件:

WaterFlow() {
  LazyForEach(this.dataSource, (item: PhotoItem) => {
    FlowItem() {
      Image(item.src)
        .width('100%')
        .height(item.height)  // 每张图片高度不同
        .objectFit(ImageFit.Cover)
    }
  })
}
.columnsTemplate('1fr 1fr 2fr')  // 三列,中间稍宽

9.4 适配折叠屏

利用 API 24 的折叠屏能力,动态调整 Grid 列数:

import { display } from '@kit.ArkUI';

@State private columns: number = 3;

aboutToAppear() {
  const windowInfo = display.getDefaultDisplaySync();
  if (windowInfo.width > 600) {
    this.columns = 4; // 平板/折叠屏展开态 4 列
  } else {
    this.columns = 3; // 手机态 3 列
  }
}

十、总结

通过本文的实战,我们完整地走了一遍使用鸿蒙原生 Grid 组件构建 Instagram 风格照片墙的全流程。关键收获:

  1. Grid + columnsTemplate = 多列网格布局的基础
  2. columnStart / columnEnd = 实现跨列效果的利器
  3. 纯流式排列 + 仅跨列项显式定位 = 避免重叠的最佳实践
  4. 数据层预计算位置 = 将布局逻辑与 UI 解耦的优雅方案
  5. 组件化拆分 = 提升代码可维护性的关键

Grid 是鸿蒙原生布局体系中功能最强大的容器之一,掌握它的使用方式,意味着你可以应对绝大多数的网格类 UI 场景——从照片墙到商品列表,从仪表盘到游戏地图。

希望本文能为你的鸿蒙原生开发之旅提供坚实的参考。欢迎在实际项目中尝试这些技术,并根据业务需求进行拓展和优化。


完整源代码:参见项目 entry/src/main/ets/ 目录下的 PhotoWall.etsPhotoData.etsIndex.ets
API 参考HarmonyOS Grid 组件文档
SDK 版本:HarmonyOS NEXT API 24
开发工具:DevEco Studio NEXT

Logo

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

更多推荐