请添加图片描述

前言

在上一期的实战中,我们掌握了沉浸式光感的毛玻璃(Glass Morphism)UI 设计。今天,我们将迎来鸿蒙 ArkUI 复杂列表布局的“终极武器”——高性能动态列数瀑布流 (WaterFlow)

在电商推荐、资讯流或图片展示等场景中,传统的 Grid 布局往往因为“行高等高”的缺陷,导致高度不一的卡片底部产生大量空白。而鸿蒙提供的 WaterFlow 容器,配合 LazyForEach 懒加载机制,不仅能实现真正的紧凑排列,还能轻松应对海量数据的流畅渲染。

这个实战案例结合了 ArkTS 的高性能渲染与动态交互能力,涵盖了以下核心知识点:

  • WaterFlow 核心算法:利用列优先填充策略,实现高度不一卡片的无缝紧凑排列。
  • LazyForEach 懒加载:配合自定义数据源,实现海量数据下的按需渲染与流畅滚动。
  • 动态列数切换:实时改变 columnsTemplate,实现从单列到多列的平滑布局切换。
  • 上拉加载更多:结合滚动监听与异步数据加载,打造无限滚动的瀑布流体验。

下面,我们就对这段实现高性能瀑布流的代码进行一次深度解析。


完整代码结构预览

首先,让我们从整体上把握代码结构。它定义了一个 WaterFlowDemo 入口组件,核心是自定义数据源的管理、瀑布流容器的渲染以及控制面板的动态交互。

import { CustomWaterFlow, WaterFlowDataSource, WaterFlowItem } from '../components/WaterFlow'

@Entry
@Component
struct WaterFlowDemo {
  // 1. 状态与数据源定义
  @State columns: number = 2
  @State gap: number = 12
  @State totalCount: number = 0
  @State isLoading: boolean = false
  private dataSource: WaterFlowDataSource = new WaterFlowDataSource()

  // 2. 数据加载与生成逻辑
  aboutToAppear() { ... }
  private loadInitialData(): void { ... }
  private loadMoreData(): void { ... }
  private generateItems(startIndex: number, count: number): WaterFlowItem[] { ... }

  // 3. 页面主体与自定义构建
  build() { ... }
  @Builder Header() { ... }
  @Builder ControlPanel() { ... }
  @Builder ColumnButton(count: number) { ... }
}

第一部分:高性能数据源与懒加载机制 (WaterFlowDataSource)

在鸿蒙 ArkTS 中,处理长列表或瀑布流时,直接使用 ForEach 会导致所有子项一次性渲染,极易引发卡顿。本案例引入了自定义的 WaterFlowDataSource 配合 LazyForEach 来解决这一问题。

private dataSource: WaterFlowDataSource = new WaterFlowDataSource()

private loadInitialData(): void {
  const items = this.generateItems(0, 50)
  this.dataSource.replaceData(items) // 初始化替换数据
  this.totalCount = 50
}

private loadMoreData(): void {
  if (this.isLoading) return
  this.isLoading = true

  setTimeout(() => {
    const items = this.generateItems(this.totalCount, 20)
    this.dataSource.addData(items) // 异步追加数据
    this.totalCount += 20
    this.isLoading = false
  }, 1000)
}
  • 数据源驱动WaterFlowDataSource 充当了 UI 与原始数据之间的桥梁。它内部维护了数据的索引和状态,能够精确告知 WaterFlow 当前需要渲染哪些子项。
  • 增量更新:在 loadMoreData 中,我们通过 addData 方法向数据源尾部追加新数据。LazyForEach 监听到数据源的变化后,只会创建新增的 20 个 FlowItem,而不会重新渲染之前的 50 个,从而保证了极致的滚动性能。
  • 随机高度模拟:在 generateItems 中,我们为每个卡片生成了 Math.floor(Math.random() * 150) + 100 的随机高度,完美模拟了真实业务中卡片内容长短不一的场景。

第二部分:WaterFlow 核心布局与动态列数

WaterFlow 区别于 Grid 的核心在于其底层算法:它按照列优先的顺序填充子项,每列独立计算累积高度,并将下一个子项自动放入当前最短的那一列。

