本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

瀑布流布局是现代移动应用中常见的UI设计模式,尤其在电商、图库、社交媒体等场景中广泛应用。鸿蒙系统通过WaterFlow组件提供了强大的瀑布流实现能力。本文将全面讲解鸿蒙开发中瀑布流的使用方法,包括核心API、常用方法、性能优化技巧。

一、瀑布流概念

1.1 瀑布流布局原理

瀑布流(WaterFlow)是一种非对称的网格布局,其核心特点是:

  • 动态高度:每个子元素(FlowItem)根据内容自动调整高度
  • 智能填充:新元素会自动填充到当前高度最小的列(纵向布局)或行(横向布局)
  • 视觉层次:形成错落有致的视觉效果,提升浏览体验 

在纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。

1.2 WaterFlow组件简介

WaterFlow是鸿蒙ArkUI提供的瀑布流容器组件,主要特点包括:

  • 支持纵向和横向两种布局方向
  • 支持条件渲染、循环渲染和懒加载
  • 提供丰富的布局控制API
  • 内置滚动和触底加载功能
  • 从API Version 9开始支持 

1.3 基本组成元素

一个完整的瀑布流实现通常包含以下部分:

  • WaterFlow容器:承载所有子组件
  • FlowItem子组件:瀑布流中的每一项内容
  • 数据源:通常实现IDataSource接口
  • 懒加载组件:LazyForEach优化性能
  • 布局参数:控制列数、间距等

二、WaterFlow核心API详解

2.1 组件构造方法

WaterFlow组件提供两种构造方式:

// 基础构造
WaterFlow()

// 带参数的构造
WaterFlow(options?: {footer?: CustomBuilder, scroller?: Scroller})

参数说明:

  • footer: 可选的底部构建器,常用于显示"加载更多"状态 
  • scroller: 可滚动组件的控制器,目前仅支持scrollToIndex接口 

示例:

WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {
  // 子组件
}

2.2 布局控制属性

2.2.1 列/行模板
  • columnsTemplate: 设置列数和列宽比例(纵向布局时有效)

    .columnsTemplate('1fr 1fr 1fr') // 三等分
    .columnsTemplate('1fr 2fr')     // 第一列占1/3,第二列占2/3
  • rowsTemplate: 设置行数和行高比例(横向布局时有效)

.rowsTemplate('1fr 1fr 1fr') // 三行等高

支持auto-fill关键字自动填充可用空间: 

.columnsTemplate('repeat(auto-fill, 100vp)') // 自动填充100vp宽的列
2.2.2 间距控制
  • columnsGap: 列间距,默认0

    .columnsGap(10) // 10vp列间距
  • rowsGap: 行间距,默认0

.rowsGap(8) // 8vp行间距

 完整示例:

WaterFlow()
  .columnsTemplate('1fr 1fr')
  .columnsGap(10)
  .rowsGap(8)
  .margin({left: 15, right: 15})
2.2.3 布局方向

layoutDirection控制主轴方向,影响columnsTemplate/rowsTemplate的生效情况:

.layoutDirection(FlexDirection.Column) // 纵向布局(默认)
.layoutDirection(FlexDirection.Row)    // 横向布局

注意:

  • 纵向布局时columnsTemplate有效
  • 横向布局时rowsTemplate有效
  • 未设置时默认为FlexDirection.Column 
2.2.4 尺寸约束

itemConstraintSize可设置子组件的约束尺寸: 

.itemConstraintSize({
  minWidth: 0,
  maxWidth: '100%',
  minHeight: 0,
  maxHeight: '100%'
})

2.3 事件API

2.3.1 触底事件

onReachEnd在滚动到底部时触发,常用于实现无限滚动:

.onReachEnd(() => {
  console.info('onReached')
  setTimeout(() => {
    for(let i = 0; i < 40; i++){
      this.dataSource.addLastItem()
    }
  }, 1000)
})
2.3.2 触顶事件

onReachStart在滚动到顶部时触发:

.onReachStart(() => {
  console.info("onReachStart")
})

三、数据管理与性能优化

3.1 LazyForEach懒加载

LazyForEach是性能优化的关键,它按需创建组件而非一次性加载全部:

LazyForEach(
  dataSource: IDataSource,             // 数据源
  itemGenerator: (item: any, index: number) => void,  // 子组件生成函数
  keyGenerator?: (item: any, index: number) => string // 键值生成函数
)

关键特性:

  • 只在可视区域加载组件
  • 必须实现IDataSource接口
  • 每次迭代只能创建一个子组件
  • 需要提供唯一键值生成器

