鸿蒙原生应用 HarmonyOS ArkTS:活动相册首页的 Banner 与双列瀑布流
鸿蒙原生应用 HarmonyOS ArkTS:活动相册首页的 Banner 与双列瀑布流
App 40「校园活动照片」首页(HomeTab),主题色
#6C5CE7紫色(violet),4 个 Tab 分别为首页(📸)、活动(🎉)、上传(📤)、我的(👤)。首页采用"Header + Banner + 瀑布流"三区布局——白色单行 Header("活动相册"22 号加粗) + 紫色渐变 Banner(📸 38 号大图标 + "定格校园美好瞬间"16 号加粗 + "已收录 12 场活动 · 858 张照片") + SectionTitle(最新相册 + 全部 >)+ 6 个相册的双列瀑布流(🎊 迎新晚会 128 张 / 🏃 运动会精彩瞬间 256 张 / 🎓 毕业典礼 89 张 / 🎸 音乐节现场 176 张 / 🍁 秋游合影 64 张 / 🎭 话剧演出 45 张,每张卡片高度 130~200 不等,构成错落有致的瀑布流视觉)。本篇基于40-event-photo/entry/src/main/ets/pages/HomeTab.ets(共 108 行)逐段拆解,附 4 张实机截图。
一、整体结构:三区"Header + Banner + 瀑布流"布局
首页是"品牌头部 + 宣传 Banner + 瀑布流相册"的三区布局——3 个 @Builder 块:
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 14 }) {
this.Banner()
this.SectionTitle('最新相册')
this.Waterfall()
}
.width('100%')
.padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
3 块结构:
- Header — 单行品牌头("活动相册"),白底、全宽、固定在顶部非滚动区
- Banner — 紫色渐变宣传卡,用"12 场活动 · 858 张照片"给用户平台规模的直观感受
- Waterfall — "最新相册"标题 + 双列瀑布流,6 个相册卡片按奇偶索引分到左右两列,是本页也是整个 App 最具视觉辨识度的部分
整页内容在 Scroll 中滚动,layoutWeight(1) 让滚动区占满剩余高度,scrollBar(BarState.Off) 隐藏滚动条,底部 safeBottom + 20 预留安全区——这套骨架与前几个 App 完全一致,是系列统一的结构范式。
二、Header:单行品牌头
Header() {
Column() {
Text('活动相册').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text)
.width('100%').padding({ top: this.safeTop + 10, left: D.pad, right: D.pad, bottom: 12 })
}.width('100%').backgroundColor(C.card)
}
单行 22 号加粗标题,顶部 safeTop + 10 避让状态栏,底部 12 内边距,全宽白底。与 App 39 首页的双行 Header 相比,本页没有副标题——因为信息量都集中在了 Banner 上,Header 保持极简,避免与 Banner 抢视觉焦点。这也是系列中"Header 与 Banner 信息量互补"的设计惯例。

