• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 创建瀑布流(WaterFlow)
2
3<!--Kit: ArkUI-->
4<!--Subsystem: ArkUI-->
5<!--Owner: @fangyuhao-->
6<!--Designer: @zcdqs-->
7<!--Tester: @liuzhenshuo-->
8<!--Adviser: @HelloCrease-->
9
10[瀑布流](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)常用于展示图片信息,尤其在购物和资讯类应用中。
11ArkUI提供了WaterFlow容器组件,用于构建瀑布流布局。WaterFlow组件支持条件渲染、循环渲染和懒加载等方式生成子组件。
12
13> **说明:**
14>
15> 本文仅展示关键代码片段,可运行的完整代码请参考[WaterFlow示例代码](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#示例)。
16
17## 布局与约束
18
19瀑布流支持横向和纵向布局。在纵向布局中,可以通过[columnsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#columnstemplate)设置列数;在横向布局中,可以通过[rowsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#rowstemplate)设置行数。
20
21在瀑布流的纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。如下图:
22
23![](figures/waterflow.png)
24
25在瀑布流的横向布局中,每个子节点都会放置在当前总宽度最小的行。若多行总宽度相同,则按照从上到下的顺序进行填充。
26
27![](figures/waterflow-row.png)
28
29## 无限滚动
30
31### 到达末尾时新增数据
32
33瀑布流常用于无限滚动的信息流。可以在瀑布流组件到达末尾位置时触发的[onReachEnd](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#onreachend)事件回调中对[LazyForEach](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)增加新数据,并将footer做成正在加载新数据的样式(使用[LoadingProgress](../reference/apis-arkui/arkui-ts/ts-basic-components-loadingprogress.md)组件)。
34
35```ts
36  @Builder
37  itemFoot() {
38    Row() {
39      LoadingProgress()
40        .color(Color.Blue).height(50).aspectRatio(1).width('20%')
41      Text(`正在加载`)
42        .fontSize(20)
43        .width('30%')
44        .height(50)
45        .align(Alignment.Center)
46        .margin({ top: 2 })
47    }.width('100%').justifyContent(FlexAlign.Center)
48  }
49
50  build() {
51    Column({ space: 2 }) {
52      WaterFlow({ footer: this.itemFoot(), layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
53        LazyForEach(this.dataSource, (item: number) => {
54          FlowItem() {
55            ReusableFlowItem({ item: item })
56          }
57          .width('100%')
58          .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
59          .backgroundColor(this.colors[item % 5])
60        }, (item: string) => item)
61      }
62      .columnsTemplate('1fr '.repeat(this.columns))
63      .backgroundColor(0xFAEEE0)
64      .width('100%')
65      .height('100%')
66      .layoutWeight(1)
67      // 触底加载数据
68      .onReachEnd(() => {
69        setTimeout(() => {
70          this.dataSource.addNewItems();
71        }, 1000);
72      })
73    }
74  }
75
76  // WaterFlowDataSource中增加在数据尾部增加count个元素的方法
77  public addNewItems(count: number): void {
78    let len = this.dataArray.length;
79    for (let i = 0; i < count; i++) {
80      this.dataArray.push(this.dataArray.length);
81    }
82    this.listeners.forEach(listener => {
83      listener.onDatasetChange([{ type: DataOperationType.ADD, index: len, count: count }]);
84    })
85  }
86
87```
88
89在此处应通过在数据末尾添加元素的方式来新增数据,不可直接修改dataArray后通过LazyForEach的onDataReloaded()方法通知瀑布流重新加载数据。
90
91由于在瀑布流布局中,各子节点的高度不一致,下面的节点位置依赖于上面的节点,所以重新加载所有数据会触发整个瀑布流重新计算布局,可能会导致卡顿。在数据末尾增加数据后,应使用`onDatasetChange([{ type: DataOperationType.ADD, index: len, count: count }])`通知,以使瀑布流能够识别新增数据并继续加载,同时避免对已有数据进行重复处理。
92
93![](figures/waterflow-demo1.gif)
94
95### 提前新增数据
96
97虽然在onReachEnd()触发时加载数据可以实现无限加载,但在滑动到底部会出现明显的停顿。
98
99为了实现更加流畅的无限滑动,需要调整增加新数据的时机。比如可以在LazyForEach还剩余若干个数据未遍历的情况下提前加载新数据。以下代码通过在WaterFlow的[onScrollIndex](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#onscrollindex11)中判断当前显示的最后一个子节点相对数据集终点的距离,并在合适时机提前加载新数据,实现了无停顿的无限滚动。
100
101```ts
102  build() {
103    Column({ space: 2 }) {
104      WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
105        LazyForEach(this.dataSource, (item: number) => {
106          FlowItem() {
107            ReusableFlowItem({ item: item })
108          }
109          .width('100%')
110          .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
111          .backgroundColor(this.colors[item % 5])
112        }, (item: string) => item)
113      }
114      .columnsTemplate('1fr '.repeat(this.columns))
115      .backgroundColor(0xFAEEE0)
116      .width('100%')
117      .height('100%')
118      .layoutWeight(1)
119      // 即将触底时提前增加数据
120      .onScrollIndex((first: number, last: number) => {
121        if (last + 20 >= this.dataSource.totalCount()) {
122          setTimeout(() => {
123            this.dataSource.addNewItems(100);
124          }, 1000);
125        }
126      })
127    }
128  }
129```
130
131![](figures/waterflow-demo2.gif)
132
133## 动态切换列数
134
135通过动态调整瀑布流的列数,应用能够实现在列表模式与瀑布流模式间的切换,或适应屏幕宽度的变化。 若要动态设置列数,建议采用瀑布流的移动窗口布局模式,这可以实现更快速的列数转换。
136
137```ts
138// 通过状态变量设置列数,可以按需修改触发布局更新
139@State columns: number = 2;
140
141@Reusable
142@Component
143struct ReusableListItem {
144  @State item: number = 0;
145
146  aboutToReuse(params: Record<string, number>) {
147    this.item = params.item;
148  }
149
150  build() {
151    Row() {
152      Image('res/waterFlow(' + this.item % 5 + ').JPG')
153        .objectFit(ImageFit.Fill)
154        .height(100)
155        .aspectRatio(1)
156      Text("N" + this.item).fontSize(12).height('16').layoutWeight(1).textAlign(TextAlign.Center)
157    }
158  }
159}
160
161  build() {
162    Column({ space: 2 }) {
163      Button('切换列数').fontSize(20).onClick(() => {
164        if (this.columns === 2) {
165          this.columns = 1;
166        } else {
167          this.columns = 2;
168        }
169      })
170      WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
171        LazyForEach(this.dataSource, (item: number) => {
172          FlowItem() {
173            if (this.columns === 1) {
174              ReusableListItem({ item: item })
175            } else {
176              ReusableFlowItem({ item: item })
177            }
178          }
179          .width('100%')
180          .aspectRatio(this.columns === 2 ? this.itemHeightArray[item % 100] / this.itemWidthArray[item % 100] : 0)
181          .backgroundColor(this.colors[item % 5])
182        }, (item: string) => item)
183      }
184      .columnsTemplate('1fr '.repeat(this.columns))
185      .backgroundColor(0xFAEEE0)
186      .width('100%')
187      .height('100%')
188      .layoutWeight(1)
189      // 即将触底时提前增加数据
190      .onScrollIndex((first: number, last: number) => {
191        if (last + 20 >= this.dataSource.totalCount()) {
192          setTimeout(() => {
193            this.dataSource.addNewItems(100);
194          }, 1000);
195        }
196      })
197    }
198  }
199```
200
201![](figures/waterflow-columns.gif)
202
203## 分组混合布局
204
205许多应用界面在瀑布流上方包含其他内容,这类场景可通过在Scroll或List内部嵌套WaterFlow来实现。类似下图:
206
207![](figures/waterflow-sections1.png)
208
209如果能够将不同部分的子节点整合到一个数据源中,那么通过设置 WaterFlowSections,可以在一个 WaterFlow 容器内实现混合布局。与嵌套滚动相比,这种方法可以简化滚动事件处理等应用逻辑。
210
211每个瀑布流分组可以分别设置自己的列数、行间距、列间距、margin和子节点总数,如下代码可以实现上述效果:
212
213```ts
214@Entry
215@Component
216struct WaterFlowDemo {
217  minSize: number = 80;
218  maxSize: number = 180;
219  colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
220  dataSource: WaterFlowDataSource = new WaterFlowDataSource(100);
221  private itemWidthArray: number[] = [];
222  private itemHeightArray: number[] = [];
223  private gridItems: number[] = [];
224  @State sections: WaterFlowSections = new WaterFlowSections();
225  sectionMargin: Margin = {
226    top: 10,
227    left: 5,
228    bottom: 10,
229    right: 5
230  };
231  oneColumnSection: SectionOptions = {
232    itemsCount: 1,
233    crossCount: 1,
234    columnsGap: 5,
235    rowsGap: 10,
236    margin: this.sectionMargin,
237  };
238  twoColumnSection: SectionOptions = {
239    itemsCount: 98,
240    crossCount: 2,
241  };
242  // 使用分组瀑布流时无法通过footer设置尾部组件,可以保留一个固定的分组作为footer
243  lastSection: SectionOptions = {
244    itemsCount: 1,
245    crossCount: 1,
246  };
247
248  // 计算FlowItem宽/高
249  getSize() {
250    let ret = Math.floor(Math.random() * this.maxSize);
251    return (ret > this.minSize ? ret : this.minSize);
252  }
253
254  // 设置FlowItem的宽/高数组
255  setItemSizeArray() {
256    for (let i = 0; i < 100; i++) {
257      this.itemWidthArray.push(this.getSize());
258      this.itemHeightArray.push(this.getSize());
259    }
260  }
261
262  aboutToAppear() {
263    this.setItemSizeArray();
264    for (let i = 0; i < 15; ++i) {
265      this.gridItems.push(i);
266    }
267    // 所有分组的itemCount之和需要和WaterFlow下数据源的子节点总数相等,否则无法正常布局
268    let sectionOptions: SectionOptions[] = [this.oneColumnSection, this.twoColumnSection, this.lastSection];
269    this.sections.splice(0, 0, sectionOptions);
270  }
271
272  build() {
273    WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW, sections: this.sections }) {
274      LazyForEach(this.dataSource, (item: number) => {
275        FlowItem() {
276          if (item === 0) {
277            Grid() {
278              ForEach(this.gridItems, (day: number) => {
279                GridItem() {
280                  Text('GridItem').fontSize(14).height(16)
281                }.backgroundColor(0xFFC0CB)
282              }, (day: number) => day.toString())
283            }
284            .height('30%')
285            .rowsGap(5)
286            .columnsGap(5)
287            .columnsTemplate('1fr '.repeat(5))
288            .rowsTemplate('1fr '.repeat(3))
289          } else {
290            ReusableFlowItem({ item: item })
291          }
292        }
293        .width('100%')
294        .aspectRatio(item != 0 ? this.itemHeightArray[item % 100] / this.itemWidthArray[item % 100] : 0)
295        .backgroundColor(item != 0 ? this.colors[item % 5] : Color.White)
296      }, (item: string) => item)
297    }
298    .backgroundColor(0xFAEEE0)
299    .height('100%')
300    // 即将触底时提前增加数据
301    .onScrollIndex((first: number, last: number) => {
302      if (last + 20 >= this.dataSource.totalCount()) {
303        setTimeout(() => {
304          this.dataSource.addNewItems(100);
305          // 增加数据后同步调整对应分组的itemCount
306          this.twoColumnSection.itemsCount += 100;
307          this.sections.update(1, this.twoColumnSection);
308        }, 1000);
309      }
310    })
311    .margin(10)
312  }
313}
314```
315
316>**说明:**
317>
318>使用分组混合布局时不支持单独设置footer,可以使用最后一个分组作为尾部组件。
319>
320>增加或删除数据后需要同步修改对应分组的itemCount。
321
322## 相关实例
323
324针对瀑布流开发,有以下实例可供参考:
325
326- [WaterFlow示例](https://gitcode.com/openharmony/applications_app_samples/tree/master/code/DocsSample/ArkUISample/ScrollableComponent/entry/src/main/ets/pages/waterFlow)
327
328<!--RP1--><!--RP1End-->