• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Creating a Waterfall Flow (WaterFlow)
2
3You can use the [WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md) component in ArkUI to create a waterfall flow layout, which is commonly used to display image collections, especially in e-commerce and news applications.
4The WaterFlow component supports conditional rendering, loop rendering (rendering of repeated content), and lazy loading to generate child components.
5
6## Layout and Constraints
7
8The waterfall flow supports both horizontal and vertical layouts. In a vertical layout, you can set the number of columns using [columnsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#columnstemplate). In a horizontal layout, you can set the number of rows using [rowsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#rowstemplate).
9
10In the vertical layout, child nodes in the first row are arranged from left to right. From the second row onward, each child node is placed in the column with the smallest total height. If multiple columns have the same total height, they are filled in order from left to right. The following figure shows this arrangement logic.
11
12![](figures/waterflow.png)
13
14In the horizontal layout, each child node is placed in the row with the smallest total width. If multiple rows have the same width, they are filled in order from left to right.
15
16![](figures/waterflow-row.png)
17
18## Infinite Scrolling
19
20### Adding Data When Reaching the End
21
22The waterfall flow layout is often used for infinite scrolling feeds. You can add new data to [LazyForEach](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md) in the [onReachEnd](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#onreachend) event callback when the **WaterFlow** component reaches the end position, and create a footer that indicates loading new data (using the [LoadingProgress](../reference/apis-arkui/arkui-ts/ts-basic-components-loadingprogress.md) component).
23
24```ts
25  @Builder
26  itemFoot() {
27    Row() {
28      LoadingProgress()
29        .color(Color.Blue).height(50).aspectRatio(1).width('20%')
30      Text(`Loading`)
31        .fontSize(20)
32        .width('30%')
33        .height(50)
34        .align(Alignment.Center)
35        .margin({ top: 2 })
36    }.width('100%').justifyContent(FlexAlign.Center)
37  }
38
39  build() {
40    Column({ space: 2 }) {
41      WaterFlow({ footer: this.itemFoot(), layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
42        LazyForEach(this.dataSource, (item: number) => {
43          FlowItem() {
44            ReusableFlowItem({ item: item })
45          }
46          .width('100%')
47          .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
48          .backgroundColor(this.colors[item % 5])
49        }, (item: string) => item)
50      }
51      .columnsTemplate('1fr '.repeat(this.columns))
52      .backgroundColor(0xFAEEE0)
53      .width('100%')
54      .height('100%')
55      .layoutWeight(1)
56      // Load data once the component reaches the bottom.
57      .onReachEnd(() => {
58        setTimeout(() => {
59          this.dataSource.addNewItems();
60        }, 1000);
61      })
62    }
63  }
64
65  // Method in WaterFlowDataSource to append count-specified elements to the data
66  public addNewItems(count: number): void {
67    let len = this.dataArray.length;
68    for (let i = 0; i < count; i++) {
69      this.dataArray.push(this.dataArray.length);
70    }
71    this.listeners.forEach(listener => {
72      listener.onDatasetChange([{ type: DataOperationType.ADD, index: len, count: count }]);
73    })
74  }
75
76```
77
78Always add data to the end of the data array (**dataArray**) instead of modifying the array directly using the **onDataReloaded()** API of **LazyForEach**.
79
80Since the heights of the child nodes in the **WaterFlow** component are inconsistent, the position of the lower nodes depends on the upper nodes. Therefore, reloading all data triggers full layout recalculation, potentially causing lag. After adding data to the end of the data array, you must use **onDatasetChange([{ type: DataOperationType.ADD, index: len, count: count }])** to notify the **WaterFlow** component of new data without reprocessing existing items.
81
82![](figures/waterflow-demo1.gif)
83
84### Pre-loading Data
85
86Triggering data loading at **onReachEnd()** can cause noticeable pause when the component scrolls to the bottom.
87
88To enable smooth infinite scrolling, you need to adjust the timing of adding new data. For example, you can preload new data when there are still several items left to be traversed in **LazyForEach**. The following code monitors the scroll position (distance of the last displayed child node from the end of the dataset) in the [onScrollIndex](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#onscrollindex11) API of **WaterFlow** and pre-loads new data at the right time to achieve smooth infinite scrolling.
89
90```ts
91  build() {
92    Column({ space: 2 }) {
93      WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
94        LazyForEach(this.dataSource, (item: number) => {
95          FlowItem() {
96            ReusableFlowItem({ item: item })
97          }
98          .width('100%')
99          .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
100          .backgroundColor(this.colors[item % 5])
101        }, (item: string) => item)
102      }
103      .columnsTemplate('1fr '.repeat(this.columns))
104      .backgroundColor(0xFAEEE0)
105      .width('100%')
106      .height('100%')
107      .layoutWeight(1)
108      // Pre-load data when approaching the bottom.
109      .onScrollIndex((first: number, last: number) => {
110        if (last + 20 >= this.dataSource.totalCount()) {
111          setTimeout(() => {
112            this.dataSource.addNewItems(100);
113          }, 1000);
114        }
115      })
116    }
117  }
118```
119
120![](figures/waterflow-demo2.gif)
121
122## Dynamically Adjusting the Column Count
123
124Dynamically adjusting the column count allows applications to switch between list and waterfall flow modes or adapt to screen width changes. For faster transitions, use the sliding window layout mode.
125
126```ts
127// Use a state variable to manage the column count and trigger layout updates.
128@State columns: number = 2;
129
130@Reusable
131@Component
132struct ReusableListItem {
133  @State item: number = 0;
134
135  aboutToReuse(params: Record<string, number>) {
136    this.item = params.item;
137  }
138
139  build() {
140    Row() {
141      Image('res/waterFlow(' + this.item % 5 + ').JPG')
142        .objectFit(ImageFit.Fill)
143        .height(100)
144        .aspectRatio(1)
145      Text("N" + this.item).fontSize(12).height('16').layoutWeight(1).textAlign(TextAlign.Center)
146    }
147  }
148}
149
150  build() {
151    Column({ space: 2 }) {
152      Button('Switch Columns').fontSize(20).onClick(() => {
153        if (this.columns === 2) {
154          this.columns = 1;
155        } else {
156          this.columns = 2;
157        }
158      })
159      WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW }) {
160        LazyForEach(this.dataSource, (item: number) => {
161          FlowItem() {
162            if (this.columns === 1) {
163              ReusableListItem({ item: item })
164            } else {
165              ReusableFlowItem({ item: item })
166            }
167          }
168          .width('100%')
169          .aspectRatio(this.columns === 2 ? this.itemHeightArray[item % 100] / this.itemWidthArray[item % 100] : 0)
170          .backgroundColor(this.colors[item % 5])
171        }, (item: string) => item)
172      }
173      .columnsTemplate('1fr '.repeat(this.columns))
174      .backgroundColor(0xFAEEE0)
175      .width('100%')
176      .height('100%')
177      .layoutWeight(1)
178      // Pre-load data when approaching the bottom.
179      .onScrollIndex((first: number, last: number) => {
180        if (last + 20 >= this.dataSource.totalCount()) {
181          setTimeout(() => {
182            this.dataSource.addNewItems(100);
183          }, 1000);
184        }
185      })
186    }
187  }
188```
189
190![](figures/waterflow-columns.gif)
191
192## Mixed Section Layout
193
194Many application UIs feature supplementary content above the **WaterFlow** component. This scenario can be implemented by nesting a **WaterFlow** within a **Scroll** or **List** container, as illustrated in the following figure:
195
196![](figures/waterflow-sections1.png)
197
198When child nodes from different sections can be integrated into a single data source, using **WaterFlowSections** enables mixed layouts within a single **WaterFlow** container. This approach simplifies scroll event handling logic compared to nested scrolling implementations.
199
200Each **WaterFlow** section can individually set its own number of columns, row spacing, column spacing, margin, and total number of child nodes. The following code can achieve the above effect:
201
202```ts
203@Entry
204@Component
205struct WaterFlowDemo {
206  minSize: number = 80;
207  maxSize: number = 180;
208  colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
209  dataSource: WaterFlowDataSource = new WaterFlowDataSource(100);
210  private itemWidthArray: number[] = [];
211  private itemHeightArray: number[] = [];
212  private gridItems: number[] = [];
213  @State sections: WaterFlowSections = new WaterFlowSections();
214  sectionMargin: Margin = {
215    top: 10,
216    left: 5,
217    bottom: 10,
218    right: 5
219  };
220  oneColumnSection: SectionOptions = {
221    itemsCount: 1,
222    crossCount: 1,
223    columnsGap: 5,
224    rowsGap: 10,
225    margin: this.sectionMargin,
226  };
227  twoColumnSection: SectionOptions = {
228    itemsCount: 98,
229    crossCount: 2,
230  };
231  // Use the last section as a footer, since footers are not supported with sections.
232  lastSection: SectionOptions = {
233    itemsCount: 1,
234    crossCount: 1,
235  };
236
237  // Calculate the FlowItem width and height.
238  getSize() {
239    let ret = Math.floor(Math.random() * this.maxSize);
240    return (ret > this.minSize ? ret : this.minSize);
241  }
242
243  // Set the FlowItem size array.
244  setItemSizeArray() {
245    for (let i = 0; i < 100; i++) {
246      this.itemWidthArray.push(this.getSize());
247      this.itemHeightArray.push(this.getSize());
248    }
249  }
250
251  aboutToAppear() {
252    this.setItemSizeArray();
253    for (let i = 0; i < 15; ++i) {
254      this.gridItems.push(i);
255    }
256    // The total number of itemCount values across sections must match the data source item count of the WaterFlow.
257    let sectionOptions: SectionOptions[] = [this.oneColumnSection, this.twoColumnSection, this.lastSection];
258    this.sections.splice(0, 0, sectionOptions);
259  }
260
261  build() {
262    WaterFlow({ layoutMode: WaterFlowLayoutMode.SLIDING_WINDOW, sections: this.sections }) {
263      LazyForEach(this.dataSource, (item: number) => {
264        FlowItem() {
265          if (item === 0) {
266            Grid() {
267              ForEach(this.gridItems, (day: number) => {
268                GridItem() {
269                  Text('GridItem').fontSize(14).height(16)
270                }.backgroundColor(0xFFC0CB)
271              }, (day: number) => day.toString())
272            }
273            .height('30%')
274            .rowsGap(5)
275            .columnsGap(5)
276            .columnsTemplate('1fr '.repeat(5))
277            .rowsTemplate('1fr '.repeat(3))
278          } else {
279            ReusableFlowItem({ item: item })
280          }
281        }
282        .width('100%')
283        .aspectRatio(item != 0 ? this.itemHeightArray[item % 100] / this.itemWidthArray[item % 100] : 0)
284        .backgroundColor(item != 0 ? this.colors[item % 5] : Color.White)
285      }, (item: string) => item)
286    }
287    .backgroundColor(0xFAEEE0)
288    .height('100%')
289    // Pre-load data when approaching the bottom.
290    .onScrollIndex((first: number, last: number) => {
291      if (last + 20 >= this.dataSource.totalCount()) {
292        setTimeout(() => {
293          this.dataSource.addNewItems(100);
294          // Update the itemCount values for sections after adding data.
295          this.twoColumnSection.itemsCount += 100;
296          this.sections.update(1, this.twoColumnSection);
297        }, 1000);
298      }
299    })
300    .margin(10)
301  }
302}
303```
304
305>**NOTE**
306>
307>Footers are not supported with **WaterFlowSections**; use the last section as a footer instead.
308>
309>Always update the corresponding **itemsCount** when adding or removing data to maintain layout consistency.
310