瀑布流(WaterFlow)是现代移动端最流行的内容展示方式之一。从 Pinterest 的图片流到淘宝的商品发现,从花瓣网的设计灵感到小红书的笔记推荐,瀑布流以"不等高但紧密排列"的独特美感成为视觉驱动型产品的首选布局方案。

HarmonyOS NEXT ArkUI 提供了 WaterFlow 组件,通过 columnsTemplate 定义列数、FlowItem 包裹每个子元素,组件自动根据每列当前高度将新元素插入最短列,形成紧密的瀑布效果。与 Grid 的"等行高网格"不同,WaterFlow 允许每个子元素拥有独立的高度,充分利用垂直空间。

本文将深入讲解 WaterFlow 组件的核心 API,并通过一个"动态商品发现页"实战案例,展示分类筛选、列数切换和收藏交互等功能。

关键词:HarmonyOS、ArkUI、WaterFlow、瀑布流、FlowItem、商品发现、分类筛选

一、WaterFlow 组件概览

WaterFlow 是 ArkUI 容器组件家族中专门用于瀑布流布局的成员。与 Grid(等分网格)、List(线性列表)和 Scroll(自由滚动)相比,WaterFlow 的核心特征在于不等高紧凑排列——每个子元素可以有不同的高度,组件自动将卡片分配到当前累计高度最小的列,避免出现大段空白。

组件 排列方式 高度特征 典型场景
List 单列线性 可变 消息列表、设置选项
Grid 多列等分 统一行高 照片墙、应用抽屉
WaterFlow 多列不等高 每项独立 商品发现、设计灵感
Scroll 自由排布 任意 内容详情页

WaterFlow 的优势在于"灵动"——当你的内容卡片高度差异明显时(如商品主图尺寸不一的电商场景),WaterFlow 能在保持多列的同时避免 Grid 带来的大量空白区域。

1.1 WaterFlow 与 Grid 的核心区别

很多开发者会混淆 WaterFlow 和 Grid。两者的关键区别在于行高处理:

  • Grid:同一行所有元素高度一致,由该行最高元素决定。如果一行中有一个 200vp 的卡片和一个 100vp 的卡片,100vp 的卡片下方会有 100vp 的空白。
  • WaterFlow:不存在"行"的概念。每列独立累加高度,新卡片总是插入当前高度最低的那一列下方。每个卡片紧贴上方内容,无额外空白。

举个直观的例子:假设有 3 列,第 1 列累计 500vp,第 2 列累计 480vp,第 3 列累计 520vp。下一个卡片会插入第 2 列下方(因为 480vp 最小),插入后第 2 列高度变为 480vp + 该卡片高度。

这种"贪心"布局算法是瀑布流视觉效果的来源,也是 WaterFlow 组件名称的由来——内容像瀑布一样自然流动、紧密排列。

二、WaterFlow 核心 API

2.1 构造函数与列模板

WaterFlow 的列定义通过 columnsTemplate 属性控制,语法与 Grid 完全一致:

WaterFlow() {
  ForEach(this.products, (item: ProductItem) => {
    FlowItem() {
      // 每个卡片的 UI
    }
  }, (item: ProductItem) => item.id.toString())
}
.columnsTemplate('1fr 1fr')  // 2 列瀑布流

columnsTemplate 支持以下格式:

  • '1fr 1fr' — 2 列瀑布流(每列 50% 宽度)
  • '1fr 1fr 1fr' — 3 列瀑布流(每列 33.3% 宽度)
  • '1fr 1fr 1fr 1fr' — 4 列瀑布流
  • '200vp 1fr' — 混合宽度(第一列固定 200vp,第二列占剩余)

fr 是 fraction unit(分数单位),1fr 表示"占 1 份可用空间"。3 个 1fr 表示三等分。

2.2 间距控制

WaterFlow() { ... }
  .columnsGap(8)  // 列间距(水平方向)
  .rowsGap(8)     // 行间距(垂直方向——实际上是为每列设置最小间距)

