鸿蒙原生ArkTS布局方式之Grid实现照片墙布局
鸿蒙原生 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 的关键特性
GridItem 是 Grid 的直接子组件,它支持的关键属性:
columnStart:起始列索引(从 0 开始)columnEnd:结束列索引(不包含,即独占第 0 列时columnEnd = 1)rowStart:起始行索引rowEnd:结束行索引(不包含)
跨列原理:当 columnEnd - columnStart > 1 时,该 GridItem 跨越多个列。
3.3 流式排列 vs 显式定位
GridItem 有两种排列方式:
| 方式 | 使用场景 | 设置方法 |
|---|---|---|
| 流式排列 | 普通等宽项 | 不设 columnStart/columnEnd,Grid 按顺序自动填充 |
| 显式定位 | 跨列/跨行/特殊位置 | 设置 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() 函数,在数据层预先计算每个 GridItem 的 colStart 和 colEnd:
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;
}
算法原理:
colCursor表示当前行已被占用的列数(从 0 到 3)- 遍历每个照片项,检查
span + colCursor <= 3是否成立 - 如果不成立,说明本行放不下了,换行(
colCursor = 0) - 设置
colStart = colCursor,colEnd = colCursor + span - 更新
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 的重要性
ForEach 和 LazyForEach 的第三个参数 keyGenerator 用于唯一标识每个项,帮助框架高效复用组件:
(item: PhotoItem): string => item.id.toString()
必须保证 key 唯一且稳定。推荐使用数据库 ID、UUID 等持久化唯一标识。
7.3 GridItem 的复用策略
鸿蒙的 Grid 组件内部会回收不可见的 GridItem。为了最大化复用效率:
- 保持
GridItem的子组件结构一致(不要在一个分支用Stack,另一个分支用Column) - 尽量减少
if条件分支对组件树结构的改变 - 使用
visibility而非if来控制显隐(如果性能敏感)
7.4 避免不必要的 Grid 属性变化
columnsTemplate、rowsTemplate 等属性变化会触发布局重排。如果需要在运行时改变列数,建议:
// 使用 @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 布局常见陷阱
- 所有 GridItem 都设置 columnStart(0) → 全部重叠在第 0 列
- Grid 没有设置 width(‘100%’) → Grid 宽度为 0,内容不显示
- Grid 嵌套在未设高度的 Column 中 → Grid 高度坍塌
- 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 风格照片墙的全流程。关键收获:
Grid+columnsTemplate= 多列网格布局的基础columnStart/columnEnd= 实现跨列效果的利器- 纯流式排列 + 仅跨列项显式定位 = 避免重叠的最佳实践
- 数据层预计算位置 = 将布局逻辑与 UI 解耦的优雅方案
- 组件化拆分 = 提升代码可维护性的关键
Grid 是鸿蒙原生布局体系中功能最强大的容器之一,掌握它的使用方式,意味着你可以应对绝大多数的网格类 UI 场景——从照片墙到商品列表,从仪表盘到游戏地图。
希望本文能为你的鸿蒙原生开发之旅提供坚实的参考。欢迎在实际项目中尝试这些技术,并根据业务需求进行拓展和优化。
完整源代码:参见项目
entry/src/main/ets/目录下的PhotoWall.ets、PhotoData.ets、Index.ets
API 参考:HarmonyOS Grid 组件文档
SDK 版本:HarmonyOS NEXT API 24
开发工具:DevEco Studio NEXT
更多推荐




所有评论(0)