鸿蒙原生 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 本文目标

通过本文,你将学到:

  1. 鸿蒙 ArkTS 中 Flex 容器的核心属性与工作原理
  2. FlexWrap.Wrap 换行机制的原理与使用方法
  3. 如何通过「固定宽度 + 不定高卡片 + 自动换行」模拟瀑布流布局
  4. 完整的项目代码逐段拆解与设计思路
  5. 组件化封装的最佳实践(@Component + @Prop
  6. 常见编译错误的排查与修复方法
  7. 从「伪瀑布流」到「严格 Masonry 布局」的进化路线

二、鸿蒙 ArkTS 布局体系全景

2.1 ArkTS 简介

ArkTS 是鸿蒙原生应用的主力开发语言,基于 TypeScript 语法扩展而来,同时兼容 JavaScript 运行时生态。它最大的特点是采用 声明式(Declarative)UI 范式——开发者只需要描述 UI 在任意状态下的「样子」,框架会自动计算出从当前状态到目标状态的最小更新代价。

一个典型的 ArkTS 页面骨架如下:

@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 构造参数
Flex(options?: FlexOptions)

FlexOptions 接口定义:

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)的算法复杂度在于:

  1. 需要维护 N 列各自当前的高度
  2. 每次新插入一个卡片时,选择当前高度最低的那一列
  3. 将卡片放置在该列底部,并更新该列高度
  4. 不断重复上述过程

这是一个典型的「贪心算法」,每次都将新卡片放在最矮的列上,以保证各列高度最终尽可能均衡。

而我们的 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

有些开发者可能会问:为什么不使用 ListGrid 组件实现瀑布流?

容器 可行性 复杂度 推荐度
List 标准 List 只能单列排列,瀑布流不可用
Grid Grid 支持网格,但自定义高度跨度较复杂 ⚠️
Flex 原生支持换行,结合不定高实现错落

实际上,鸿蒙的 Grid 组件确实可以通过 GridItemrowStart / rowEnd 实现类似 masonry 的效果,但需要配合 JS 计算出每个卡片应该跨越的行数,实现复杂度和 FlexWrap 不是一个量级。

因此,Flex 是实现瀑布流雏形的最佳起点


四、项目搭建与环境准备

4.1 创建 HarmonyOS 工程

在 DevEco Studio 中创建新工程:

  1. File → New → Create Project
  2. 选择模板:Empty Ability
  3. 配置项目:
    • Project Name:WaterfallDemo(可自定义)
    • Bundle Name:com.example.waterfalldemo
    • Save Location:自定义
    • Compile SDK:选择 API 24
    • Model:Stage(HarmonyOS NEXT 推荐模式)
    • Language:ArkTS

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 中注册两个页面:

{
  "src": [
    "pages/Index",
    "pages/WaterfallFlex"
  ]
}

这样,首页 Index.ets 即可通过 router.pushUrl({ url: 'pages/WaterfallFlex' }) 跳转到瀑布流页面。

4.4 编译运行

在 DevEco Studio 中:

  1. 连接真机或启动模拟器
  2. 点击运行按钮(绿色三角)或使用快捷键 Shift + F10
  3. 等待编译完成,应用自动安装并启动

也可以使用命令行编译:

hvigorw assembleApp --no-daemon

编译产物位于 entry/build/default/outputs/default/ 目录下。


五、核心代码逐段精讲

这一章是整篇文章的核心。我们将逐段、逐行地拆解 WaterfallFlex.ets 的每一处设计决策和实现细节。

5.1 数据模型 —— WaterfallItem 接口

/**
 * 瀑布流数据模型
 * 每个卡片包含:标题、描述、高度比例、颜色
 */
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 装饰的数据数组

@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' },
];

数据设计思路

  1. 14 个卡片,14 种颜色:涵盖粉色、蓝色、橙色、紫色、绿色、青色、灰色等,色彩丰富,视觉上不单调
  2. 高度比例分布: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。既有矮卡片也有高卡片,错落效果明显
  3. 主题统一:每个卡片都是一幅「风景主题」,配对应的 emoji 图标,增强阅读体验
  4. @State 的作用:如果未来需要动态加载更多数据(如下拉刷新加载新卡片),只需向 items 数组追加数据,UI 会自动更新

5.3 卡片子组件 —— WaterfallCard

将卡片抽取为独立组件是软件工程中「关注点分离」原则的体现。我们来看完整代码:

/**
 * 卡片子组件:独立渲染一张瀑布流卡片
 * 将卡片抽成独立 @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 容器

这是整个瀑布流布局的「心脏」:

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 安全边距

执行流程

  1. ForEach 遍历 items 数组,依次创建 WaterfallCard 组件
  2. 每张卡片宽度 48%,两张一排占 96%
  3. Flex 容器水平排列卡片,当第二张卡片放完时,一行已满
  4. 第三张卡片自动换行到第二行
  5. SpaceBetween 将两张卡片推向左右两侧,中间留出均匀间距
  6. 由于不同卡片的 heightRatio 不同,每张卡片的高度不一致
  7. 第二行的起始位置对齐第一行的「下一行起点」,但由于第一行左右两卡片高度不同,第二行左列卡片的上边缘与右列的上边缘形成了「视觉错位」

关键认知

FlexWrap.Wrap 的换行逻辑是:当前行放不下下一个子项时,就新起一行,新行的起始位置在交叉轴方向上紧挨着上一行。这个「紧挨」是指新行顶部对齐上一行底部,但因为上一行左右卡片高度不同,新行的左右卡片自然就「错开」了。

5.5 页面外壳 —— Scroll + Column

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 首页导航页面

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)
  }
}

设计要点

  1. Blank():Flex 布局中的弹性空白组件,它会占据剩余空间。上下两个 Blank() 将中间的内容「推」到垂直中央
  2. .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center):Column 的主轴是垂直方向,justifyContent: Center 使内容在垂直方向居中;alignItems: Center 使内容在水平方向居中
  3. 按钮样式:红色圆角按钮(#FF6B6B),圆角 24vp(高度 48vp 的一半,形成胶囊形状),220vp 宽度,视觉上突出且友好
  4. router.pushUrl:标准页面跳转 API。弃用警告是由于新版 API 调整路由接口所致,不影响功能

六、完整可运行代码

为了方便读者直接复制使用,以下提供两个文件的完整代码。

6.1 WaterfallFlex.ets

/*
 * 鸿蒙原生 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

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

{
  "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'.

错误代码

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 的不同而分化,导致 HorizontalAlignVerticalAlign 不能混用。

解决方案

放弃使用 RelativeContainer + alignRules,改用 Column + Blank + justifyContent + alignItems 实现居中布局。新的方案代码量更少,跨版本兼容性更好,且更容易理解和维护。

// 替代方案: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, ...

错误代码

Row()
  .width('100%')
  .height(80 * item.heightRatio)
  .overlay(
    Text('🌸')
      .fontSize(32)
      .fontColor('#FFFFFF')
  )

产生原因

.overlay() 方法接受两类参数:要么是一个普通字符串(如 '🌸'),要么是一个 CustomBuilder 构建函数。而直接链式调用的 Text('🌸').fontSize(32) 返回的是 TextAttribute 类型——它既不是字符串也不是 CustomBuilder,所以类型检查不通过。

解决方案

将 emoji 图标从 .overlay() 改为直接作为 Row 的子组件嵌套:

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.

错误代码

@Component
struct WaterfallCard {
  private item: WaterfallItem;  // ❌ 编译错误
  // ...
}

产生原因

在 ArkTS 中,子组件通过构造函数接收父组件传参时,编译器要求:

  1. 属性必须有初始值,或者使用 ! 非空断言(definite assignment assertion)
  2. 属性不能声明为 private,因为构造函数初始化需要外部访问
  3. 更重要的是,必须使用装饰器(@Prop / @Link / @State 等)显式标记属性的响应式角色

解决方案

@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

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

// 使用新的 pushUrl 接口
router.pushUrl({ url: 'pages/WaterfallFlex' })
  .then(() => {
    console.info('页面跳转成功');
  })
  .catch((err: BusinessError) => {
    console.error('页面跳转失败: ' + JSON.stringify(err));
  });

方案二:改用 Navigation 组件 + NavPathStack

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 应用时,如果遇到编译错误,建议按以下步骤排查:

  1. 查看完整的错误输出hvigorw assembleApp --no-daemon --stacktrace 可以看到更详细的错误栈
  2. 定位到具体行号:错误信息通常会给出文件路径和行号,如 File: Index.ets:39:18
  3. 缩小问题范围:注释掉可疑代码块,确认问题是否消失,然后逐步恢复
  4. 查阅官方 API 文档:鸿蒙开发者官网的 API 文档是最权威的参考
  5. 留意版本差异:不同 API 版本之间的接口可能有差异,确保参考对应版本的文档

八、FlexWrap 瀑布流的优缺点与优化方向

8.1 优点

  1. 代码极简:核心瀑布流逻辑仅需约 30 行代码(Flex 配置 + ForEach 遍历),加上卡片子组件也不到 100 行有效代码
  2. 纯原生 API:完全基于 ArkUI 内置的 FlexColumnText 等组件,零第三方依赖
  3. 编译时类型安全:ArkTS 是静态类型语言,所有属性传参都有类型检查,运行时极少出现因类型错误导致的崩溃
  4. 性能优良:Flex 布局的渲染计算由 ArkUI 引擎层完成,效率远高于 JS 方案
  5. 响应式:Flex 容器宽度自适应,在不同屏幕尺寸下都能正确排列
  6. 易于扩展:增加卡片只需要往数据数组中添加新 item,无需修改布局逻辑

8.2 局限性

  1. 行内顶部对齐:同一行内的卡片顶部是在同一水平线上对齐的,而不是像严格 Masonry 那样每列独立对齐——这意味着同一行中矮卡片的下方会留白,高卡片则填满整行
  2. 列数硬编码:当前通过 .width('48%') 固定为两列,如果要改为三列或四列,需要手动调整卡片宽度和列高追踪逻辑
  3. 数据顺序敏感:卡片严格按照数据源中的顺序排列,无法像 Masonry 算法那样智能地将卡片分配到「当前最矮的列」以优化整体平衡
  4. 无法实现跨列卡片:如果某张卡片需要占据两列宽度(类似 Pinterest 的特色 Pin),FlexWrap 无法直接支持

8.3 优化方向

方向一:支持更多列数

通过计算容器宽度动态设置卡片宽度:

getCardWidth(containerWidth: number): string {
  if (containerWidth >= 600) return '23%';  // 4列
  if (containerWidth >= 400) return '31%';  // 3列
  return '48%';                              // 2列
}

但需要注意,这只是改变了卡片宽度,并未真正实现「列高度均衡」的 Masonry 布局。

方向二:多 Column 严格 Masonry
// 将数据均衡分配到多列
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:

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,实现按需渲染:

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 框架也在不断丰富布局能力。从社区动态和官方路标来看,以下几个方向值得关注:

  1. 更丰富的 Flex API:未来可能提供 gap 属性(类似于 CSS 的 gap)来替代复杂的 SpaceBetween + margin 组合
  2. 原生 Masonry 容器:有推测说鸿蒙团队正在考虑加入原生的瀑布流容器组件,类似于 Flutter 的 MasonryGridView
  3. 布局动画增强transitionanimateTo 的配合越来越流畅,使得卡片增删、排序的过渡动画可以轻松实现

9.2 声明式 UI 的思考

使用 ArkTS 进行布局开发时,一个值得深思的问题是:声明式 UI 到底给我们带来了什么?

在传统的命令式 UI 中:你需要手动创建视图对象,设置属性,添加到父视图,处理布局变更通知……每一步都需要明确的代码指令。

在声明式 UI 中:你只需要描述 UI 的最终形态,框架负责推断从当前形态到目标形态的变换路径。这种思维方式的转变带来的好处:

  1. 减少 boilerplate 代码:不需要写 createsetLayoutParamsaddView 等重复代码
  2. 状态驱动 UI:UI 是状态的纯函数,UI = f(state),减少不一致性
  3. 更少的 bug:状态变化的中间步骤由框架管理,开发者不需要担心忘记更新某个视图
  4. 更好的可读性build() 方法中的 UI 树结构一目了然

9.3 社区资源推荐

学习和深入鸿蒙 ArkTS 布局开发,以下资源值得收藏:


十、总结

10.1 本文要点回顾

本文通过一个完整的鸿蒙 ArkTS 示例项目,详细讲解了如何利用 Flex + FlexWrap.Wrap 实现瀑布流布局雏形。以下是核心知识点的回顾:

  1. Flex 布局核心

    • direction: FlexDirection.Row — 主轴水平排列
    • wrap: FlexWrap.Wrap — 允许换行,这是瀑布流的关键
    • justifyContent: FlexAlign.SpaceBetween — 两列均匀分布
  2. 不定高卡片设计

    • 通过 heightRatio 参数控制每张卡片的高度
    • 高度差异化是瀑布流错落感的来源
    • 卡片内使用 Column 垂直排列:图片区 → 标题 → 描述
  3. 组件化封装

    • WaterfallCard 子组件通过 @Prop 接收数据
    • 独立的 getItemEmoji() 方法管理 Emoji 映射
  4. 避坑指南

    • alignRules 中的 HorizontalAlign / VerticalAlign 不能混用
    • .overlay() 不接受链式的 Text 组件
    • private 属性不能用于接收父组件传参,必须用 @Prop

10.2 下一步学习建议

如果你已经掌握了本文的内容,建议继续探索:

  1. 将数据源改为网络请求:从 REST API 或 GraphQL 获取真实数据
  2. 添加图片加载:用 Image 组件替代彩色方块,加载真实图片
  3. 实现下拉刷新 + 上拉加载:结合 @ohos.animator 或社区组件实现
  4. 尝试严格 Masonry:基于第九章的多 Column 方案实现真正的列高度均衡
  5. 加入点击事件:点击卡片跳转到详情页,或弹出模态框

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 开源协议,欢迎自由使用、修改和分享。

如果您觉得本文有帮助,欢迎点赞、收藏和转发,让更多鸿蒙开发者受益。如有问题或建议,请留言讨论。

Logo

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

更多推荐