三、Banner:紫色渐变宣传卡
Banner() {
Row({ space: 12 }) {
Text('📸').fontSize(38)
Column({ space: 4 }) {
Text('定格校园美好瞬间').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('已收录 12 场活动 · 858 张照片').fontSize(12).fontColor('#FFFFFF').opacity(0.9)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%').padding(18).borderRadius(D.rLg)
.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}
技术拆解:
- 大图标 + 双行文案:左侧 38 号 📸 emoji 是视觉锚点,右侧 16 号加粗主文案"定格校园美好瞬间" + 12 号 90% 透明度副文案"已收录 12 场活动 · 858 张照片"。
- 数据说服力:副文案用具体数字(12 场活动、858 张照片)而不是空泛形容词,让用户在进入首页的第一秒就感知平台体量——这是"用数据说话"的运营思路。
- 渐变背景:
linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })从主题紫#6C5CE7渐变到浅紫#9B8AF5,135° 对角过渡。 - 布局弹性:右侧 Column
layoutWeight(1)占满剩余宽度,左侧图标固定大小,任何屏宽下排版稳定。 - 无点击事件:Banner 纯展示,没有挂 onClick——它只承担"规模背书"职能,把交互都让给下方瀑布流。
四、SectionTitle:标题 + "全部 >"入口
SectionTitle(title: string) {
Row() {
Text(title).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('全部 >').fontSize(12).fontColor(C.textDim)
}.width('100%')
}
本页的 SectionTitle 比 App 39 多了一个"全部 >"尾部入口——左侧 16 号加粗标题、右侧 12 号浅灰"全部 >",中间用 Blank() 弹性撑开。Blank() 是 ArkUI 的弹性占位组件,自动吸收剩余空间,是"左文右入口"标题行的标准写法。虽然 Demo 中"全部 >"未挂点击事件,但产品语义明确:点击进入全部相册列表页。
五、Waterfall:双列瀑布流核心实现
这是本页乃至全 App 最值得深挖的布局:
Waterfall() {
Row({ space: 12 }) {
Column({ space: 12 }) {
ForEach(this.albums, (a: Album, i: number) => {
if (i % 2 === 0) { this.AlbumCard(a) }
}, (a: Album) => a.id.toString())
}.layoutWeight(1)
Column({ space: 12 }) {
ForEach(this.albums, (a: Album, i: number) => {
if (i % 2 === 1) { this.AlbumCard(a) }
}, (a: Album) => a.id.toString())
}.layoutWeight(1)
}.width('100%').alignItems(VerticalAlign.Top)
}
实现原理:真正的瀑布流组件 WaterFlow 需要 LazyForEach + FlowItem,代码较重;本页用"双列手写瀑布流"轻量替代——外层 Row({ space: 12 }) 分成左右两个 Column,各自 layoutWeight(1) 等宽;遍历 albums 数组时按索引奇偶分流:偶数索引(0/2/4)进左列、奇数索引(1/3/5)进右列。每个 Column 内部 space: 12 控制纵向间距。alignItems(VerticalAlign.Top) 保证两列顶部对齐。
为什么视觉上是瀑布流:关键在于每张卡片的图片区高度不同(height: 140/180/150/200/130/170 写死在数据里),左列卡与右列卡参差错开、互不齐平,就形成了"瀑布"般的犬牙交错效果。6 个相册的分配与高度:
| 列 | 相册 | 高度 |
|---|---|---|
| 左列 | 🎊 迎新晚会 | 140 |
| 左列 | 🎓 毕业典礼 | 150 |
| 左列 | 🍁 秋游合影 | 130 |
| 右列 | 🏃 运动会精彩瞬间 | 180 |
| 右列 | 🎸 音乐节现场 | 200 |
| 右列 | 🎭 话剧演出 | 170 |
左列三张(140/150/130)与右列三张(180/200/170)在纵向推进速度上不同步,交错出丰富的层次感。
六、AlbumCard:相册卡片
AlbumCard(a: Album) {
Column() {
Row() { Text(a.emoji).fontSize(46) }
.width('100%').height(a.height).backgroundColor(C.primarySoft)
.borderRadius({ topLeft: D.rMd, topRight: D.rMd }).justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(a.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
Text('📷 ' + a.count + ' 张').fontSize(11).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).width('100%').padding(10)
}
.width('100%').backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
.onClick(() => { promptAction.showToast({ message: a.title }); })
}
卡片细节:
- 图片区:
height(a.height)从数据读取(140~200 动态),46 号 emoji 居中,primarySoft浅紫底,只设上圆角(borderRadius({ topLeft, topRight }))——下缘与信息区相接,形成"图在上、文在下"的卡片结构。 - 信息区:14 号 Medium 标题(
maxLines(1)+textOverflow省略号防止长标题换行撑破布局)+ 11 号浅灰"📷 N 张",10 内边距。 - 整卡容器:白底圆角 14 + 描边,点击 Toast 相册名。
- key:外层 ForEach 以
a.id.toString()为 key,且两个列的 ForEach 共享同一数组——注意两列各自遍历时会重复生成 key,但因为分属两个不同 ForEach,key 命名空间独立,不会冲突。
七、跨页数据自洽:相册与活动、我的页的数字闭环
首页不是孤立页面,数据与其他 Tab 严格呼应:
- Banner"12 场活动" ↔ 我的页统计"12 参与活动"数字一致(平台活动总数 = 用户参与数在 Demo 中的简化设定)。
- 首页 6 个相册 ↔ 活动页 4 个活动(迎新晚会、秋季运动会、校园音乐节、话剧社公演)直接同名——首页相册就是活动页活动的"照片集",点进活动即可查看对应相册。
- 相册张数:迎新晚会 128、运动会 256、音乐节 176、话剧 45 与活动页各活动的"📷 N 张照片"完全一致(毕业典礼 89、秋游合影 64 是活动页未列出的已结束活动照片)。
- 我的页"96 我的照片" ↔ Banner"858 张照片":858 是平台总量,96 是个人上传量,个人与平台的量级关系合理。
八、实机截图与交互演示
本节结合 4 张实机截图,逐张还原首页的视觉效果与交互过程。
1. 首页首屏:渐变 Banner + 双列瀑布流
第一张截图是进入 App 40 后的默认首屏:顶部"活动相册"单行标题,下方紫色渐变 Banner(📸 大图标 + "定格校园美好瞬间" + "已收录 12 场活动 · 858 张照片"),再往下是"最新相册 全部 >"标题行与双列瀑布流——左列可见🎊迎新晚会、🎓毕业典礼,右列可见🏃运动会精彩瞬间,两列卡片高度错落、顶部对齐,瀑布流的犬牙交错感已经显现。
2. 点击相册卡:弹出相册名 Toast
第二张截图是点击左列第一张"🎊 迎新晚会"相册卡后的反馈:弹出"迎新晚会"Toast。每张卡片独立挂载 onClick 且闭包捕获各自的 a.title,点击哪张就弹出哪个相册名,多人卡片列表的点击参数传递正确无误。
3. 滚动中部:更多相册露出
第三张截图是向上滑动后的中部视图:左列第三张"🍁 秋游合影"(64 张)与右列"🎸 音乐节现场"(176 张)完整露出,两列高度差(130 vs 200)带来的错落感更加明显。可以看到 Scroll 滚动流畅、瀑布流双列节奏稳定。
4. 滚动到底:底部相册与安全区
第四张截图是滚动到底部的视图:右列最末的"🎭 话剧演出"(45 张)完整可见,6 个相册全部浏览完毕。页面底部预留了 safeBottom + 20 的安全区内边距,底部 Tab 栏始终可点,滚动与导航互不干扰。
九、扩展思考:手写瀑布流 vs WaterFlow 组件
本页用"双列 ForEach 分流"手写了瀑布流,工程上如何选型值得展开:
- 手写双列的适用场景:数据量小(几十条以内)、卡片数量相对固定时,双列手写法代码直观、无额外依赖,且无需处理滚动复用,是原型与轻量产品的首选。本页 6 个相册完全够用。
- WaterFlow 的适用场景:数据量大(上百条)或需要无限滚动加载时,应使用 ArkUI 原生
WaterFlow+FlowItem,配合LazyForEach实现按需渲染与节点回收,性能远超手写双列。 - 高度来源的差异:手写法的卡片高度写死在数据里(
height: 140/180),而真实产品中图片高度来自网络图片的宽高比,通常用aspectRatio或服务端返回的尺寸动态计算——手写方案要支持"高度动态"只需把height改为运行时计算值。 - 两端对齐策略:手写法的两列是独立 Column,若某一列内容明显更长,页面底部会出现"一列长一列短"的不对齐——WaterFlow 会动态把新卡片塞进更短的一列,保证两端基本齐平,这是组件方案的核心优势。