列间距和行间距独立控制。在瀑布流中,"行间距"的含义与 Grid 略有不同——它控制的是同一列中相邻卡片之间的最小距离,而非全局行对齐。

2.3 FlowItem

每个瀑布流单元必须包裹在 FlowItem() 中,这与 Grid 使用 GridItem 类似:

FlowItem() {
  Column() {
    // 卡片内容:图片区域 + 文字信息
    Row() {
      Text('商品图标')
    }
    .width('100%')
    .height(item.imgH)  // 可变高度——瀑布流的关键

    Column() {
      Text(item.title)
      Text(item.shop)
      Text(item.price)
    }
    .padding(10)
  }
  .width('100%')
  .borderRadius(10)
}

FlowItem 提供瀑布流布局所需的尺寸测量能力。与 GridItem 的关键区别在于:FlowItem 的子元素可以拥有任意高度,不会受同一行其他元素影响。

2.4 动态列模板

列数切换是瀑布流的常见需求。通过动态构建模板字符串实现:

buildColTemplate(): string {
  return this.colCount === 2 ? '1fr 1fr' : '1fr 1fr 1fr';
}

colCount 变化时,WaterFlow 自动重新计算每列的累计高度并重新排版所有卡片。

2.5 滚动控制

WaterFlow 内置滚动能力,通常配合 layoutWeight(1) 占据剩余空间:

WaterFlow() { ... }
  .columnsTemplate('1fr 1fr')
  .columnsGap(8)
  .rowsGap(8)
  .layoutWeight(1)          // 占据父容器剩余高度
  .scrollBar(BarState.Off)  // 隐藏滚动条

scrollBar(BarState.Off) 隐藏滚动条以保持界面简洁——在视觉驱动的页面中,滚动条会分散用户注意力。
在这里插入图片描述

三、实战:动态商品发现页

我们构建一个"商品发现"页面,包含 15 件商品卡片、5 个分类筛选标签、2/3 列切换和收藏交互。

3.1 数据模型

每件商品包含标题、店铺、价格、图片高度、图片颜色、分类和收藏状态:

interface ProductItem {
  id: number;
  title: string;    // 商品名称
  shop: string;     // 店铺名称
  price: string;    // 价格
  imgH: number;     // 图片区域高度——瀑布流的核心差异变量
  imgColor: string; // 图片区背景色(模拟商品图)
  category: string; // 分类
  liked: boolean;   // 是否收藏
}

imgH 是瀑布流效果的关键——不同商品使用 100vp 到 200vp 不等的图片高度,使得每张卡片在垂直方向上占据不同空间,从而产生自然的瀑布排列。