Scroll() {
  CustomWaterFlow({
    dataSource: this.dataSource,
    columns: this.columns, // 动态绑定的列数
    gap: this.gap,         // 动态绑定的间距
    onLoadMore: () => this.loadMoreData() // 触底加载更多回调
  })
  .width('100%')
  .padding({ left: 16, right: 16, top: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
  • 动态列数切换columns 是一个 @State 变量。当用户在控制面板点击不同的列数按钮时,CustomWaterFlow 内部会动态更新 columnsTemplate(例如从 '1fr 1fr' 变为 '1fr 1fr 1fr 1fr')。WaterFlow 容器会自动重新计算所有卡片的排列位置,实现布局的无缝切换。
  • 间距动态调节gap 同样绑定状态,用户拖动滑块时,卡片之间的 columnsGaprowsGap 会实时响应,方便开发者调试最佳的视觉呼吸感。

第三部分:毛玻璃控制面板与交互反馈

为了让这个技术演示更具现代感,我们在页面顶部嵌入了一个带有毛玻璃效果的控制面板。

@Builder ControlPanel() {
  Column({ space: 16 }) {
    // 列数切换按钮组
    Row({ space: 8 }) {
      this.ColumnButton(1)
      this.ColumnButton(2)
      this.ColumnButton(3)
      this.ColumnButton(4)
    }

    // 间距调节滑块
    Slider({ value: this.gap, min: 4, max: 24, style: SliderStyle.OutSet })
      .onChange((value: number) => { this.gap = value })
  }
  .backdropBlur(20)
  .backgroundColor('rgba(255,255,255,0.06)')
  .borderColor('rgba(255,255,255,0.1)')
}
  • 状态联动ColumnButton 通过对比 this.columns === count 来动态切换按钮的高亮状态和字体颜色。点击按钮时,直接修改 @State 变量 columns,触发 UI 重渲染。
  • 视觉统一:控制面板沿用了上一期实战中的毛玻璃(Glass Morphism)设计规范,使用 backdropBlur 和半透明边框,使其悬浮在深色背景之上,既保证了操作的便捷性,又不破坏整体界面的沉浸感。

完整代码

import { CustomWaterFlow, WaterFlowDataSource, WaterFlowItem } from '../components/WaterFlow'



struct WaterFlowDemo {
   columns: number = 2
   gap: number = 12
   totalCount: number = 0
   isLoading: boolean = false

  private dataSource: WaterFlowDataSource = new WaterFlowDataSource()

  aboutToAppear() {
    this.loadInitialData()
  }

  private loadInitialData(): void {
    const items = this.generateItems(0, 50)
    this.dataSource.replaceData(items)
    this.totalCount = 50
  }

  private loadMoreData(): void {
    if (this.isLoading) return
    this.isLoading = true

    setTimeout(() => {
      const items = this.generateItems(this.totalCount, 20)
      this.dataSource.addData(items)
      this.totalCount += 20
      this.isLoading = false
    }, 1000)
  }

  private generateItems(startIndex: number, count: number): WaterFlowItem[] {
    const colors = ['#667EEA', '#F093FB', '#4FACFE', '#43E97B', '#FA709A', '#FECA57', '#95E1D3', '#FF9FF3']
    const contents = ['瀑布流', '高性能', '自定义布局', '响应式', '列平衡', '可变高度', 'Grid', 'Flex', 'Layout', 'Waterfall', 'Flow', 'Masonry', 'Lazy', 'Virtual', 'Scroll']

    const items: WaterFlowItem[] = []
    for (let i = 0; i < count; i++) {
      const index = startIndex + i
      items.push({
        id: `item_${index}`,
        height: Math.floor(Math.random() * 150) + 100,
        content: contents[index % contents.length],
        color: colors[index % colors.length]
      })
    }
    return items
  }

  build() {
    Column() {
      this.Header()

      this.ControlPanel()

      Scroll() {
        CustomWaterFlow({
          dataSource: this.dataSource,
          columns: this.columns,
          gap: this.gap,
          onLoadMore: () => this.loadMoreData()
        })
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
  }

   Header() {
    Column({ space: 8 }) {
      Text('瀑布流布局')
        .fontSize(32)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')

      Text(`高性能 LazyForEach · 已加载 ${this.totalCount} 项`)
        .fontSize(16)
        .fontColor('#FFFFFF')
        .opacity(0.7)
    }
    .width('100%')
    .padding({ top: 60, bottom: 20, left: 16, right: 16 })
    .alignItems(HorizontalAlign.Center)
  }

   ControlPanel() {
    Column({ space: 16 }) {
      Row() {
        Text('列数:')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .opacity(0.8)

        Blank()

        Row({ space: 8 }) {
          this.ColumnButton(1)
          this.ColumnButton(2)
          this.ColumnButton(3)
          this.ColumnButton(4)
        }
      }
      .width('100%')

      Row() {
        Text('间距:')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .opacity(0.8)

        Blank()

        Slider({
          value: this.gap,
          min: 4,
          max: 24,
          style: SliderStyle.OutSet
        })
          .layoutWeight(1)
          .trackThickness(4)
          .selectedColor('#667EEA')
          .blockColor('#FFFFFF')
          .onChange((value: number) => {
            this.gap = value
          })
      }
      .width('100%')
    }
    .width('90%')
    .padding(16)
    .backdropBlur(20)
    .backgroundColor('rgba(255,255,255,0.06)')
    .borderWidth(1)
    .borderColor('rgba(255,255,255,0.1)')
    .borderRadius(16)
    .margin({ bottom: 16 })
  }

   ColumnButton(count: number) {
    Button() {
      Text(count.toString())
        .fontSize(14)
        .fontColor(this.columns === count ? '#667EEA' : '#FFFFFF')
        .fontWeight(this.columns === count ? FontWeight.Bold : FontWeight.Normal)
    }
    .width(40)
    .height(40)
    .backgroundColor(this.columns === count ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.08)')
    .borderRadius(12)
    .onClick(() => {
      this.columns = count
    })
  }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结与实战建议

通过这个高性能瀑布流的实战,我们掌握了以下 ArkTS 高阶列表渲染技能:

  1. WaterFlow vs Grid 的本质区别:深刻理解了 WaterFlow 的列优先填充算法,它是解决“参差不齐卡片布局”的唯一正解,彻底告别了 Grid 带来的底部留白问题。
  2. LazyForEach 的性能优势:在处理超过 50 项以上的长列表时,必须使用数据源驱动的 LazyForEach 替代 ForEach,以实现组件复用和按需渲染。
  3. 动态布局的平滑切换:通过将 columnsTemplate@State 变量绑定,我们可以轻松实现类似新闻客户端“单列/双列/三列”视图切换的高级功能。
  4. 无限滚动的实现范式:结合 Scroll 容器、onLoadMore 回调以及异步数据追加,构建了一套标准的海量数据加载流程。

希望这篇详细的代码解析能帮你彻底掌握鸿蒙 ArkTS 的瀑布流布局与高性能渲染技巧!如果你觉得有用,欢迎点赞、收藏,我们下期再见!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