完整示例: 

LazyForEach(this.dataSource, (item: Item, index) => {
  FlowItem(){
    Column(){
      Text('第' + `${item.index.toString()}` + '个')
      Image(item.image)
        .objectFit(ImageFit.Fill)
    }
    .backgroundColor('#ffa4eaac')
    .borderRadius(8)
  }
}, (item: Item) => item.index.toString())

3.2 IDataSource接口实现

自定义数据源需要实现IDataSource接口:

export class WaterFlowDataSource implements IDataSource {
  private dataArray: Item[] = []
  private listeners: DataChangeListener[] = []

  // 获取数据项
  getData(index: number): Item {
    return this.dataArray[index]
  }

  // 返回数据总数
  totalCount(): number {
    return this.dataArray.length
  }

  // 注册数据变化监听器
  registerDataChangeListener(listener: DataChangeListener): void {
    if(this.listeners.indexOf(listener) < 0){
      this.listeners.push(listener)
    }
  }

  // 注销数据变化监听器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if(pos >= 0){
      this.listeners.splice(pos, 1)
    }
  }

  // 添加新项
  public addLastItem(): void {
    let newItem = this.createNewItem(this.dataArray.length)
    this.dataArray.push(newItem)
    this.notifyDataAdd(this.dataArray.length - 1)
  }

  // 创建新项
  private createNewItem(index: number): Item {
    let randomNumber = getRandomInt(1,6)
    return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)
  }

  // 通知监听器数据变化
  private notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }
}

3.3 无限滚动实现

结合onReachEnd和动态数据加载实现无限滚动:

// 1. 定义footer组件
@Builder itemFoot() {
  Row() {
    LoadingProgress()
      .color(Color.Blue).height(50).aspectRatio(1).width('20%')
    Text('正在加载')
      .fontSize(20)
      .width('30%')
      .height(50)
      .align(Alignment.Center)
      .margin({top: 2})
  }
  .width('100%')
  .justifyContent(FlexAlign.Center)
}

// 2. 在build方法中使用
build() {
  Column() {
    WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {
      LazyForEach(this.dataSource, (item: number) => {
        FlowItem() {
          // 内容
        }
      }, (item: string) => item)
    }
    .onReachEnd(() => {
      setTimeout(() => {
        this.dataSource.addNewItems(10); // 加载10个新项
      }, 1000);
    })
  }
}

// 3. 数据源中添加新项的方法
public addNewItems(count: number): void {
  let len = this.dataArray.length;
  for(let i = 0; i < count; i++) {
    this.dataArray.push(this.dataArray.length);
  }
  this.listeners.forEach(listener => {
    listener.onDatasetChange([{
      type: DataOperationType.ADD, 
      index: len, 
      count: count
    }]);
  })
}

四、实战案例

4.1 瀑布流

完整实现一个电商商品列表,包含图片和标题:

// 商品接口定义
export interface GoodsItem {
  title: string
  imageUrl: string
}

// mock数据
export const mockGoodsList: GoodsItem[] = [
  {
    title: '宁雨昔美式复古字母三条杠圆领短袖T恤女纯棉宽松2024夏季新款学生上衣 白色 M',
    imageUrl: 'https://img10.360buyimg.com/n2/s240x240_jfs/t1/145307/22/41197/71267/65b5d932Fb67b2c27/cd986ae610999146.jpg!q70.jpg.webp'
  },
  // 更多商品...
]

// 主页面实现
@Entry
@Component
struct WaterFlowGoodsPage {
  @State goodsList: GoodsItem[] = mockGoodsList
  @State isLoadMore: boolean = false

  @Builder
  getGoodsItemView(item: GoodsItem, index: number) {
    Column({ space: 5 }) {
      Image(item.imageUrl)
        .height(index % 2 ? 120 : 180) // 交错高度
        .borderRadius(8)
      Text(item.title)
        .fontSize(14)
        .lineHeight(22)
        .maxLines(3)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
  }

  @Builder
  getFooter() {
    Row() {
      Text('加载更多...')
    }
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Pink)
    .height(60)
    .width('100%')
  }

