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 24 25在瀑布流的横向布局中,每个子节点都会放置在当前总宽度最小的行。若多行总宽度相同,则按照从上到下的顺序进行填充。 26 27 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 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 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 202 203## 分组混合布局 204 205许多应用界面在瀑布流上方包含其他内容,这类场景可通过在Scroll或List内部嵌套WaterFlow来实现。类似下图: 206 207 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-->