十、扩展思考:相册类产品的数据模型设计
从首页 6 个相册可以反推一套相册数据模型:
- Album 模型:
{ id, emoji, title, count, height }中 id 是唯一标识、count 是照片总数、height 是卡片占位高度。真实产品中应扩展coverUrl(封面图)、author(作者)、createdAt(创建时间)、likes(点赞数)等字段。 - 封面策略:Demo 用 emoji 占位封面,真实产品应展示相册第一张照片(或运营配置的精选封面),瀑布流的"图"才是视觉主体,emoji 只是原型替代。
- 列表与详情的层级:相册卡点击后应进入相册详情页(九宫格照片墙 + 点赞评论),首页只承担"发现"职责——本 Demo 的 Toast 占位了这条跳转链路。
- 增量更新:用户每上传一批照片(如上传页的"上传 3 张"),对应相册的 count 应 +3、封面可刷新为最新照片——首页相册与上传页的数据联动是产品化必经之路。

十一、常见问题与开发避坑
- 双列 key 重复警告:两个列的 ForEach 都遍历同一
albums数组、都用a.id.toString()作 key——ArkUI 对同一 ForEach 内的 key 唯一性校验,不同 ForEach 之间 key 重复不报错,但若抽成共享子组件需注意 key 传递。 - borderRadius 只设上角:
borderRadius({ topLeft, topRight })只圆上方两角,下方与信息区直角相接。若想四角全圆,需要把图片区与信息区放进同一圆角容器再clip(true),否则图片区会溢出圆角。 - maxLines 省略号依赖宽度:
textOverflow({ overflow: TextOverflow.Ellipsis })必须配合maxLines(1)与明确的width('100%')才生效,三者缺一不可。 - Blank() 的弹性行为:
Blank()在 Row 中占据剩余空间,如果 Row 里还有layoutWeight组件会冲突——本页标题行没有 layoutWeight 组件,使用正确。 - 瀑布流底部不对齐:手写双列在内容量不平衡时底部会参差,可接受(瀑布流本就追求错落);若要求齐平,需在较短列尾部补空占位卡或改用 WaterFlow。
十二、扩展思考:相册详情页与照片浏览的完整设计
首页的瀑布流卡片点击后(当前 Toast)应进入相册详情页,围绕"看照片"这一核心诉求,详情页的设计可以非常丰富:
- 照片九宫格墙:详情页主体用
Grid(复用上传页的columnsTemplate('1fr 1fr 1fr')三列方案)或瀑布流展示相册内全部照片,点击单张进入全屏预览。首页卡片count字段(128/256/89...)正好驱动详情页的"共 N 张"标题。 - 图片懒加载:一个相册可能有上百张照片,九宫格必须用
LazyForEach+IDataSource按需渲染,首屏只加载可视区域内的缩略图,滚动时动态构建——否则一次渲染 128 个Image节点会明显卡顿。 - 全屏预览与手势:单张照片支持双指缩放(
PinchGesture)、左右滑动切换(Swiper)、双击放大——ArkUI 的Swiper组件天然支持多图轮播,配合PinchGesture实现浏览体验。 - 互动能力:照片页常配点赞(❤️ 计数)、评论、下载按钮——与我的页"234 获赞 / 18 收藏 / 下载记录"的数据口径对应,互动数据从详情页产生、在个人页汇总。
- 照片信息:每张照片可展示拍摄者、拍摄时间、所属活动(从
Album反查Event)——EXIF 元数据是照片类产品的差异化信息层。
十三、扩展思考:系列主题色与视觉演进回顾
把 App 40 放入本系列横向审视,可以看到一条清晰的主题色演进线:
| App | 主题 | 色值 | 视觉氛围 |
|---|---|---|---|
| 35 | 笔记交换 | 紫罗兰 #9B51E0 | 沉静文艺 |
| 38 | 技能交换 | 青蓝 #2D9CDB | 清爽理性 |
| 39 | 兴趣匹配 | 粉色 #FF6B9D | 甜美社交 |
| 40 | 活动照片 | 紫色 #6C5CE7 | 青春活力 |
App 40 的紫色 #6C5CE7 与 App 35 的紫罗兰 #9B51E0 同属紫色系但取向不同:前者偏"蓝紫"(活泼、年轻、适合照片社区)、后者偏"红紫"(深沉、文艺、适合笔记场景)——同色系不同色相的细微取舍,正是主题设计的精妙之处。而"渐变方向统一 135°、primary→accent 双色渐变、白卡悬浮头图、负边距统计"这些范式在 39/40 两代 App 中一脉相承,说明这套代码模板已经高度成熟——新 App 只需换色值与数据,UI 质量即达标,这正是本系列"一 App 一主题"批量生产方法论的底气。

