ArkTS 瀑布流 WaterFlow 总结

📋 概述

WaterFlow 是 ArkTS 中用于实现瀑布流布局的容器组件。它可以按照列或行的方式排列子组件,自动根据子组件高度或宽度进行布局,常用于图片墙、商品列表、信息流等场景。

主要特点

  • 自适应布局 - 自动计算子项位置,适应不同尺寸
  • 高性能 - 支持懒加载,优化大数据场景
  • 灵活配置 - 支持自定义列数、间距、滚动方向
  • 滚动控制 - 内置滚动控制器,支持滚动到指定位置
  • 多种布局模式 - 支持按列、按行布局

🎯 基本用法

语法结构

WaterFlow(options?: WaterFlowOptions)

interface WaterFlowOptions {
  footer?: CustomBuilder  // 尾部组件
  scroller?: Scroller     // 滚动控制器
}

简单示例

@Entry
@Component
struct WaterFlowExample {
  @State items: number[] = Array.from({ length: 20 }, (_, i) => i)

  build() {
    WaterFlow() {
      ForEach(this.items, (item: number) => {
        FlowItem() {
          Column() {
            Text(`Item ${item}`)
              .fontSize(16)
              .textAlign(TextAlign.Center)
          }
          .width('100%')
          .height(100 + Math.random() * 100) // 随机高度
          .backgroundColor('#E0E0E0')
          .borderRadius(8)
        }
      })
    }
    .columnsTemplate('1fr 1fr') // 2列布局
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)
  }
}

⚙️ 常用属性

1. columnsTemplate - 列模板

columnsTemplate(value: string)

说明: 设置瀑布流的列数和宽度分配方式

示例:

// 2列,等宽
WaterFlow().columnsTemplate("1fr 1fr");

// 3列,等宽
WaterFlow().columnsTemplate("1fr 1fr 1fr");

// 2列,第一列是第二列的2倍宽
WaterFlow().columnsTemplate("2fr 1fr");

// 固定宽度列
WaterFlow().columnsTemplate("100vp 1fr 100vp");

2. rowsTemplate - 行模板

rowsTemplate(value: string)

说明: 设置瀑布流的行数和高度分配方式(用于横向滚动)

示例:

// 2行,等高
WaterFlow().rowsTemplate("1fr 1fr");

// 3行,等高
WaterFlow().rowsTemplate("1fr 1fr 1fr");

3. columnsGap - 列间距

columnsGap(value: Length)

示例:

WaterFlow().columnsGap(10); // 列间距 10vp

WaterFlow().columnsGap(16); // 列间距 16vp

4. rowsGap - 行间距

rowsGap(value: Length)

示例:

WaterFlow().rowsGap(10); // 行间距 10vp

WaterFlow().rowsGap(16); // 行间距 16vp

5. layoutDirection - 布局方向

layoutDirection(value: FlexDirection)

enum FlexDirection {
  Row,           // 主轴为水平方向,子项从左到右排列
  Column,        // 主轴为垂直方向,子项从上到下排列(默认)
  RowReverse,    // 主轴为水平方向,子项从右到左排列
  ColumnReverse  // 主轴为垂直方向,子项从下到上排列
}

示例:

// 垂直滚动(默认)
WaterFlow().layoutDirection(FlexDirection.Column);

// 横向滚动
WaterFlow().layoutDirection(FlexDirection.Row);

6. enableScrollInteraction - 启用滚动交互

enableScrollInteraction(value: boolean)

示例:

WaterFlow().enableScrollInteraction(true); // 允许滚动(默认)

WaterFlow().enableScrollInteraction(false); // 禁用滚动

7. nestedScroll - 嵌套滚动

nestedScroll(value: NestedScrollOptions)

interface NestedScrollOptions {
  scrollForward: NestedScrollMode  // 向前滚动时的嵌套模式
  scrollBackward: NestedScrollMode // 向后滚动时的嵌套模式
}

enum NestedScrollMode {
  SELF_ONLY,   // 只自己滚动
  SELF_FIRST,  // 自己先滚动
  PARENT_FIRST, // 父组件先滚动
  PARALLEL     // 自己和父组件同时滚动
}

示例:

WaterFlow().nestedScroll({
  scrollForward: NestedScrollMode.PARENT_FIRST,
  scrollBackward: NestedScrollMode.SELF_FIRST,
});

8. friction - 滚动摩擦系数

friction(value: number | Resource)

示例:

WaterFlow().friction(0.6); // 默认值 0.6,值越大滚动越慢

9. cachedCount - 缓存数量