  build() {
    WaterFlow({ footer: this.getFooter }) {
      ForEach(this.goodsList, (item: GoodsItem, index: number) => {
        FlowItem() {
          this.getGoodsItemView(item, index)
        }
      })
    }
    .height('100%')
    .columnsTemplate('1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)
    .onReachEnd(async () => {
      if (!this.isLoadMore) {
        try {
          this.isLoadMore = true
          await this.loadMore()
          this.isLoadMore = false
        } catch (error) {
          promptAction.showToast({ message: JSON.stringify(error) })
        }
      }
    })
  }

  loadMore() {
    return new Promise<boolean>((resolve) => {
      setTimeout(() => {
        this.goodsList.push(...this.goodsList.slice(0, 5)) // 追加5个商品
        resolve(true)
      }, 2000)
    })
  }
}

4.2 图片瀑布流画廊

实现一个图片瀑布流,包含随机高度和点击预览:

@Entry
@Component
struct ImageWaterFlow {
  private scroller: Scroller = new Scroller()
  private dataSource: WaterFlowDataSource = new WaterFlowDataSource()
  
  @Builder
  itemFoot() {
    Row() {
      LoadingProgress()
      Text('加载更多图片...')
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Column() {
      WaterFlow({ footer: this.itemFoot, scroller: this.scroller }) {
        LazyForEach(this.dataSource, (item: Item, index) => {
          FlowItem() {
            Column() {
              Text('第' + `${index + 1}` + '张')
              Image(item.image)
                .objectFit(ImageFit.Fill)
                .sharedTransition(`sharedImage${index}`, { 
                  duration: 300, 
                  curve: Curve.Linear, 
                  delay: 50 
                })
            }
            .backgroundColor('#ffa4eaac')
            .borderRadius(8)
          }
          .onClick(() => {
            router.pushUrl({
              url: 'pages/waterflow/Preview',
              params: { item: item, index: index }
            })
          })
        })
      }
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(8)
      .margin({left: 15, right: 15})
      .onReachEnd(() => {
        setTimeout(() => {
          for(let i = 0; i < 10; i++) {
            this.dataSource.addLastItem()
          }
        }, 1000)
      })
    }
    .margin({top: 20})
  }
}

// 数据源实现
export class WaterFlowDataSource implements IDataSource {
  private dataArray: Item[] = []
  private listeners: DataChangeListener[] = []

  constructor() {
    for(let i = 1; i <= 40; i++) {
      let r = getRandomInt(1, 6)
      this.dataArray.push(new Item($r(`app.media.img_gril_${r}`), i))
    }
  }

  // ...其他必要接口方法实现

  public addLastItem(): void {
    let newItem = this.createNewItem(this.dataArray.length)
    this.dataArray.push(newItem)
    this.notifyDataAdd(this.dataArray.length - 1)
  }

  private createNewItem(index: number): Item {
    let randomNumber = getRandomInt(1,6)
    return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)
  }
}

五、高级技巧

5.1 性能优化策略

  1. 合理设置FlowItem尺寸

    • 避免频繁测量布局
    • 对于已知尺寸的内容,明确设置宽高
    FlowItem()
      .width(this.itemWidthArray[item % 100])
      .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
  2. 使用缓存策略

    • 对于网络图片,实现内存和磁盘缓存
    • 考虑使用ReusableFlowItem复用组件 
  3. 虚拟化长列表

    • 确保使用LazyForEach而非普通ForEach
    • 设置合理的keyGenerator提高复用率
LazyForEach(this.dataSource, (item) => {
  // ...
}, (item) => item.id.toString()) // 唯一键值

5.2 动态布局调整

根据屏幕方向或尺寸变化调整布局:

@State columnsTemplate: string = '1fr 1fr' // 默认两列

// 监听屏幕变化
.onAreaChange((oldValue, newValue) => {
  if(newValue.width > 600) { // 宽屏设备
    this.columnsTemplate = '1fr 1fr 1fr' // 三列
  } else {
    this.columnsTemplate = '1fr 1fr' // 两列
  }
})

// 应用动态模板
WaterFlow()
  .columnsTemplate(this.columnsTemplate)

六、常见问题与解决方案

6.1 布局错乱问题

现象:图片加载后布局跳动或重叠

解决方案

  1. 预计算或固定宽高比
    .aspectRatio(1) // 1:1比例
  2. 使用占位图保持布局稳定
  3. 实现图片加载完成回调后再显示

6.2 滚动卡顿问题

优化建议

  1. 减少FlowItem内部组件复杂度
  2. 避免在FlowItem中使用深层次嵌套
  3. 对于复杂内容,考虑使用自定义绘制代替多个组件

6.3 内存占用过高

控制策略

  1. 实现数据分页加载,不一次性加载所有数据
  2. 监听滚动位置,释放不可见区域的资源
  3. 对于大图,使用合适尺寸的缩略图
Logo

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

更多推荐