十四、系列横向对比:照片社区类首页的设计分层
把 App 40 首页放入本系列横向对比,可以看到"内容型首页"的设计分层:
| 首页类型 | 代表 App | 视觉核心 | 内容组织 |
|---|---|---|---|
| 双列瀑布流 | App 40 活动照片 | 渐变 Banner + 错落相册 | 相册卡片 |
| 单列信息流 | App 38 技能交换 | 分类胶囊 + 渐变 Banner | 技能卡片 |
| 卡片推荐流 | App 39 兴趣匹配 | 环形进度 + 匹配卡 | 用户卡片 |
| 分段目录流 | App 36 课本共享 | 商品分区标题 | 商品卡片 |
App 40 是系列中唯一使用双列瀑布流的首页——因为照片本身是"图为主"的内容,两列排布能在一屏展示更多封面、减少滚动深度,这是照片/视频类 App(如小红书、ins)的通用选择。而技能、用户、商品类内容以"文字信息"为主,单列列表更能保证信息可读性。这一对比揭示了首页布局的第一原则:内容形态决定布局形态——图片内容用瀑布流、文字内容用列表、混合内容用卡片流。
再看技术实现,App 40 首页用"双列 ForEach + 奇偶分流"手写瀑布流,而真实产品在数据量大时会切到 WaterFlow 组件;对比 App 39 首页的 Progress 环形进度、App 38 首页的 CatRow 横滑胶囊,每个首页都在复用同一套"Header + Banner + 列表"骨架的同时,用一个差异化的视觉组件(瀑布流/环形进度/分类胶囊)建立页面记忆点——骨架统一、亮点各异,是批量产出高质量页面的核心方法论。
十五、开发者视角:首页的调试与验证技巧
在开发本页时,有几个工程化技巧值得记录:
- 布局调试:
Scroll内的卡片高度动态(130~200),调试时可用dumpLayout导出节点树确认每个 AlbumCard 的 bounds 是否符合预期——本系列截图流程正是基于此工具做页面验收。若卡片错位,优先检查aspectRatio与height是否冲突。 - 瀑布流对齐验证:双列瀑布流的常见 Bug 是某列多卡导致底部严重参差。可在数据源临时加
height: 0或极大值卡片测试两列推进速度,确认奇偶分流逻辑正确后再还原。 - emoji 渲染差异:🎊🏃🎓 等 emoji 在不同机型字体下尺寸略有差异(46 号在部分字体下偏大导致溢出容器)——建议容器留 padding 或用
TextOverflow兜底,保证任意设备不溢出。 - Toast 联调:点击卡片弹 Toast 验证闭包捕获正确性时,可同时点击多张卡,观察 Toast 文案是否与卡片一一对应——若所有 Toast 都显示最后一张卡的名字,说明 ForEach 的 key 或闭包捕获有误。
- 安全区回归:修改
safeTop/safeBottom后务必在刘海屏与普通屏两种形态下回归测试——本页 Header 顶部与列表底部都有安全区消费,任何一侧漏配都会出现内容遮挡。
十六、FAQ 与一句话总结
Q1:为什么相册用 emoji 而不是真实照片? 原型阶段用 emoji 占位零资源、零网络、渲染快,同时 🎊🏃🎓 等 emoji 与活动主题天然对应,视觉不违和。真实产品把 Text(a.emoji) 换成 Image(a.coverUrl) 即可,布局代码零改动。
Q2:瀑布流高度为什么写死? 6 个相册是静态数据,高度写死最简单直接,且刻意制造"140~200 错落"的视觉效果。若接入网络图片,改为 aspectRatio 或运行时计算高度即可。
Q3:"全部 >"为什么不能点? Demo 未实现全部相册列表页,属产品化步骤。挂上 onClick 跳转相册列表页后,首页即具备"最新 6 个 + 全部入口"的完整导航结构。
Q4:双列瀑布流和单列列表哪个更好? 取决于内容密度:照片类 App 用双列/三列瀑布流能在相同高度展示更多内容、减少滚动;信息类(如技能列表)用单列更清晰。本 App 是照片场景,双列是正确选择。
Q5:858 张照片是怎么来的? Banner 文案写死,实为 6 个相册张数(128+256+89+176+64+45=758)之外的平台口径(含未展示相册)。产品化时应由服务端统计返回真实总量。
一句话总结:首页用"渐变 Banner + 双列瀑布流"两大视觉武器把"校园照片共享"的定位讲得明明白白——Banner 亮平台规模、瀑布流亮内容质量。手写双列瀑布流的奇偶分流技巧、maxLines 省略号、Blank() 弹性占位都是 ArkUI 高频手法,是照片社区类 App 首页的轻量范本。
值得补充的是,本页的 Waterfall 实现还顺带示范了一个 ArkUI 细节:两列 ForEach 共享同一个数组但各自过滤(i % 2 === 0 / i % 2 === 1),这种"同源分流"模式在相册、商品、资讯等双列场景中可反复复用——把过滤条件换成 i % 3 即可升级为三列瀑布流,换成按字段分组即可实现"分组瀑布流"。而 AlbumCard 的 height 从数据读取(而非写死统一高度)这一设计,也让未来接入真实封面图时只需把"静态高度"换成"图片宽高比计算值",卡片结构零改动。整体而言,首页 108 行代码承载了瀑布流、渐变、弹性布局、文本省略四大 ArkUI 高频能力,配合双列视觉与统一安全区体系,达到了"小代码、大效果"的示范目标。

更多推荐



所有评论(0)