数据源包含 15 件商品,覆盖 4 个品类(数码、家居、服饰、美妆),每件商品使用独立的色值(如 #3D5A80 深海蓝、#E09F3E 暖橙金),确保瀑布流在视觉上呈现出丰富的色彩层次:

aboutToAppear(): void {
  const raw: Array<[string, string, string, number, string, string]> = [
    ['无线降噪耳机 Pro', '数码旗舰店', '¥899', 130, '#3D5A80', '数码'],
    ['北欧简约台灯', '家居生活馆', '¥199', 160, '#E09F3E', '家居'],
    ['轻薄羽绒夹克', '潮流服饰店', '¥459', 180, '#2D3436', '服饰'],
    ['保湿精华液 30ml', '美妆专柜', '¥329', 110, '#E17055', '美妆'],
    ['机械键盘 87键', '数码旗舰店', '¥599', 140, '#6C5CE7', '数码'],
    ['纯棉四件套', '家居生活馆', '¥399', 150, '#00B894', '家居'],
    ['复古牛仔外套', '潮流服饰店', '¥589', 200, '#636E72', '服饰'],
    ['防晒霜 SPF50', '美妆专柜', '¥149', 120, '#FD79A8', '美妆'],
    ['便携蓝牙音箱', '数码旗舰店', '¥299', 125, '#0984E3', '数码'],
    ['陶瓷马克杯套装', '家居生活馆', '¥89', 105, '#E8B730', '家居'],
    ['运动跑鞋 Air', '潮流服饰店', '¥699', 170, '#FF5722', '服饰'],
    ['卸妆油 200ml', '美妆专柜', '¥179', 115, '#AAD8E6', '美妆'],
    ['智能手表青春版', '数码旗舰店', '¥1299', 155, '#B79462', '数码'],
    ['香薰蜡烛礼盒', '家居生活馆', '¥129', 100, '#9B59B6', '家居'],
    ['真皮斜挎包', '潮流服饰店', '¥389', 190, '#C0392B', '服饰'],
  ];
  // 转换为 ProductItem 数组
  this.allProducts = [];
  for (let i = 0; i < raw.length; i++) {
    this.allProducts.push({
      id: i,
      title: raw[i][0], shop: raw[i][1], price: raw[i][2],
      imgH: raw[i][3], imgColor: raw[i][4], category: raw[i][5],
      liked: false
    });
  }
  this.filterByCategory('全部');
}

关键设计:15 件商品的 imgH 值在 100 到 200 之间波动,没有两件连续商品的图片高度相同。这种高度差异化确保在 2 列或 3 列布局下都能产生明显的瀑布错落效果。

3.2 分类筛选

5 个分类标签(全部、数码、家居、服饰、美妆)水平排列在可滚动的 Row 中。点击标签时,重新过滤商品列表:

filterByCategory(cat: string): void {
  this.activeCat = cat;
  this.products = [];
  for (let i = 0; i < this.allProducts.length; i++) {
    if (cat === '全部' || this.allProducts[i].category === cat) {
      this.products.push(this.allProducts[i]);
    }
  }
}

筛选逻辑很简单:遍历全量数据,匹配分类名的商品加入显示列表。"全部"类别匹配所有商品。

筛选时重新赋值 this.products 会触发 WaterFlow 重新渲染,卡片重新分配到各列。这个行为是符合预期的——用户切换分类后,展示的是全新的一组商品,瀑布流会重新计算布局。

3.3 列数切换

右上角的图标按钮(☷ 代表 2 列,☰ 代表 3 列)在 2 列和 3 列之间切换:

Text(this.colCount === 2 ? '☷' : '☰')
  .fontSize(20)
  .fontColor('#666677')
  .padding({ left: 10, right: Spacing.LG })
  .onClick(() => { this.colCount = this.colCount === 2 ? 3 : 2; })

列模板方法响应式更新:

buildColTemplate(): string {
  return this.colCount === 2 ? '1fr 1fr' : '1fr 1fr 1fr';
}

切换列数的 UX 考量:

  • 2 列时每个卡片更宽(约占 50% 屏幕宽度),适合快速浏览商品主图和名称
  • 3 列时每个卡片更窄(约占 33%),适合在有限屏幕内展示更多商品
  • 切换时保留当前分类和收藏状态,只改变布局方式

3.4 收藏交互

每张卡片的右下角有一个心形图标(♡ / ♥),点击切换收藏状态:

toggleLike(id: number): void {
  for (let i = 0; i < this.products.length; i++) {
    if (this.products[i].id === id) {
      this.products[i].liked = !this.products[i].liked;
      break;
    }
  }
}

收藏状态通过两个维度传达:

  1. 图标变化:未收藏时显示空心 ♡(#BBBBCC 浅灰),已收藏时显示实心 ♥(#FF4D4F 红色)
  2. 颜色语义:红色心形是全球通用的"喜欢/收藏"视觉符号,用户无需阅读文字即可理解
Text(item.liked ? '♥' : '♡')
  .fontSize(18)
  .fontColor(item.liked ? '#FF4D4F' : '#BBBBCC')
  .onClick(() => { this.toggleLike(item.id); })

这个交互不需要额外的 Toast 或弹窗反馈——图标的即时颜色变化足够直观。

3.5 页面结构

整个页面由 5 层组成:

  1. 标题栏(52vp):深蓝色背景(#1a1a2e),居中显示"商品发现"
  2. 说明卡片:白色圆角卡片,解释 WaterFlow 组件的概念和用法
  3. 分类筛选栏(44vp):水平滚动的标签 + 列数切换按钮
  4. 商品计数:灰色小字显示"X 件商品"
  5. 瀑布流主体(layoutWeight(1)):占据剩余所有空间,展示商品卡片
build() {
  Column() {
    // 1. 标题栏
    Row() {
      Text('商品发现')
        .fontSize(FontSize.HEADLINE)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }
    .width('100%').height(52)
    .padding({ left: Spacing.LG, right: Spacing.LG })
    .backgroundColor('#1a1a2e')

    // 2. 说明卡片
    Column() {
      Text('WaterFlow 组件')
        .fontSize(18).fontColor('#1a1a2e').fontWeight(FontWeight.Bold)
      Text('WaterFlow 是 ArkUI 的瀑布流布局容器...')
        .fontSize(FontSize.CAPTION).fontColor('#666677').lineHeight(20)
    }
    .width('100%').padding(Spacing.LG)
    .backgroundColor('#FFFFFF').borderRadius(BorderRadius.LG)
    .margin({ top: Spacing.MD, bottom: Spacing.SM, left: Spacing.LG, right: Spacing.LG })

    // 3. 分类筛选栏
    Row() {
      Scroll() {
        Row() {
          ForEach(this.categories, (cat: string) => {
            Text(cat)
              .fontSize(13)
              .fontColor(this.activeCat === cat ? '#FFFFFF' : '#666677')
              .fontWeight(this.activeCat === cat ? FontWeight.Bold : FontWeight.Normal)
              .padding({ top: 7, bottom: 7, left: 14, right: 14 })
              .borderRadius(14)
              .backgroundColor(this.activeCat === cat ? '#1677FF' : '#F2F3F5')
              .onClick(() => { this.filterByCategory(cat); })
          }, (cat: string) => cat)
        }
      }
      .scrollBar(BarState.Off).layoutWeight(1)

      Text(this.colCount === 2 ? '☷' : '☰')
        .fontSize(20).fontColor('#666677')
        .onClick(() => { this.colCount = this.colCount === 2 ? 3 : 2; })
    }
    .width('100%').height(44)

    // 4. 商品计数
    Text(this.products.length.toString().concat(' 件商品'))
      .fontSize(11).fontColor('#9999AA')
      .padding({ left: Spacing.LG, bottom: 8 })

    // 5. 瀑布流主体
    WaterFlow() {
      ForEach(this.products, (item: ProductItem) => {
        FlowItem() {
          Column() {
            // 图片区域(可变高度)
            Row() {
              Text(item.title.charAt(0))
                .fontSize(24).fontColor('#FFFFFFAA').fontWeight(FontWeight.Bold)
            }
            .width('100%').height(item.imgH)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(item.imgColor)
            .borderRadius({ topLeft: 10, topRight: 10 })

            // 信息区域
            Column() {
              Text(item.title)
                .fontSize(13).fontColor('#1a1a2e')
                .fontWeight(FontWeight.Medium)
                .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(item.shop)
                .fontSize(11).fontColor('#9999AA')
              Row() {
                Text(item.price)
                  .fontSize(16).fontColor('#FF5722').fontWeight(FontWeight.Bold)
                Blank()
                Text(item.liked ? '♥' : '♡')
                  .fontSize(18)
                  .fontColor(item.liked ? '#FF4D4F' : '#BBBBCC')
                  .onClick(() => { this.toggleLike(item.id); })
              }.width('100%')
            }
            .width('100%')
            .padding({ top: 10, bottom: 10, left: 10, right: 10 })
            .backgroundColor('#FFFFFF')
            .borderRadius({ bottomLeft: 10, bottomRight: 10 })
          }
          .width('100%').borderRadius(10)
          .shadow({ radius: 6, color: '#00000010', offsetX: 0, offsetY: 2 })
        }
      }, (item: ProductItem) => item.id.toString())
    }
    .columnsTemplate(this.buildColTemplate())
    .columnsGap(8).rowsGap(8)
    .layoutWeight(1)
    .padding({ left: Spacing.LG, right: Spacing.LG, bottom: 12 })
    .scrollBar(BarState.Off)
  }
  .width('100%').height('100%')
  .backgroundColor('#F2F3F5')
}

页面整体使用浅灰色背景(#F2F3F5),卡片和说明区域使用白色,形成清晰的内容层次。
在这里插入图片描述

四、瀑布流布局算法深度解析

4.1 WaterFlow 的列分配策略

WaterFlow 组件的内部布局算法可以概括为"贪心最短列策略":

  1. 按数据顺序依次处理每个 FlowItem
  2. 测量当前 FlowItem 的高度
  3. 比较各列的当前累计高度,找到高度最小的列
  4. 将 FlowItem 放入该列,更新该列的累计高度

以下是一个简化的算法示意:

数据:[A:150vp, B:200vp, C:120vp, D:180vp, E:100vp]
列数:2

初始状态:列1=0vp, 列2=0vp

A(150vp) → 列1=0vp ≤ 列2=0vp → 放入列1 → 列1=150vp, 列2=0vp
B(200vp) → 列1=150vp > 列2=0vp → 放入列2 → 列1=150vp, 列2=200vp
C(120vp) → 列1=150vp ≤ 列2=200vp → 放入列1 → 列1=270vp, 列2=200vp
D(180vp) → 列1=270vp > 列2=200vp → 放入列2 → 列1=270vp, 列2=380vp
E(100vp) → 列1=270vp ≤ 列2=380vp → 放入列1 → 列1=370vp, 列2=380vp

最终:列1=[A,C,E], 列2=[B,D]

这个算法保证了两列高度尽可能接近(370vp vs 380vp),避免了某列远长于另一列的不平衡状态。

4.2 数据顺序对布局的影响

由于上述贪心策略是按数据顺序处理的,数据的前后排列会影响最终的视觉效果。如果你希望某些重点商品出现在更显眼的位置(如首屏),应该将它们放在数组的前部。

不过,这种策略也有局限——如果前几个元素恰好都是矮卡片,后续的高卡片会使两列逐渐恢复平衡。WaterFlow 的"自愈"能力使其对数据顺序不太敏感,这对实际应用是件好事。

4.3 Grid 与 WaterFlow 的性能对比

在渲染 15 个卡片的场景下:

  • Grid:需要在每个"行"中确定所有列的最大高度,然后统一设置行高。计算简单,但会产生空白。
  • WaterFlow:需要维护每列的累计高度,每次插入时比较各列高度。计算量略多一些,但不产生空白。

两者的性能差异在 100 项以内完全可以忽略。选择哪一个,取决于你要的是"整齐"还是"紧凑"。

五、为什么选择瀑布流

5.1 视觉密度

瀑布流最直观的优势是视觉密度。对比 2 列 Grid 和 2 列 WaterFlow 展示同样 15 件商品:

  • Grid 方式:每行 2 个卡片,共 8 行(最后一行 1 个),每行高度统一(比如全是 200vp),总高度 1600vp。但如果有一张卡片实际只需要 100vp 空间,其下方会有 100vp 空白。
  • WaterFlow 方式:每列独立累加,两列总高度之和为 15 张卡片的高度之和(约 2200vp),每列平均 1100vp。无任何空白区域。

瀑布流大约能节省 30% 的垂直空间,这对需要展示大量内容的页面至关重要。

5.2 视觉节奏

Grid 的整齐统一带来秩序感,但也容易显得单调。WaterFlow 的错落排列产生了自然的视觉节奏——目光在卡片间流动,不会被固定的行边界束缚。

这种"有组织的不规则"更符合人类自然浏览行为。当你在电商平台"逛"商品时,你不会一行一行扫视——你的目光会跳跃式地移动,被有趣的图片、颜色或价格吸引。WaterFlow 恰好模拟了这种浏览模式。

5.3 适用场景判断

选择 WaterFlow 的最佳场景:

  • 内容包含可变高度的视觉元素(不同比例的图片、不同长度的文案)
  • 以"发现/探索"而非"搜索/查找"为主要用户行为模式
  • 需要营造丰富、充满活力的视觉体验

不用 WaterFlow 的场景:

  • 内容高度统一(如图标网格、应用抽屉)→ 用 Grid
  • 严格的线性顺序很重要(如消息时间线、任务列表)→ 用 List
  • 需要精确的行定位和对齐(如数据表格)→ 用 Grid

六、完整交互流程

  1. 初始状态:进入页面,默认 2 列瀑布流,显示全部 15 件商品,"全部"标签高亮
  2. 分类筛选:点击"数码"标签,列表过滤为 4 件数码商品(耳机、键盘、音箱、手表),瀑布流重新布局
  3. 切换列数:点击 ☰ 图标,变为 3 列瀑布流,每张卡片更窄但展示更多商品
  4. 收藏商品:点击某件商品的心形图标,♡ 变为红色 ♥;再次点击取消收藏
  5. 跨分类收藏:切换到不同分类,收藏状态独立保留

七、进阶扩展方向

本文的 Demo 覆盖了 WaterFlow 的基础操作。在实际项目中,可以进一步扩展以下功能:

7.1 下拉刷新与加载更多

WaterFlow 外层包裹 Refresh 组件实现下拉刷新,监听滚动到底部事件触发分页加载:

Refresh({ refreshing: $$this.isRefreshing }) {
  WaterFlow() { ... }
}
.onRefreshing(() => {
  // 重新加载数据
  this.loadData();
})

7.2 图片懒加载

当前 Demo 用纯色块 + 首字母模拟商品图。在实际项目中,WaterFlow 常配合 Image 组件和懒加载策略——仅加载可视区域内的图片,减少内存占用和流量消耗。

7.3 拖拽排序

WaterFlow 支持通过长按触发拖拽排序,让用户自定义商品在瀑布流中的位置。需要结合 onItemDragStartonItemDrop 等事件。

7.4 混合列宽

对于有"主打商品"需求的场景,可以使用混合列宽:

.columnsTemplate('2fr 1fr 1fr')  // 第一列占 50% 宽度,第二三列各 25%

将重点推广商品放在宽列中,普通商品放在窄列,形成视觉焦点。

八、总结

WaterFlow 是 ArkUI 布局容器体系中的重要一员。它的核心就两个概念:columnsTemplate 控制列宽分配,FlowItem 包裹每个不等高的卡片。掌握了这两个 API,就能在 2 列、3 列之间自由切换,构建出视觉丰富、空间高效的瀑布流页面。

本文通过"商品发现页"这个实战案例,覆盖了:

  1. WaterFlow 与 Grid 的核心区别——不等高紧凑排列 vs 等行高网格
  2. columnsTemplate 的动态构建与列数切换
  3. FlowItem 的使用和可变高度的实现
  4. 分类筛选的数据过滤逻辑
  5. 收藏交互的即时视觉反馈
  6. 瀑布流布局算法的"贪心最短列"策略
  7. 适用场景的判断原则

在 ArkUI 布局体系的水桶中,List 是最长的木板(最常用),Grid 是最整齐的木板(最规整),而 WaterFlow 是最有活力的木板——它让你的内容"流动"起来。


Logo

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

更多推荐