cachedCount(value: number)

说明: 设置预加载的子项数量,提升滚动性能

示例:

WaterFlow().cachedCount(2); // 预加载前后各2个子项

🎮 滚动控制器 Scroller

基本用法

@Entry
@Component
struct ScrollerDemo {
  private scroller: Scroller = new Scroller()
  @State items: number[] = Array.from({ length: 50 }, (_, i) => i)

  build() {
    Column() {
      // 控制按钮
      Row() {
        Button('滚动到顶部')
          .onClick(() => {
            this.scroller.scrollEdge(Edge.Top)
          })

        Button('滚动到底部')
          .onClick(() => {
            this.scroller.scrollEdge(Edge.Bottom)
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .padding(10)

      // 瀑布流
      WaterFlow({ scroller: this.scroller }) {
        ForEach(this.items, (item: number) => {
          FlowItem() {
            Text(`Item ${item}`)
              .width('100%')
              .height(80 + Math.random() * 80)
              .backgroundColor('#E0E0E0')
              .textAlign(TextAlign.Center)
              .borderRadius(8)
          }
        })
      }
      .columnsTemplate('1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .padding(10)
      .layoutWeight(1)
    }
    .height('100%')
  }
}

Scroller 常用方法

// 1. 滚动到指定位置
scroller.scrollTo({
  xOffset: 0,
  yOffset: 100,
  animation: { duration: 300, curve: Curve.Smooth },
});

// 2. 滚动到边缘
scroller.scrollEdge(Edge.Top); // 顶部
scroller.scrollEdge(Edge.Bottom); // 底部

// 3. 滚动指定距离
scroller.scrollBy(0, 100); // 向下滚动 100vp

// 4. 滚动到指定索引
scroller.scrollToIndex(10); // 滚动到第10个子项

// 5. 获取当前滚动偏移
let offset = scroller.currentOffset();

🔄 事件回调

1. onReachStart - 到达起始位置

onReachStart(callback: () => void)

示例:

WaterFlow().onReachStart(() => {
  console.log("已到达顶部");
});

2. onReachEnd - 到达结束位置

onReachEnd(callback: () => void)

示例:

WaterFlow().onReachEnd(() => {
  console.log("已到达底部");
  // 可以在这里加载更多数据
  this.loadMore();
});

3. onScrollFrameBegin - 滚动帧开始

onScrollFrameBegin(callback: (offset: number, state: ScrollState) => ScrollFrameResult)

interface ScrollFrameResult {
  offsetRemain: number  // 剩余滚动距离
}

enum ScrollState {
  Idle,   // 空闲状态
  Scroll, // 滚动中
  Fling   // 惯性滚动
}

示例:

WaterFlow().onScrollFrameBegin((offset: number, state: ScrollState) => {
  console.log(`滚动距离: ${offset}, 状态: ${state}`);
  return { offsetRemain: offset };
});

4. onScroll - 滚动时触发

onScroll(callback: (scrollOffset: number, scrollState: ScrollState) => void)

示例:

WaterFlow().onScroll((scrollOffset: number, scrollState: ScrollState) => {
  console.log(`当前滚动偏移: ${scrollOffset}`);
});

💡 实战案例

案例 1:图片瀑布流

interface ImageItem {
  id: number
  url: string
  width: number
  height: number
}

@Entry
@Component
struct ImageWaterFlowDemo {
  @State images: ImageItem[] = [
    { id: 1, url: 'https://example.com/img1.jpg', width: 200, height: 300 },
    { id: 2, url: 'https://example.com/img2.jpg', width: 200, height: 200 },
    { id: 3, url: 'https://example.com/img3.jpg', width: 200, height: 400 },
    { id: 4, url: 'https://example.com/img4.jpg', width: 200, height: 250 },
    { id: 5, url: 'https://example.com/img5.jpg', width: 200, height: 350 },
    { id: 6, url: 'https://example.com/img6.jpg', width: 200, height: 280 }
  ]

  build() {
    WaterFlow() {
      ForEach(this.images, (item: ImageItem) => {
        FlowItem() {
          Column() {
            Image(item.url)
              .width('100%')
              .aspectRatio(item.width / item.height)
              .borderRadius({ topLeft: 8, topRight: 8 })

            Text(`ID: ${item.id}`)
              .fontSize(14)
              .padding(8)
              .width('100%')
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(8)
          .shadow({ radius: 4, color: '#00000020' })
        }
      })
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(12)
    .rowsGap(12)
    .padding(12)
    .backgroundColor('#F5F5F5')
  }
}

案例 2:商品列表(下拉刷新 + 上拉加载)

interface Product {
  id: number
  name: string
  price: number
  image: string
}

@Entry
@Component
struct ProductListDemo {
  private scroller: Scroller = new Scroller()
  @State products: Product[] = []
  @State isLoading: boolean = false
  @State hasMore: boolean = true

  aboutToAppear() {
    this.loadProducts()
  }

  // 加载商品数据
  loadProducts() {
    const newProducts: Product[] = Array.from({ length: 10 }, (_, i) => ({
      id: this.products.length + i,
      name: `商品 ${this.products.length + i + 1}`,
      price: Math.floor(Math.random() * 1000) + 100,
      image: `https://example.com/product${i}.jpg`
    }))
    this.products = [...this.products, ...newProducts]
  }

  // 加载更多
  loadMore() {
    if (this.isLoading || !this.hasMore) return

    this.isLoading = true
    console.log('加载更多...')

    // 模拟网络请求
    setTimeout(() => {
      this.loadProducts()
      this.isLoading = false

      // 假设加载3次后没有更多数据
      if (this.products.length >= 30) {
        this.hasMore = false
      }
    }, 1000)
  }

  @Builder
  ProductCard(product: Product) {
    Column() {
      Image(product.image)
        .width('100%')
        .height(150)
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 8, topRight: 8 })

      Column() {
        Text(product.name)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(`¥${product.price}`)
          .fontSize(16)
          .fontColor('#FF4500')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 4 })
      }
      .width('100%')
      .padding(8)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow({ radius: 4, color: '#00000015' })
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('商品列表')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      // 商品瀑布流
      WaterFlow({ scroller: this.scroller }) {
        ForEach(this.products, (product: Product) => {
          FlowItem() {
            this.ProductCard(product)
          }
        })

        // 加载更多提示
        if (this.isLoading) {
          FlowItem() {
            Row() {
              LoadingProgress()
                .width(24)
                .height(24)
                .margin({ right: 8 })
              Text('加载中...')
                .fontSize(14)
                .fontColor('#999999')
            }
            .width('100%')
            .height(60)
            .justifyContent(FlexAlign.Center)
          }
          .columnStart(0)
          .columnEnd(1) // 跨2列
        }

        // 没有更多数据提示
        if (!this.hasMore) {
          FlowItem() {
            Text('没有更多数据了')
              .fontSize(14)
              .fontColor('#999999')
              .width('100%')
              .height(60)
              .textAlign(TextAlign.Center)
          }
          .columnStart(0)
          .columnEnd(1)
        }
      }
      .columnsTemplate('1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .padding(10)
      .backgroundColor('#F5F5F5')
      .layoutWeight(1)
      .onReachEnd(() => {
        this.loadMore()
      })
    }
    .width('100%')
    .height('100%')
  }
}

案例 3:动态列数(响应式布局)

@Entry
@Component
struct ResponsiveWaterFlowDemo {
  @State items: number[] = Array.from({ length: 30 }, (_, i) => i)
  @State columnsTemplate: string = '1fr 1fr'
  @State currentBreakpoint: string = 'sm'

  // 根据屏幕宽度更新列数
  updateColumnsTemplate(width: number) {
    if (width < 600) {
      this.columnsTemplate = '1fr 1fr' // 小屏:2列
      this.currentBreakpoint = 'sm'
    } else if (width < 900) {
      this.columnsTemplate = '1fr 1fr 1fr' // 中屏:3列
      this.currentBreakpoint = 'md'
    } else {
      this.columnsTemplate = '1fr 1fr 1fr 1fr' // 大屏:4列
      this.currentBreakpoint = 'lg'
    }
  }

  build() {
    Column() {
      // 信息栏
      Text(`当前断点: ${this.currentBreakpoint}`)
        .fontSize(16)
        .padding(16)
        .backgroundColor('#E0E0E0')

      WaterFlow() {
        ForEach(this.items, (item: number) => {
          FlowItem() {
            Column() {
              Text(`Item ${item}`)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)

              Text(`列模板: ${this.columnsTemplate}`)
                .fontSize(12)
                .fontColor('#666666')
                .margin({ top: 4 })
            }
            .width('100%')
            .height(100 + (item % 3) * 50)
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
          }
        })
      }
      .columnsTemplate(this.columnsTemplate)
      .columnsGap(12)
      .rowsGap(12)
      .padding(12)
      .backgroundColor('#F5F5F5')
      .layoutWeight(1)
      .onAreaChange((oldValue: Area, newValue: Area) => {
        const width = Number(newValue.width)
        this.updateColumnsTemplate(width)
      })
    }
    .width('100%')
    .height('100%')
  }
}

案例 4:横向瀑布流

@Entry
@Component
struct HorizontalWaterFlowDemo {
  @State items: number[] = Array.from({ length: 20 }, (_, i) => i)

  build() {
    Column() {
      Text('横向瀑布流')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .padding(16)

      WaterFlow() {
        ForEach(this.items, (item: number) => {
          FlowItem() {
            Column() {
              Text(`Item ${item}`)
                .fontSize(14)
                .textAlign(TextAlign.Center)
            }
            .width(80 + Math.random() * 80) // 随机宽度
            .height('100%')
            .backgroundColor('#E0E0E0')
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
          }
        })
      }
      .rowsTemplate('1fr 1fr 1fr') // 3行
      .rowsGap(10)
      .columnsGap(10)
      .layoutDirection(FlexDirection.Row) // 横向布局
      .padding(10)
      .backgroundColor('#F5F5F5')
      .height(400)
    }
    .width('100%')
    .height('100%')
  }
}

案例 5:带分组的瀑布流

interface GroupData {
  title: string
  items: string[]
}

@Entry
@Component
struct GroupedWaterFlowDemo {
  @State groups: GroupData[] = [
    { title: '分组 A', items: ['A1', 'A2', 'A3', 'A4', 'A5'] },
    { title: '分组 B', items: ['B1', 'B2', 'B3', 'B4'] },
    { title: '分组 C', items: ['C1', 'C2', 'C3', 'C4', 'C5', 'C6'] }
  ]

  @Builder
  GroupHeader(title: string) {
    Text(title)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .width('100%')
      .padding({ left: 16, top: 16, bottom: 8 })
      .backgroundColor('#F5F5F5')
  }

  build() {
    WaterFlow() {
      ForEach(this.groups, (group: GroupData) => {
        // 分组标题
        FlowItem() {
          this.GroupHeader(group.title)
        }
        .columnStart(0)
        .columnEnd(1) // 跨2列

        // 分组内容
        ForEach(group.items, (item: string) => {
          FlowItem() {
            Column() {
              Text(item)
                .fontSize(16)
            }
            .width('100%')
            .height(80 + Math.random() * 60)
            .backgroundColor(Color.White)
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
          }
        })
      })
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)
    .backgroundColor('#F5F5F5')
  }
}

🎯 最佳实践

1. 合理设置列数

// ✅ 推荐:根据屏幕尺寸动态设置列数
@State columnsTemplate: string = '1fr 1fr'

WaterFlow()
  .columnsTemplate(this.columnsTemplate)
  .onAreaChange((oldValue, newValue) => {
    const width = Number(newValue.width)
    if (width < 600) {
      this.columnsTemplate = '1fr 1fr'
    } else if (width < 900) {
      this.columnsTemplate = '1fr 1fr 1fr'
    } else {
      this.columnsTemplate = '1fr 1fr 1fr 1fr'
    }
  })

// ❌ 不推荐:固定列数不适配
WaterFlow()
  .columnsTemplate('1fr 1fr') // 所有屏幕都是2列

2. 使用懒加载优化性能

// ✅ 推荐:使用 LazyForEach 懒加载
class MyDataSource implements IDataSource {
  private dataArray: number[] = []

  public totalCount(): number {
    return this.dataArray.length
  }

  public getData(index: number): number {
    return this.dataArray[index]
  }

  public addData(data: number[]): void {
    this.dataArray = [...this.dataArray, ...data]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    // 实现注册逻辑
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    // 实现注销逻辑
  }
}

@State dataSource: MyDataSource = new MyDataSource()

WaterFlow() {
  LazyForEach(this.dataSource, (item: number) => {
    FlowItem() {
      Text(`Item ${item}`)
    }
  })
}

// ❌ 不推荐:大数据量使用 ForEach(性能差)
WaterFlow() {
  ForEach(this.largeArray, (item) => {
    // 大量数据一次性渲染
  })
}

3. 设置合理的间距

// ✅ 推荐:设置适中的间距
WaterFlow()
  .columnsGap(10) // 10-16vp 适中
  .rowsGap(10);

// ❌ 不推荐:间距过大或过小
WaterFlow()
  .columnsGap(2) // 太小,视觉拥挤
  .rowsGap(50); // 太大,浪费空间

4. 合理使用缓存

// ✅ 推荐:设置合理的缓存数量
WaterFlow().cachedCount(2); // 预加载前后各2个子项

// ❌ 不推荐:缓存过多占用内存
WaterFlow().cachedCount(10); // 缓存过多

5. 上拉加载更多

// ✅ 推荐:使用 onReachEnd 实现加载更多
@State isLoading: boolean = false
@State hasMore: boolean = true

WaterFlow()
  .onReachEnd(() => {
    if (!this.isLoading && this.hasMore) {
      this.loadMore()
    }
  })

loadMore() {
  this.isLoading = true
  // 加载数据
  fetchData().then(newData => {
    this.items = [...this.items, ...newData]
    this.isLoading = false
    this.hasMore = newData.length > 0
  })
}

6. 跨列布局

// ✅ 推荐:标题、提示等元素使用跨列
FlowItem() {
  Text('分组标题')
    .width('100%')
}
.columnStart(0)
.columnEnd(1) // 跨2列(假设总共2列)

// 适用于:
// - 分组标题
// - 加载提示
// - 空状态提示

⚠️ 注意事项

1. FlowItem 必须是直接子组件

// ✅ 正确:FlowItem 是 WaterFlow 的直接子组件
WaterFlow() {
  FlowItem() {
    Text('内容')
  }
}

// ❌ 错误:FlowItem 不是直接子组件
WaterFlow() {
  Column() {
    FlowItem() {  // 错误!
      Text('内容')
    }
  }
}

2. columnsTemplate 和 rowsTemplate 不能同时设置

// ✅ 正确:只设置 columnsTemplate(垂直滚动)
WaterFlow().columnsTemplate("1fr 1fr").layoutDirection(FlexDirection.Column);

// ✅ 正确:只设置 rowsTemplate(横向滚动)
WaterFlow().rowsTemplate("1fr 1fr").layoutDirection(FlexDirection.Row);

// ❌ 错误:同时设置
WaterFlow().columnsTemplate("1fr 1fr").rowsTemplate("1fr 1fr"); // 错误!

3. 子项高度/宽度必须明确

// ✅ 正确:明确设置高度
FlowItem() {
  Column() {
    Text('内容')
  }
  .width('100%')
  .height(150) // 明确高度
}

// ❌ 错误:高度不确定
FlowItem() {
  Column() {
    Text('内容')
  }
  .width('100%')
  // 缺少 height,布局可能异常
}

4. 性能优化

// ✅ 推荐:使用 LazyForEach + 虚拟滚动
WaterFlow() {
  LazyForEach(dataSource, (item) => {
    FlowItem() {
      // 内容
    }
  })
}
.cachedCount(2)

// ❌ 不推荐:ForEach 渲染大量数据
WaterFlow() {
  ForEach(this.largeArray, (item) => {
    // 大量内容一次性渲染
  })
}

5. 状态管理

// ✅ 正确:使用 @State 管理数据
@State items: number[] = []

// ❌ 错误:使用普通变量
private items: number[] = [] // UI 不会响应变化

📊 常见场景配置

场景 columnsTemplate columnsGap rowsGap cachedCount
图片墙(手机) ‘1fr 1fr’ 8-12 8-12 2
图片墙(平板) ‘1fr 1fr 1fr’ 12-16 12-16 3
商品列表 ‘1fr 1fr’ 10 10 2
信息流 ‘1fr’ 0 12 3
横向滚动 - 10 10 2

📚 总结

核心要点

  1. 基本结构

    • WaterFlow 作为容器
    • FlowItem 作为子项(必须是直接子组件)
    • 使用 columnsTemplate 或 rowsTemplate 定义布局
  2. 布局模式

    • 垂直瀑布流:设置 columnsTemplate
    • 横向瀑布流:设置 rowsTemplate + layoutDirection(FlexDirection.Row)
  3. 关键属性

    • columnsTemplate / rowsTemplate - 定义列/行数
    • columnsGap / rowsGap - 设置间距
    • cachedCount - 优化性能
    • enableScrollInteraction - 控制滚动
  4. 滚动控制

    • 使用 Scroller 控制滚动位置
    • onReachEnd 实现加载更多
    • onScroll 监听滚动状态
  5. 性能优化

    • 使用 LazyForEach 懒加载
    • 设置合理的 cachedCount
    • 避免过度渲染
    • 明确子项尺寸
  6. 响应式设计

    • 根据屏幕宽度动态调整列数
    • 使用 onAreaChange 监听尺寸变化
    • 跨列布局用于标题和提示

📖 参考资料


最后更新时间: 2025-10-22

Logo

更多推荐