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 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 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 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 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 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 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