鸿蒙原生 ArkTS 布局实战:用 Flex + FlexWrap 实现瀑布流布局雏形



一、前言
1.1 写作背景
鸿蒙原生应用开发(HarmonyOS NEXT)自全面推出以来,其 ArkTS 声明式 UI 框架受到了广大开发者的关注和青睐。然而,网上关于 ArkTS 布局实践的中文资料仍然相对稀缺,尤其是涉及到「瀑布流布局」这类综合性较强的场景时,很多开发者只能参考 Android 或 iOS 的实践经验,再自行摸索鸿蒙上的实现方式。
笔者在实际项目开发中深入研究并实践了多种 ArkTS 布局方案,本篇文章将重点分享如何利用鸿蒙原生的 Flex 容器配合 FlexWrap.Wrap 换行特性,以极简的代码量实现一个瀑布流布局的完整雏形。
1.2 什么是瀑布流布局
瀑布流(Waterfall Flow / Masonry Layout)是一种多栏错落排列的内容展示布局方式。它的名字源自其视觉特征------像瀑布一样从上往下流淌,高低起伏,错落有致。这一布局最早由 Pinterest 图片社交平台大规模推广使用,随后被各大互联网产品广泛采用。
瀑布流的核心特征包括:
- 每个内容卡片的宽度保持一致(按列固定)
- 每个卡片的高度各不相同(由内容多少或图片比例决定)
- 卡片从左到右、从上到下依次排列
- 排满一列宽度后自动折行到下一列开始位置
- 视觉上呈现「高低起伏、参差有致」的自然错落感
1.3 瀑布流的应用场景
瀑布流布局因其视觉冲击力强、信息密度高、浏览体验流畅等优点,在以下场景中应用非常广泛:
| 应用场景 | 代表产品 | 布局特点 |
|---|---|---|
| 图片社区 | Pinterest、花瓣网 | 图片比例各异,天然适合错落排列 |
| 电商推荐 | 淘宝、拼多多 | 商品图 + 标题 + 价格,卡片高度不一 |
| 社交动态 | Instagram、小红书 | 图文混排,每篇内容长度不同 |
| 笔记/博客 | Notion、简书 | 文章摘要长度不等,形成自然高度差 |
| 标签云 | 各种兴趣推荐 | 标签文字长度 + 字号不同 |
1.4 本文目标
通过本文,你将学到:
- 鸿蒙 ArkTS 中
Flex容器的核心属性与工作原理 FlexWrap.Wrap换行机制的原理与使用方法- 如何通过「固定宽度 + 不定高卡片 + 自动换行」模拟瀑布流布局
- 完整的项目代码逐段拆解与设计思路
- 组件化封装的最佳实践(
@Component+@Prop) - 常见编译错误的排查与修复方法
- 从「伪瀑布流」到「严格 Masonry 布局」的进化路线
二、鸿蒙 ArkTS 布局体系全景
2.1 ArkTS 简介
ArkTS 是鸿蒙原生应用的主力开发语言,基于 TypeScript 语法扩展而来,同时兼容 JavaScript 运行时生态。它最大的特点是采用 声明式(Declarative)UI 范式------开发者只需要描述 UI 在任意状态下的「样子」,框架会自动计算出从当前状态到目标状态的最小更新代价。
一个典型的 ArkTS 页面骨架如下:
typescript
@Entry
@Component
struct MyPage {
@State message: string = 'Hello HarmonyOS';
build() {
// 在这里以声明方式描述 UI 树
Column() {
Text(this.message)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('点击更新')
.onClick(() => {
this.message = '欢迎来到鸿蒙世界';
})
}
.width('100%')
.height('100%')
}
}
关键装饰器和结构说明:
@Entry:标记该组件为页面入口,每个 HAP 包中只能有一个@Entry组件@Component:声明这是一个可复用的自定义组件@State:声明一个响应式状态变量,当其值变化时,引用该变量的 UI 会自动刷新build():组件的 UI 构建方法,在初始化时会执行一次,之后仅当依赖的状态变量变化时按需重新执行
2.2 ArkUI 布局容器全景图
鸿蒙 ArkUI 框架提供了丰富多样的布局容器,每种容器有其特定的适用场景和布局算法。以下是对比表:
| 容器名称 | 布局方向 | 是否支持换行 | 是否支持网格 | 典型应用 |
|---|---|---|---|---|
Column |
纵向(单列) | 否 | 否 | 列表、表单、垂直信息流 |
Row |
横向(单行) | 否 | 否 | 按钮组、导航栏、标签行 |
Flex |
可配置(默认纵向) | 是(FlexWrap.Wrap) | 否 | 瀑布流、标签云、自适应排列 |
Grid |
二维网格 | 是(网格概念) | 是 | 九宫格、相册、网格列表 |
RelativeContainer |
相对定位 | 否 | 否 | 精准对齐、复杂叠加布局 |
Stack |
层叠 | 否 | 否 | 悬浮按钮、遮罩层、图片上的文字 |
List |
纵向/横向列表 | 否 | 否 | 长列表、聊天记录、设置页 |
Swiper |
横向滑动 | 否 | 否 | 轮播图、引导页 |
从表中可以看出,Flex 是唯一一个既支持水平排列又支持换行的一维布局容器,这正是它能用来模拟瀑布流的根本原因。
2.3 Flex 布局深入剖析
2.3.1 构造参数
typescript
Flex(options?: FlexOptions)
FlexOptions 接口定义:
typescript
interface FlexOptions {
direction?: FlexDirection; // 主轴方向
wrap?: FlexWrap; // 是否换行
justifyContent?: FlexAlign; // 主轴对齐方式
alignItems?: ItemAlign; // 交叉轴单行对齐
alignContent?: FlexAlign; // 交叉轴多行对齐
}
2.3.2 主轴方向(direction)
FlexDirection 枚举有四个值:
| 枚举值 | 主轴方向 | 排列效果 |
|---|---|---|
FlexDirection.Row |
水平从左到右 | → → → |
FlexDirection.RowReverse |
水平从右到左 | ← ← ← |
FlexDirection.Column |
垂直从上到下 | ↓ ↓ ↓ |
FlexDirection.ColumnReverse |
垂直从下到上 | ↑ ↑ ↑ |
对于瀑布流场景,我们使用 Row 方向,即水平排列。
2.3.3 换行模式(wrap)
FlexWrap 枚举有三个值:
| 枚举值 | 行为 | 说明 |
|---|---|---|
FlexWrap.NoWrap |
不换行 | 所有子项排在同一行,可能会被压缩 |
FlexWrap.Wrap |
允许换行 | 排满一行后自动折行到下一行 |
FlexWrap.WrapReverse |
反向换行 | 换行后交叉轴方向反转 |
瀑布流最关键的就是 FlexWrap.Wrap。只有开启了换行,当第一行的卡片总宽度超过容器宽度时,后续卡片才会「流」到下一行,从而形成多行布局。
2.3.4 主轴对齐(justifyContent)
FlexAlign 枚举控制主轴方向上的对齐方式:
| 枚举值 | 效果 | 适用场景 |
|---|---|---|
FlexAlign.Start |
起始对齐 | 默认,左对齐 |
FlexAlign.Center |
居中对齐 | 居中排列 |
FlexAlign.End |
末尾对齐 | 右对齐 |
FlexAlign.SpaceBetween |
两端对齐 | 两列瀑布流,均匀分布 |
FlexAlign.SpaceAround |
环绕间距 | 每个子项两侧间距相等 |
FlexAlign.SpaceEvenly |
均匀间距 | 子项之间、首尾与容器之间间距相等 |
注意 :当 wrap: FlexWrap.Wrap 生效时,如果一行中实际只有一列(例如屏幕很宽但卡片也宽),SpaceBetween 会将子项推到两端,可能不是你想要的效果。但在两列场景下,SpaceBetween 是最佳选择。
2.3.5 交叉轴对齐(alignItems vs alignContent)
这两个属性容易混淆,它们的区别如下:
| 属性 | 控制对象 | 生效条件 | 类比 |
|---|---|---|---|
alignItems |
单行内子项在交叉轴上的对齐 | 始终生效 | 行内对齐 |
alignContent |
多行作为一个整体在交叉轴上的分布 | 需要 wrap: Wrap |
行间分布 |
如果 wrap: NoWrap,则只有 alignItems 生效,alignContent 会被忽略。
在我们的瀑布流场景中:
alignContent: FlexAlign.Start:多行从顶部开始排列(这是默认且合理的行为)alignItems可以不设置(默认是ItemAlign.Start),让每一行内的卡片顶部对齐------这正是我们想要的效果
2.4 State 管理与 UI 刷新
ArkTS 提供了多种装饰器来实现状态管理:
| 装饰器 | 作用范围 | 数据流向 | 说明 |
|---|---|---|---|
@State |
当前组件 | 单向(自身) | 最基本的响应式状态 |
@Prop |
父→子 | 单向(父→子) | 子组件接收父组件传参 |
@Link |
父子双向 | 双向 | 父子组件共享状态 |
@Provide / @Consume |
跨层级 | 单向/双向 | 祖先与后代传递状态 |
@StorageLink |
应用全局 | 双向 | 与应用级存储同步 |
@LocalStorageLink |
页面级 | 双向 | 与页面级存储同步 |
在我们的瀑布流示例中:
- 页面级状态
items使用@State管理 - 卡片组件的
item使用@Prop接收父组件传入的数据
之所以卡片不能用 private 声明属性,是因为 ArkTS 的编译要求所有从父组件接收数据的属性必须用装饰器显式标注,否则编译器无法生成正确的更新逻辑。
三、FlexWrap 瀑布流的实现原理
3.1 核心设计思想
在深入代码之前,我们先理清一个关键问题:为什么 FlexWrap 能做出瀑布流效果?
传统瀑布流(Masonry)的算法复杂度在于:
- 需要维护 N 列各自当前的高度
- 每次新插入一个卡片时,选择当前高度最低的那一列
- 将卡片放置在该列底部,并更新该列高度
- 不断重复上述过程
这是一个典型的「贪心算法」,每次都将新卡片放在最矮的列上,以保证各列高度最终尽可能均衡。
而我们的 FlexWrap 方案绕过了这个复杂算法,它利用了一个更简单的原理:
- 所有卡片按从左到右、从上到下的顺序依次排列
- 每张卡片的宽度固定(48%,即两列布局)
- 由于卡片高度各不相同,在第一行中,A 列和 B 列的卡片高度就已经产生了差异
- 当排列到第二行时,第 3 张卡片自然紧接着第 1 张卡片的下方(而不是对齐第 2 张的底部)
- 这种差异在视觉上就形成了「瀑布流」的错落效果
让我们用一个更直观的例子来说明:
第一行:
┌─────────────┐ ┌─────────────┐
│ 卡片 1 │ │ 卡片 2 │
│ (高 80vp) │ │ (高 160vp) │
│ │ │ │
│ │ │ │
└─────────────┘ │ │
│ │
└─────────────┘
第二行:
┌─────────────┐ ┌─────────────┐
│ 卡片 3 │ │ 卡片 4 │
│ (高 120vp) │ │ (高 100vp) │
│ │ │ │
└─────────────┘ └─────────────┘
注意看:卡片 1 高度只有 80vp,卡片 2 高达 160vp,因此卡片 3 在第二行左列(卡片 1 下方)与卡片 2 形成了一段「高度差」。这个高度差就是瀑布流「错落感」的来源。
3.2 与严格 Masonry 的对比
| 对比维度 | FlexWrap 瀑布流 | 严格 Masonry |
|---|---|---|
| 算法复杂度 | O(1) | O(N)(遍历所有列找最矮) |
| 代码量 | 约 30 行核心逻辑 | 需独立维护列高追踪逻辑 |
| 视觉效果 | 同一行顶部对齐,行间错落 | 每列独立,各行不对齐 |
| 列数调整 | 修改卡片宽度百分比 | 修改列追踪逻辑 |
| 数据顺序 | 严格按给定顺序排列 | 优化排列使列均衡 |
| 适用场景 | 标签云、简短摘要卡片 | 图片流、长内容卡片 |
简单来说:FlexWrap 方案是「轻量级瀑布流」,视觉上够用,实现上极简。对于大多数信息流场景,它的效果已经足够好。
3.3 为什么选择 Flex 而不是 List 或 Grid
有些开发者可能会问:为什么不使用 List 或 Grid 组件实现瀑布流?
| 容器 | 可行性 | 复杂度 | 推荐度 |
|---|---|---|---|
List |
标准 List 只能单列排列,瀑布流不可用 | --- | ❌ |
Grid |
Grid 支持网格,但自定义高度跨度较复杂 | 高 | ⚠️ |
Flex |
原生支持换行,结合不定高实现错落 | 低 | ✅ |
实际上,鸿蒙的 Grid 组件确实可以通过 GridItem 的 rowStart / rowEnd 实现类似 masonry 的效果,但需要配合 JS 计算出每个卡片应该跨越的行数,实现复杂度和 FlexWrap 不是一个量级。
因此,Flex 是实现瀑布流雏形的最佳起点。
四、项目搭建与环境准备
4.1 创建 HarmonyOS 工程
在 DevEco Studio 中创建新工程:
- File → New → Create Project
- 选择模板:Empty Ability
- 配置项目:
- Project Name:
WaterfallDemo(可自定义) - Bundle Name:
com.example.waterfalldemo - Save Location:自定义
- Compile SDK:选择 API 24
- Model:Stage(HarmonyOS NEXT 推荐模式)
- Language:ArkTS
- Project Name:
4.2 项目目录结构
WaterfallDemo/
├── AppScope/
│ └── app.json5 # 应用级别配置
├── entry/
│ ├── build-profile.json5 # 模块构建配置
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability 生命周期
│ │ │ └── pages/
│ │ │ ├── Index.ets # 首页(导航入口)
│ │ │ └── WaterfallFlex.ets # ★ 瀑布流布局演示页
│ │ ├── module.json5 # 模块清单文件
│ │ └── resources/
│ │ └── base/profile/
│ │ └── main_pages.json # 路由注册
│ └── oh-package.json5
├── build-profile.json5 # 项目级别构建配置
├── hvigorfile.ts
└── oh-package.json5
4.3 页面路由注册
在 entry/src/main/resources/base/profile/main_pages.json 中注册两个页面:
json
{
"src": [
"pages/Index",
"pages/WaterfallFlex"
]
}
这样,首页 Index.ets 即可通过 router.pushUrl({ url: 'pages/WaterfallFlex' }) 跳转到瀑布流页面。
4.4 编译运行
在 DevEco Studio 中:
- 连接真机或启动模拟器
- 点击运行按钮(绿色三角)或使用快捷键
Shift + F10 - 等待编译完成,应用自动安装并启动
也可以使用命令行编译:
bash
hvigorw assembleApp --no-daemon
编译产物位于 entry/build/default/outputs/default/ 目录下。
五、核心代码逐段精讲
这一章是整篇文章的核心。我们将逐段、逐行地拆解 WaterfallFlex.ets 的每一处设计决策和实现细节。
5.1 数据模型 ------ WaterfallItem 接口
typescript
/**
* 瀑布流数据模型
* 每个卡片包含:标题、描述、高度比例、颜色
*/
interface WaterfallItem {
id: number;
title: string; // 卡片标题
desc: string; // 卡片描述文字
heightRatio: number; // ★ 高度权重(1.0 ~ 2.0),数值越大卡片越高
color: string; // 卡片主色调(十六进制色值,如 '#FFB7C5')
}
设计要点:
id:唯一标识符,在ForEach中作为key使用,帮助框架高效识别哪个 item 被修改/删除/新增heightRatio:制造瀑布流错落感的核心参数 。每张卡片图片区的高度为80 × heightRatio,取值范围 1.0 到 1.9 之间。1.0 的卡片只有 80vp 高,而 1.9 的卡片高达 152vp,几乎是前者的两倍color:使用十六进制字符串而非Color枚举,因为 14 张卡片需要 14 种不同的颜色,Color枚举只有有限几个预定义颜色
为什么不把 heightRatio 直接改为具体高度值(如 imageHeight: number)?
使用「比例值」的语义更清晰------它表达了「这张卡片相对于最矮卡片的高度倍数」。如果要统一调整所有卡片的高度基准值,只需修改公式中的
80这个基数即可,而不需要逐一修改每个卡片的数据。
5.2 数据源 ------ @State 装饰的数据数组
typescript
@State private items: WaterfallItem[] = [
{ id: 1, title: '春日樱花', desc: '粉色的花瓣随风飘落,铺满整条小径。',
heightRatio: 1.2, color: '#FFB7C5' },
{ id: 2, title: '夏日海滩', desc: '阳光、沙滩、海浪,还有冰镇的椰子汁。',
heightRatio: 1.6, color: '#87CEEB' },
{ id: 3, title: '秋日枫叶', desc: '满山红叶层林尽染,秋意正浓时。',
heightRatio: 1.0, color: '#FF8C42' },
{ id: 4, title: '冬日雪景', desc: '白雪皑皑覆盖大地,世界变得安静。',
heightRatio: 1.8, color: '#B0C4DE' },
{ id: 5, title: '星空物语', desc: '浩瀚银河横跨天际,繁星点点闪烁。',
heightRatio: 1.4, color: '#6A5ACD' },
{ id: 6, title: '森林秘境', desc: '古木参天,藤蔓缠绕,阳光透过叶隙洒下。',
heightRatio: 1.1, color: '#6B8E23' },
{ id: 7, title: '城市夜景', desc: '华灯初上,车水马龙,霓虹照亮不夜城。',
heightRatio: 1.7, color: '#2F4F4F' },
{ id: 8, title: '花海田园', desc: '薰衣草田一望无际,微风吹过紫色波浪。',
heightRatio: 1.3, color: '#9370DB' },
{ id: 9, title: '极光之舞', desc: '绿色光带在夜空中流转,如梦似幻。',
heightRatio: 1.9, color: '#00CED1' },
{ id: 10, title: '山水墨韵', desc: '远山如黛,近水含烟,泼墨山水画中游。',
heightRatio: 1.5, color: '#708090' },
{ id: 11, title: '落日余晖', desc: '夕阳将天空染成金红色,海面波光粼粼。',
heightRatio: 1.2, color: '#FF6347' },
{ id: 12, title: '雨后天晴', desc: '彩虹横跨天际,空气中弥漫着泥土的清香。',
heightRatio: 1.6, color: '#48D1CC' },
{ id: 13, title: '古镇小巷', desc: '青石板路蜿蜒曲折,古色古香的建筑诉说着历史。',
heightRatio: 1.0, color: '#BC8F8F' },
{ id: 14, title: '云雾山巅', desc: '云海翻涌如浪,山峰若隐若现宛如仙境。',
heightRatio: 1.8, color: '#9ACD32' },
];
数据设计思路:
- 14 个卡片,14 种颜色:涵盖粉色、蓝色、橙色、紫色、绿色、青色、灰色等,色彩丰富,视觉上不单调
- 高度比例分布:1.0(矮)×2、1.1 ×1、1.2 ×2、1.3 ×1、1.4 ×1、1.5 ×1、1.6 ×2、1.7 ×1、1.8 ×2、1.9 ×1。既有矮卡片也有高卡片,错落效果明显
- 主题统一:每个卡片都是一幅「风景主题」,配对应的 emoji 图标,增强阅读体验
@State的作用 :如果未来需要动态加载更多数据(如下拉刷新加载新卡片),只需向items数组追加数据,UI 会自动更新
5.3 卡片子组件 ------ WaterfallCard
将卡片抽取为独立组件是软件工程中「关注点分离」原则的体现。我们来看完整代码:
typescript
/**
* 卡片子组件:独立渲染一张瀑布流卡片
* 将卡片抽成独立 @Component,以便在图片区直接嵌入 Emoji 文字,
* 避免在 Row 上使用 .overlay(Text(...)) 导致的类型不兼容问题。
*/
@Component
struct WaterfallCard {
/**
* @Prop 装饰器:父组件传参到此子组件,数据为单向同步(父→子)。
* 必须显式标记为 @Prop,不能使用 private ------ 否则编译报错
* "Property has no initializer" 且无法通过构造函数初始化。
*/
@Prop item: WaterfallItem;
build() {
// 卡片容器:Column 垂直排布,固定宽度 48%(两列布局)
Column() {
// ---- 图片占位区(带颜色的圆角方块 + 居中 Emoji) ----
Row() {
// 在色块中央显示主题 emoji 图标
Text(this.getItemEmoji(this.item.title))
.fontSize(32)
.fontColor('#FFFFFF')
}
.width('100%')
// ★ 关键:高度 = 基础值 × heightRatio,每个卡片高度不同,产生错落感
.height(80 * this.item.heightRatio)
.borderRadius(12)
.backgroundColor(this.item.color)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
// ---- 标题文字 ----
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 4 })
// ---- 描述文字(最多 2 行,超长省略号) ----
Text(this.item.desc)
.fontSize(13)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.lineHeight(18)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('48%')
.backgroundColor('#FAFAFA')
.borderRadius(14)
.padding(10)
.margin({ bottom: 12 })
.shadow({
radius: 6,
offsetX: 0,
offsetY: 2,
color: 'rgba(0, 0, 0, 0.08)'
})
}
/**
* 根据标题返回对应的 emoji 图标
*/
private getItemEmoji(title: string): string {
const emojiMap: Record<string, string> = {
'春日樱花': '🌸',
'夏日海滩': '🏖️',
'秋日枫叶': '🍁',
'冬日雪景': '❄️',
'星空物语': '🌌',
'森林秘境': '🌲',
'城市夜景': '🌃',
'花海田园': '🌸',
'极光之舞': '🌠',
'山水墨韵': '⛰️',
'落日余晖': '🌅',
'雨后天晴': '🌈',
'古镇小巷': '🏘️',
'云雾山巅': '☁️',
};
return emojiMap[title] ?? '📸';
}
}
布局层级示意:
Column (.width('48%'), 圆角白色背景 + 阴影)
├── Row (.height = 80 × heightRatio, 彩色背景, 圆角) ← 模拟图片
│ └── Text (Emoji 图标, 白色, 32号字, 居中)
├── Text (标题, 16号字, 左对齐)
└── Text (描述, 13号字, 灰色, 最多2行)
每个属性的作用:
.width('48%'):控制列宽。48% 意味着两列分别占 48% + 48% = 96%,剩下的 4% 成为两列之间的间隙。注意这里没有用50%,因为如果两列各占 50%,加上justifyContent: SpaceBetween后,它们会紧密贴合两侧边缘,中间留出很大间隙------视觉效果不如 48% 紧凑.height(80 * this.item.heightRatio):瀑布流核心公式。80 是基础高度(vp 单位),乘以比例因子后产生差异化高度.borderRadius(12/14):内层图片 12vp 圆角,外层卡片 14vp 圆角------外层稍大,形成完美的双层圆角嵌套.shadow(...):卡片阴影参数分别为:阴影模糊半径 6vp,水平偏移 0,垂直偏移 2vp,颜色为半透明黑色。效果柔和,不太过夸张.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis }):描述文字最多两行,超出部分以省略号截断。保证卡片高度不会因为文字过长而失控
5.4 核心 Flex 容器
这是整个瀑布流布局的「心脏」:
typescript
Flex({
direction: FlexDirection.Row, // 主轴为水平方向
wrap: FlexWrap.Wrap, // ★ 允许换行 ------ 瀑布流的关键
justifyContent: FlexAlign.SpaceBetween, // 两列两端对齐,均匀分布
alignContent: FlexAlign.Start, // 多行从顶部开始排列
}) {
ForEach(this.items, (item: WaterfallItem) => {
WaterfallCard({ item: item })
}, (item: WaterfallItem) => item.id.toString())
}
.width('100%')
.padding({ left: 12, right: 12 }) // 左右留 12vp 安全边距
执行流程:
ForEach遍历items数组,依次创建WaterfallCard组件- 每张卡片宽度
48%,两张一排占96% - Flex 容器水平排列卡片,当第二张卡片放完时,一行已满
- 第三张卡片自动换行到第二行
SpaceBetween将两张卡片推向左右两侧,中间留出均匀间距- 由于不同卡片的
heightRatio不同,每张卡片的高度不一致 - 第二行的起始位置对齐第一行的「下一行起点」,但由于第一行左右两卡片高度不同,第二行左列卡片的上边缘与右列的上边缘形成了「视觉错位」
关键认知:
FlexWrap.Wrap的换行逻辑是:当前行放不下下一个子项时,就新起一行,新行的起始位置在交叉轴方向上紧挨着上一行。这个「紧挨」是指新行顶部对齐上一行底部,但因为上一行左右卡片高度不同,新行的左右卡片自然就「错开」了。
5.5 页面外壳 ------ Scroll + Column
typescript
Scroll() {
Column({ space: 12 }) {
// ---------- 页面标题 ----------
Text('🌊 瀑布流布局(FlexWrap 实现)')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 16, bottom: 4 })
// ---------- 说明文字 ----------
Text('利用 Flex + FlexWrap 换行 + 不定高卡片,自然形成错落瀑布流效果')
.fontSize(13)
.fontColor('#888888')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ bottom: 8 })
// ---------- ★ 核心 ------ Flex 容器 ----------
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap,
justifyContent: FlexAlign.SpaceBetween,
alignContent: FlexAlign.Start,
}) {
ForEach(this.items, (item: WaterfallItem) => {
WaterfallCard({ item: item })
}, (item: WaterfallItem) => item.id.toString())
}
.width('100%')
.padding({ left: 12, right: 12 })
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F0F0F0') // 浅灰背景,柔和护眼
结构分析:
Scroll(纵向滚动,100% × 100%)
└── Column(space = 12vp)
├── Text(主标题)
├── Text(副标题/说明)
└── Flex(★ 瀑布流核心容器)
├── WaterfallCard(卡片 1)
├── WaterfallCard(卡片 2)
├── WaterfallCard(卡片 3)
└── ...(直至卡片 14)
为什么需要 Scroll?
如果去掉
Scroll,当瀑布流内容总高度超出屏幕高度时,超出部分将被裁剪,用户将无法看到下方的卡片。Scroll提供了纵向滚动的能力,这是任何内容列表类页面都需要的标配组件。
为什么 Column 的 space 是 12?
space: 12控制 Column 内各子组件(标题区与 Flex 区)之间的垂直间距,12vp 在鸿蒙 UI 中是一个标准的舒适间距值。
5.6 首页导航页面
typescript
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
build() {
// Column + Blank 实现垂直居中,替代 RelativeContainer + alignRules
Column() {
// 顶部弹性占位
Blank()
// ---- 主标题 ----
Text('鸿蒙 ArkTS 布局示例')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('100%')
// ---- 副标题 ----
Text('Flex 实现瀑布流布局雏形')
.fontSize(16)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 8, bottom: 40 })
// ---- 进入瀑布流按钮 ----
Button('🏞️ 查看瀑布流效果')
.width(220)
.height(48)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.backgroundColor('#FF6B6B')
.borderRadius(24)
.onClick(() => {
router.pushUrl({ url: 'pages/WaterfallFlex' });
})
// 底部弹性占位
Blank()
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
设计要点:
Blank():Flex 布局中的弹性空白组件,它会占据剩余空间。上下两个Blank()将中间的内容「推」到垂直中央.justifyContent(FlexAlign.Center)+.alignItems(HorizontalAlign.Center):Column 的主轴是垂直方向,justifyContent: Center使内容在垂直方向居中;alignItems: Center使内容在水平方向居中- 按钮样式 :红色圆角按钮(
#FF6B6B),圆角 24vp(高度 48vp 的一半,形成胶囊形状),220vp 宽度,视觉上突出且友好 router.pushUrl:标准页面跳转 API。弃用警告是由于新版 API 调整路由接口所致,不影响功能
六、完整可运行代码
为了方便读者直接复制使用,以下提供两个文件的完整代码。
6.1 WaterfallFlex.ets
typescript
/*
* 鸿蒙原生 ArkTS 布局方式之 Flex 实现瀑布流布局雏形
*
* 核心思路:
* 利用 Flex 容器的 FlexWrap.Wrap 换行能力 + 每个 item 高度不一致,
* 让子项在水平排列时自然出现「参差不齐」的错落效果,
* 形成视觉上的瀑布流(Waterfall)雏形。
*
* 布局要点:
* 1. Flex 容器:direction = FlexDirection.Row(主轴水平)
* wrap = FlexWrap.Wrap(允许换行)
* 2. 每个卡片 item 固定宽度(如 48%),高度随机变化
* 3. 配合 justifyContent: FlexAlign.SpaceBetween 让两列分布均匀
* 4. 卡片内使用 Column 垂直布局
*/
interface WaterfallItem {
id: number;
title: string;
desc: string;
heightRatio: number;
color: string;
}
@Component
struct WaterfallCard {
@Prop item: WaterfallItem;
build() {
Column() {
Row() {
Text(this.getItemEmoji(this.item.title))
.fontSize(32)
.fontColor('#FFFFFF')
}
.width('100%')
.height(80 * this.item.heightRatio)
.borderRadius(12)
.backgroundColor(this.item.color)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 6 })
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 4 })
Text(this.item.desc)
.fontSize(13)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.lineHeight(18)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('48%')
.backgroundColor('#FAFAFA')
.borderRadius(14)
.padding(10)
.margin({ bottom: 12 })
.shadow({
radius: 6,
offsetX: 0,
offsetY: 2,
color: 'rgba(0, 0, 0, 0.08)'
})
}
private getItemEmoji(title: string): string {
const emojiMap: Record<string, string> = {
'春日樱花': '🌸',
'夏日海滩': '🏖️',
'秋日枫叶': '🍁',
'冬日雪景': '❄️',
'星空物语': '🌌',
'森林秘境': '🌲',
'城市夜景': '🌃',
'花海田园': '🌸',
'极光之舞': '🌠',
'山水墨韵': '⛰️',
'落日余晖': '🌅',
'雨后天晴': '🌈',
'古镇小巷': '🏘️',
'云雾山巅': '☁️',
};
return emojiMap[title] ?? '📸';
}
}
@Entry
@Component
struct WaterfallFlex {
@State private items: WaterfallItem[] = [
{ id: 1, title: '春日樱花',
desc: '粉色的花瓣随风飘落,铺满整条小径。',
heightRatio: 1.2, color: '#FFB7C5' },
{ id: 2, title: '夏日海滩',
desc: '阳光、沙滩、海浪,还有冰镇的椰子汁。',
heightRatio: 1.6, color: '#87CEEB' },
{ id: 3, title: '秋日枫叶',
desc: '满山红叶层林尽染,秋意正浓时。',
heightRatio: 1.0, color: '#FF8C42' },
{ id: 4, title: '冬日雪景',
desc: '白雪皑皑覆盖大地,世界变得安静。',
heightRatio: 1.8, color: '#B0C4DE' },
{ id: 5, title: '星空物语',
desc: '浩瀚银河横跨天际,繁星点点闪烁。',
heightRatio: 1.4, color: '#6A5ACD' },
{ id: 6, title: '森林秘境',
desc: '古木参天,藤蔓缠绕,阳光透过叶隙洒下。',
heightRatio: 1.1, color: '#6B8E23' },
{ id: 7, title: '城市夜景',
desc: '华灯初上,车水马龙,霓虹照亮不夜城。',
heightRatio: 1.7, color: '#2F4F4F' },
{ id: 8, title: '花海田园',
desc: '薰衣草田一望无际,微风吹过紫色波浪。',
heightRatio: 1.3, color: '#9370DB' },
{ id: 9, title: '极光之舞',
desc: '绿色光带在夜空中流转,如梦似幻。',
heightRatio: 1.9, color: '#00CED1' },
{ id: 10, title: '山水墨韵',
desc: '远山如黛,近水含烟,泼墨山水画中游。',
heightRatio: 1.5, color: '#708090' },
{ id: 11, title: '落日余晖',
desc: '夕阳将天空染成金红色,海面波光粼粼。',
heightRatio: 1.2, color: '#FF6347' },
{ id: 12, title: '雨后天晴',
desc: '彩虹横跨天际,空气中弥漫着泥土的清香。',
heightRatio: 1.6, color: '#48D1CC' },
{ id: 13, title: '古镇小巷',
desc: '青石板路蜿蜒曲折,古色古香的建筑诉说着历史。',
heightRatio: 1.0, color: '#BC8F8F' },
{ id: 14, title: '云雾山巅',
desc: '云海翻涌如浪,山峰若隐若现宛如仙境。',
heightRatio: 1.8, color: '#9ACD32' },
];
build() {
Scroll() {
Column({ space: 12 }) {
Text('🌊 瀑布流布局(FlexWrap 实现)')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 16, bottom: 4 })
Text('利用 Flex + FlexWrap 换行 + 不定高卡片,自然形成错落瀑布流效果')
.fontSize(13)
.fontColor('#888888')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ bottom: 8 })
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap,
justifyContent: FlexAlign.SpaceBetween,
alignContent: FlexAlign.Start,
}) {
ForEach(this.items, (item: WaterfallItem) => {
WaterfallCard({ item: item })
}, (item: WaterfallItem) => item.id.toString())
}
.width('100%')
.padding({ left: 12, right: 12 })
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F0F0F0')
}
}
6.2 Index.ets
typescript
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
build() {
Column() {
Blank()
Text('鸿蒙 ArkTS 布局示例')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('100%')
Text('Flex 实现瀑布流布局雏形')
.fontSize(16)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 8, bottom: 40 })
Button('🏞️ 查看瀑布流效果')
.width(220)
.height(48)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.backgroundColor('#FF6B6B')
.borderRadius(24)
.onClick(() => {
router.pushUrl({ url: 'pages/WaterfallFlex' });
})
Blank()
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
6.3 main_pages.json
json
{
"src": [
"pages/Index",
"pages/WaterfallFlex"
]
}
七、编译踩坑与修复记录
在开发这个示例项目时,我们遇到了几个典型的 ArkTS 编译错误。这些经验对其他鸿蒙开发者同样有参考价值,因此详细记录下来。
7.1 alignRules 类型不匹配
错误等级:❌ ERROR
完整错误信息:
ArkTS Compiler Error
No overload matches this call.
Overload 1 of 2, '(value: AlignRuleOption): TextAttribute',
gave the following error.
Type 'HorizontalAlign' is not assignable to type 'VerticalAlign'.
错误代码:
typescript
RelativeContainer() {
Text('标题')
.alignRules({
center: { anchor: '__container__', align: HorizontalAlign.Center },
middle: { anchor: '__container__', align: VerticalAlign.Top }
})
}
产生原因:
在较早版本的 HarmonyOS SDK 中,AlignRule 接口的 align 字段类型是 VerticalAlign | HorizontalAlign,即两者皆可。但在 API 24 版本中,AlignRuleOption 接口的类型定义发生了变化------align 字段的期望类型随着属性 key 的不同而分化,导致 HorizontalAlign 和 VerticalAlign 不能混用。
解决方案:
放弃使用 RelativeContainer + alignRules,改用 Column + Blank + justifyContent + alignItems 实现居中布局。新的方案代码量更少,跨版本兼容性更好,且更容易理解和维护。
typescript
// 替代方案:Column + Blank 弹性布局
Column() {
Blank()
Text('标题')
Button('按钮')
Blank()
}
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
7.2 overlay() 参数类型错误
错误等级:❌ ERROR
完整错误信息:
ArkTS Compiler Error
Argument of type 'TextAttribute' is not assignable to
parameter of type 'string | CustomBuilder | ComponentContent<Object>'.
Type 'TextAttribute' is missing the following properties
from type 'ComponentContent<Object>': update, recycle, dispose, ...
错误代码:
typescript
Row()
.width('100%')
.height(80 * item.heightRatio)
.overlay(
Text('🌸')
.fontSize(32)
.fontColor('#FFFFFF')
)
产生原因:
.overlay() 方法接受两类参数:要么是一个普通字符串(如 '🌸'),要么是一个 CustomBuilder 构建函数。而直接链式调用的 Text('🌸').fontSize(32) 返回的是 TextAttribute 类型------它既不是字符串也不是 CustomBuilder,所以类型检查不通过。
解决方案:
将 emoji 图标从 .overlay() 改为直接作为 Row 的子组件嵌套:
typescript
Row() {
Text('🌸')
.fontSize(32)
.fontColor('#FFFFFF')
}
.width('100%')
.height(80 * item.heightRatio)
这样既实现了「居中显示图标」的效果,又避免了类型问题。同时将卡片抽为独立 WaterfallCard 组件,结构更清晰。
7.3 子组件 @Prop 声明问题
错误等级:❌ ERROR(实际报了两个相关错误)
完整错误信息:
Error 1: Property 'item' has no initializer and is not
definitely assigned in the constructor.
Error 2: Property 'item' is private and can not be
initialized through the component constructor.
错误代码:
typescript
@Component
struct WaterfallCard {
private item: WaterfallItem; // ❌ 编译错误
// ...
}
产生原因:
在 ArkTS 中,子组件通过构造函数接收父组件传参时,编译器要求:
- 属性必须有初始值,或者使用
!非空断言(definite assignment assertion) - 属性不能声明为
private,因为构造函数初始化需要外部访问 - 更重要的是,必须使用装饰器(
@Prop/@Link/@State等)显式标记属性的响应式角色
解决方案:
typescript
@Component
struct WaterfallCard {
@Prop item: WaterfallItem; // ✅ @Prop 声明,父→子单向数据同步
// ...
}
@Prop 的作用:
- 告诉编译器:这是一个从父组件接收的输入属性
- 父组件数据变化时,子组件自动刷新
- 子组件内部不能修改
@Prop属性(遵守单向数据流原则)
延伸理解 :ArkTS 的装饰器不仅是「标记」,它们同时承担了「代码生成」的功能。编译器在看到 @Prop 时,会生成相应的属性更新逻辑。如果仅用 private 声明,编译器无法生成这些逻辑,自然就会报错。
7.4 router.pushUrl 弃用警告
错误等级:⚠️ WARN(不影响编译和运行)
完整警告信息:
ArkTS:WARN File: Index.ets:39:18
'pushUrl' has been deprecated.
产生原因:
在 API 24 版本中,鸿蒙路由框架进行了重构,引入了新的路由接口。旧的 router.pushUrl() 虽然仍然可用,但已被标记为弃用。
解决方案(如果不希望看到警告):
方案一:使用新的路由 API
typescript
import { router } from '@kit.ArkUI';
// 使用新的 pushUrl 接口
router.pushUrl({ url: 'pages/WaterfallFlex' })
.then(() => {
console.info('页面跳转成功');
})
.catch((err: BusinessError) => {
console.error('页面跳转失败: ' + JSON.stringify(err));
});
方案二:改用 Navigation 组件 + NavPathStack
typescript
import { Navigation, NavPathStack } from '@kit.ArkUI';
@Entry
@Component
struct Index {
private stack: NavPathStack = new NavPathStack();
build() {
Navigation(this.stack) {
Button('跳转')
.onClick(() => {
this.stack.pushPath({ name: 'WaterfallFlex' });
})
}
}
}
Navigation 方式是鸿蒙推荐的页面路由方式,支持更丰富的转场动画和参数传递,但在简单场景下 router.pushUrl 仍然是最便捷的选择。
7.5 调试建议
在开发 SparkTS 应用时,如果遇到编译错误,建议按以下步骤排查:
- 查看完整的错误输出 :
hvigorw assembleApp --no-daemon --stacktrace可以看到更详细的错误栈 - 定位到具体行号 :错误信息通常会给出文件路径和行号,如
File: Index.ets:39:18 - 缩小问题范围:注释掉可疑代码块,确认问题是否消失,然后逐步恢复
- 查阅官方 API 文档:鸿蒙开发者官网的 API 文档是最权威的参考
- 留意版本差异:不同 API 版本之间的接口可能有差异,确保参考对应版本的文档
八、FlexWrap 瀑布流的优缺点与优化方向
8.1 优点
- 代码极简:核心瀑布流逻辑仅需约 30 行代码(Flex 配置 + ForEach 遍历),加上卡片子组件也不到 100 行有效代码
- 纯原生 API :完全基于 ArkUI 内置的
Flex、Column、Text等组件,零第三方依赖 - 编译时类型安全:ArkTS 是静态类型语言,所有属性传参都有类型检查,运行时极少出现因类型错误导致的崩溃
- 性能优良:Flex 布局的渲染计算由 ArkUI 引擎层完成,效率远高于 JS 方案
- 响应式:Flex 容器宽度自适应,在不同屏幕尺寸下都能正确排列
- 易于扩展:增加卡片只需要往数据数组中添加新 item,无需修改布局逻辑
8.2 局限性
- 行内顶部对齐:同一行内的卡片顶部是在同一水平线上对齐的,而不是像严格 Masonry 那样每列独立对齐------这意味着同一行中矮卡片的下方会留白,高卡片则填满整行
- 列数硬编码 :当前通过
.width('48%')固定为两列,如果要改为三列或四列,需要手动调整卡片宽度和列高追踪逻辑 - 数据顺序敏感:卡片严格按照数据源中的顺序排列,无法像 Masonry 算法那样智能地将卡片分配到「当前最矮的列」以优化整体平衡
- 无法实现跨列卡片:如果某张卡片需要占据两列宽度(类似 Pinterest 的特色 Pin),FlexWrap 无法直接支持
8.3 优化方向
方向一:支持更多列数
通过计算容器宽度动态设置卡片宽度:
typescript
getCardWidth(containerWidth: number): string {
if (containerWidth >= 600) return '23%'; // 4列
if (containerWidth >= 400) return '31%'; // 3列
return '48%'; // 2列
}
但需要注意,这只是改变了卡片宽度,并未真正实现「列高度均衡」的 Masonry 布局。
方向二:多 Column 严格 Masonry
typescript
// 将数据均衡分配到多列
function distributeToColumns(
items: WaterfallItem[],
columnCount: number
): WaterfallItem[][] {
const columns: WaterfallItem[][] = Array.from(
{ length: columnCount }, () => []
);
const heights: number[] = new Array(columnCount).fill(0);
for (const item of items) {
// 找到当前最矮的那一列
const minHeight = Math.min(...heights);
const targetCol = heights.indexOf(minHeight);
columns[targetCol].push(item);
// 估算该卡片高度,累加到该列
heights[targetCol] += 80 * item.heightRatio + 100 + 12;
}
return columns;
}
然后渲染 N 个 Column:
typescript
Row() {
ForEach(this.columns, (column: WaterfallItem[], colIndex: number) => {
Column() {
ForEach(column, (item: WaterfallItem) => {
WaterfallCard({ item: item })
}, (item: WaterfallItem) => item.id.toString())
}
.width(1 / this.columnCount * 100 + '%')
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
方向三:使用 LazyForEach 实现性能优化
当数据量庞大时(数百甚至上千条),ForEach 会一次性渲染所有卡片,造成性能问题。应替换为 LazyForEach,实现按需渲染:
typescript
class WaterfallDataSource extends BasicDataSource<WaterfallItem> {
// 实现数据源接口 ...
}
@Entry
@Component
struct WaterfallFlex {
private dataSource: WaterfallDataSource = new WaterfallDataSource();
build() {
LazyForEach(this.dataSource, (item: WaterfallItem) => {
WaterfallCard({ item: item })
}, (item: WaterfallItem) => item.id.toString())
}
}
LazyForEach 只渲染当前可见区域内的卡片,当用户滚动时动态创建和回收组件,内存占用大幅降低。
九、延伸思考
9.1 ArkTS 布局的未来演进
随着 HarmonyOS NEXT 的持续演进,ArkUI 框架也在不断丰富布局能力。从社区动态和官方路标来看,以下几个方向值得关注:
- 更丰富的 Flex API :未来可能提供
gap属性(类似于 CSS 的gap)来替代复杂的SpaceBetween+margin组合 - 原生 Masonry 容器 :有推测说鸿蒙团队正在考虑加入原生的瀑布流容器组件,类似于 Flutter 的
MasonryGridView - 布局动画增强 :
transition和animateTo的配合越来越流畅,使得卡片增删、排序的过渡动画可以轻松实现
9.2 声明式 UI 的思考
使用 ArkTS 进行布局开发时,一个值得深思的问题是:声明式 UI 到底给我们带来了什么?
在传统的命令式 UI 中:你需要手动创建视图对象,设置属性,添加到父视图,处理布局变更通知......每一步都需要明确的代码指令。
在声明式 UI 中:你只需要描述 UI 的最终形态,框架负责推断从当前形态到目标形态的变换路径。这种思维方式的转变带来的好处:
- 减少 boilerplate 代码 :不需要写
create、setLayoutParams、addView等重复代码 - 状态驱动 UI :UI 是状态的纯函数,
UI = f(state),减少不一致性 - 更少的 bug:状态变化的中间步骤由框架管理,开发者不需要担心忘记更新某个视图
- 更好的可读性 :
build()方法中的 UI 树结构一目了然
9.3 社区资源推荐
学习和深入鸿蒙 ArkTS 布局开发,以下资源值得收藏:
- HarmonyOS 开发者官网 --- 官方文档,最权威的参考
- ArkUI 组件参考 --- 所有原生组件的 API 文档
- ArkTS 语言指南 --- 语言特性详述
- 鸿蒙开发者论坛 --- 社区问答与经验分享
十、总结
10.1 本文要点回顾
本文通过一个完整的鸿蒙 ArkTS 示例项目,详细讲解了如何利用 Flex + FlexWrap.Wrap 实现瀑布流布局雏形。以下是核心知识点的回顾:
-
Flex 布局核心:
direction: FlexDirection.Row--- 主轴水平排列wrap: FlexWrap.Wrap--- 允许换行,这是瀑布流的关键justifyContent: FlexAlign.SpaceBetween--- 两列均匀分布
-
不定高卡片设计:
- 通过
heightRatio参数控制每张卡片的高度 - 高度差异化是瀑布流错落感的来源
- 卡片内使用 Column 垂直排列:图片区 → 标题 → 描述
- 通过
-
组件化封装:
WaterfallCard子组件通过@Prop接收数据- 独立的
getItemEmoji()方法管理 Emoji 映射
-
避坑指南:
alignRules中的 HorizontalAlign / VerticalAlign 不能混用.overlay()不接受链式的 Text 组件private属性不能用于接收父组件传参,必须用@Prop
10.2 下一步学习建议
如果你已经掌握了本文的内容,建议继续探索:
- 将数据源改为网络请求:从 REST API 或 GraphQL 获取真实数据
- 添加图片加载 :用
Image组件替代彩色方块,加载真实图片 - 实现下拉刷新 + 上拉加载 :结合
@ohos.animator或社区组件实现 - 尝试严格 Masonry:基于第九章的多 Column 方案实现真正的列高度均衡
- 加入点击事件:点击卡片跳转到详情页,或弹出模态框
10.3 写在最后
鸿蒙原生应用开发正处于高速发展期,ArkTS 作为主力语言,其声明式 UI 框架的设计哲学与现代前端框架(React、SwiftUI、Flutter 等)一脉相承,但又融合了鸿蒙独有的特性和优化。
瀑布流布局只是 ArkTS 布局能力的一个小小缩影。Flex容器的灵活性和表现力远超本文所展示的内容------FlexDirection.Column + FlexWrap.Wrap 可以实现类似 iOS 的「标签流布局」;justifyContent.SpaceEvenly 可以轻松实现「完美居中排列」;alignItems.Stretch 可以让同一行内的卡片等高排列......
希望本文能够帮助你在鸿蒙原生开发的道路上少走一些弯路。布局是 UI 开发的地基,打好地基,才能盖出漂亮的应用大楼。
如果你在实践过程中遇到任何问题,或者有更好的实现方案,欢迎留言交流。让我们共同推动鸿蒙开发者社区的发展!
附录
A. 完整项目文件清单
WaterfallDemo/
├── AppScope/
│ └── app.json5
├── entry/
│ ├── build-profile.json5
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets
│ │ │ └── pages/
│ │ │ ├── Index.ets
│ │ │ └── WaterfallFlex.ets
│ │ ├── module.json5
│ │ └── resources/base/profile/
│ │ └── main_pages.json
│ └── oh-package.json5
├── build-profile.json5
├── hvigorfile.ts
├── hvigor-config.json5
├── oh-package.json5
└── oh-package-lock.json5
B. 常见属性速查表
Flex 容器常用配置组合:
| 用途 | direction | wrap | justifyContent | alignContent |
|---|---|---|---|---|
| 瀑布流(本文) | Row |
Wrap |
SpaceBetween |
Start |
| 标签云 | Row |
Wrap |
Start |
Start |
| 居中换行列 | Row |
Wrap |
Center |
Center |
| 底部导航 | Row |
NoWrap |
SpaceAround |
--- |
| 纵向换行 | Column |
Wrap |
Start |
Start |
卡片圆角搭配建议:
| 卡片外层圆角 | 内部子组件圆角 | 效果 |
|---|---|---|
| 14vp | 12vp | 完美嵌套(外层略大) |
| 16vp | 12vp | 嵌套留边明显 |
| 12vp | 12vp | 可能产生锯齿 |
| 0 | 8vp | 只有内部圆角 |
颜色搭配参考(背景色与卡片色):
| 整体背景色 | 卡片背景色 | 效果 |
|---|---|---|
#F0F0F0 |
#FAFAFA |
柔和护眼(本文采用) |
#FFFFFF |
#F5F5F5 |
明亮简洁 |
#1A1A2E |
#16213E |
深色模式 |
#F8F9FA |
#FFFFFF |
高对比度 |
C. 参考文档
本文为鸿蒙原生应用开发系列教程之一,配套代码已通过 hvigorw assembleApp 编译验证(API 24 / SDK 6.1.0),在 DevEco Studio 中可直接编译运行。文中所有代码均遵循 Apache 2.0 开源协议,欢迎自由使用、修改和分享。
如果您觉得本文有帮助,欢迎点赞、收藏和转发,让更多鸿蒙开发者受益。如有问题或建议,请留言讨论。