在HarmonyOS应用开发中,瀑布流布局因其灵活性和美观性而广受欢迎。HarmonyOS NEXT 提供了强大的 WaterFlow 组件,可以帮助开发者轻松实现瀑布流布局,并支持多种自定义布局和性能优化特性。本文将通过两个具体场景,详细介绍如何使用 WaterFlow 组件实现页面滑动加载和吸顶效果。

场景一:瀑布流页面多列混排的布局场景

场景描述

在一个瀑布流页面中,不同区域的布局可能有所不同,例如前10个item在2列内布局,中间5个item在1列内撑满宽度布局,后10个item在3列内布局。

实现思路

  1. 计算FlowItem宽/高:动态计算每个item的宽高,确保布局的多样性。
  2. 设置FlowItem的宽/高数组:预设每个item的宽高,以便在布局时使用。
  3. 配置分组信息:通过 sections 配置分组信息,支持不同列数的布局。
  4. 懒加载:在即将触底时提前加载更多数据,确保性能。

核心代码

@Entry
@Component
struct WaterFlowLayout {
  @State dataSource: Array<number> = [];
  @State sections: WaterFlowSections = new WaterFlowSections();
  @State itemWidthArray: Array<number> = [];
  @State itemHeightArray: Array<number> = [];
  @State dataCount: number = 100;
  @State maxSize: number = 200;
  @State minSize: number = 100;
  @State scroller: Scroller = new Scroller();

  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize);
    return (ret > this.minSize ? ret : this.minSize);
  }

  setItemSizeArray() {
    for (let i = 0; i < 100; i++) {
      this.itemWidthArray.push(this.getSize());
      this.itemHeightArray.push(this.getSize());
    }
  }

  aboutToAppear() {
    this.setItemSizeArray();
    let sectionOptions: SectionOptions[] = [];
    let count = 0;
    let oneOrTwo = 0;
    while (count < this.dataCount) {
      if (oneOrTwo++ % 2 == 0) {
        sectionOptions.push({
          itemsCount: 4,
          crossCount: 1,
          columnsGap: 5,
          rowsGap: 10,
          margin: { top: 10, left: 5, bottom: 10, right: 5 },
          onGetItemMainSizeByIndex: (index: number) => 150
        });
        count += 4;
      } else {
        sectionOptions.push({
          itemsCount: 20,
          crossCount: 2,
          onGetItemMainSizeByIndex: (index: number) => this.itemHeightArray[index % 100]
        });
        count += 20;
      }
    }
    this.sections.splice(-1, 0, sectionOptions);
  }

  build() {
    Column() {
      WaterFlow({ scroller: this.scroller, sections: this.sections })
        .height('100%')
        .width('100%')
        .onScrollIndex((first: number, last: number) => {
          if (last + 20 >= this.dataSource.length) {
            for (let i = 0; i < 100; i++) {
              this.dataSource.push(this.dataSource.length);
            }
            let newSection: SectionOptions = {
              itemsCount: 100,
              crossCount: 2,
              onGetItemMainSizeByIndex: (index: number) => this.itemHeightArray[index % 100]
            };
            this.sections.push(newSection);
          }
        })
        .children(LazyForEach(this.dataSource, (item: number) => {
          FlowItem() {
            Column() {
              Image(`./Image/${item % 10}.png`)
                .objectFit(ImageFit.Cover)
                .width('90%')
                .height(100)
                .layoutWeight(1)
                .margin(5);
              Text("必吃榜").fontSize(12).height('16');
            }
          }
          .width('100%')
          .height(this.itemHeightArray[item % 100])
          .backgroundColor(Color.White);
        }, (item: string) => item));
    }
  }
}

场景二:瀑布流页面中某一个Item固定展示在某一个位置

场景描述

在瀑布流页面中,某个Item需要固定展示在某个位置,当页面滑动时,该Item在到达吸顶位置后保持不动,其他内容继续滑动。

实现思路

  1. 预留吸顶部分位置:在第一个分组中剔除特定的Item,为吸顶部分留出位置。
  2. 数据渲染:在数据渲染时剔除特定的Item,确保吸顶部分不被覆盖。
  3. 监听滚动事件:通过 onWillScroll 回调监听滚动事件,根据滚动偏移量调整吸顶部分的位置。

核心代码

@Entry
@Component
struct StickyItemLayout {
  @State dataSource: Array<number> = [];
  @State sections: WaterFlowSections = new WaterFlowSections();
  @State itemWidthArray: Array<number> = [];
  @State itemHeightArray: Array<number> = [];
  @State dataCount: number = 100;
  @State maxSize: number = 200;
  @State minSize: number = 100;
  @State scroller: Scroller = new Scroller();
  @State scrollOffset: number = 0;

  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize);
    return (ret > this.minSize ? ret : this.minSize);
  }

  setItemSizeArray() {
    for (let i = 0; i < 100; i++) {
      this.itemWidthArray.push(this.getSize());
      this.itemHeightArray.push(this.getSize());
    }
  }

  aboutToAppear() {
    this.setItemSizeArray();
    let sectionOptions: SectionOptions[] = [];
    sectionOptions.push({
      itemsCount: 3,
      crossCount: 1,
      columnsGap: 5,
      rowsGap: 10,
      margin: { top: 10, left: 5, bottom: 10, right: 5 },
      onGetItemMainSizeByIndex: (index: number) => {
        if (index == 1) {
          return 100; // 剔除Item=1,为吸顶部分留出位置
        } else {
          return 200;
        }
      }
    });
    this.sections.splice(-1, 0, sectionOptions);
  }

  build() {
    Column() {
      WaterFlow({ scroller: this.scroller, sections: this.sections })
        .height('100%')
        .width('100%')
        .onScrollIndex((first: number, last: number) => {
          if (last + 20 >= this.dataSource.length) {
            for (let i = 0; i < 100; i++) {
              this.dataSource.push(this.dataSource.length);
            }
            let newSection: SectionOptions = {
              itemsCount: 100,
              crossCount: 2,
              onGetItemMainSizeByIndex: (index: number) => this.itemHeightArray[index % 100]
            };
            this.sections.push(newSection);
          }
        })
        .onWillScroll((offset: number) => {
          this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
        })
        .children(LazyForEach(this.dataSource, (item: number) => {
          if (item != 1) {
            FlowItem() {
              Column() {
                Image(`./Image/${item % 10}.png`)
                  .objectFit(ImageFit.Cover)
                  .width('90%')
                  .height(100)
                  .layoutWeight(1)
                  .margin(5);
                Text("必吃榜").fontSize(12).height('16');
              }
            }
            .width('100%')
            .height(this.itemHeightArray[item % 100])
            .backgroundColor(Color.White);
          }
        }, (item: string) => item));

      Stack() {
        Column() {
          // 吸顶部分
          Text("吸顶内容")
            .fontSize(20)
            .fontColor(Color.Black)
            .width('100%')
            .height(100)
            .hitTestBehavior(HitTestMode.Transparent)
            .position({ x: 0, y: this.scrollOffset >= 220 ? 0 : 220 - this.scrollOffset });
        }
      }
      .alignItems(HorizontalAlign.Start)
      .backgroundColor(Color.White);
    }
  }
}