1# 创建瀑布流(WaterFlow) 2 3[瀑布流](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)常用于展示图片信息,尤其在购物和资讯类应用中。 4ArkUI提供了WaterFlow容器组件,用于构建瀑布流布局。WaterFlow组件支持条件渲染、循环渲染和懒加载等方式生成子组件。 5 6## 布局与约束 7 8瀑布流支持横向和纵向布局。在纵向布局中,可以通过[columnsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#columnstemplate)设置列数;在横向布局中,可以通过[rowsTemplate](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#rowstemplate)设置行数。 9 10在瀑布流的纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。如下图: 11 12 13 14在瀑布流的横向布局中,每个子节点都会放置在当前总宽度最小的行。若多行总宽度相同,则按照从上到下的顺序进行填充。 15 16 17 18## 无限滚动 19 20### 到达末尾时新增数据 21 22瀑布流常用于无限滚动的信息流。可以在瀑布流组件到达末尾位置时触发的[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)组件)。 23 24```ts 25 @Builder 26 itemFoot() { 27 Row() { 28 LoadingProgress() 29 .color(Color.Blue).height(50).aspectRatio(1).width('20%') 30 Text(`正在加载`) 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 // 触底加载数据 57 .onReachEnd(() => { 58 setTimeout(() => { 59 this.dataSource.addNewItems(); 60 }, 1000); 61 }) 62 } 63 } 64 65 // WaterFlowDataSource中增加在数据尾部增加count个元素的方法 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 78在此处应通过在数据末尾添加元素的方式来新增数据,不可直接修改dataArray后通过LazyForEach的onDataReloaded()方法通知瀑布流重新加载数据。 79 80由于在瀑布流布局中,各子节点的高度不一致,下面的节点位置依赖于上面的节点,所以重新加载所有数据会触发整个瀑布流重新计算布局,可能会导致卡顿。在数据末尾增加数据后,应使用`onDatasetChange([{ type: DataOperationType.ADD, index: len, count: count }])`通知,以使瀑布流能够识别新增数据并继续加载,同时避免对已有数据进行重复处理。 81 82 83 84### 提前新增数据 85 86虽然在onReachEnd()触发时加载数据可以实现无限加载,但在滑动到底部会出现明显的停顿。 87 88为了实现更加流畅的无限滑动,需要调整增加新数据的时机。比如可以在LazyForEach还剩余若干个数据未遍历的情况下提前加载新数据。以下代码通过在WaterFlow的[onScrollIndex](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#onscrollindex11)中判断当前显示的最后一个子节点相对数据集终点的距离,并在合适时机提前加载新数据,实现了无停顿的无限滚动。 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 // 即将触底时提前增加数据 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## 动态切换列数 123 124通过动态调整瀑布流的列数,应用能够实现在列表模式与瀑布流模式间的切换,或适应屏幕宽度的变化。 若要动态设置列数,建议采用瀑布流的移动窗口布局模式,这可以实现更快速的列数转换。 125 126```ts 127// 通过状态变量设置列数,可以按需修改触发布局更新 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('切换列数').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 // 即将触底时提前增加数据 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## 分组混合布局 193 194许多应用界面在瀑布流上方包含其他内容,这类场景可通过在Scroll或List内部嵌套WaterFlow来实现。类似下图: 195 196 197 198如果能够将不同部分的子节点整合到一个数据源中,那么通过设置 WaterFlowSections,可以在一个 WaterFlow 容器内实现混合布局。与嵌套滚动相比,这种方法可以简化滚动事件处理等应用逻辑。 199 200每个瀑布流分组可以分别设置自己的列数、行间距、列间距、margin和子节点总数,如下代码可以实现上述效果: 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 // 使用分组瀑布流时无法通过footer设置尾部组件,可以保留一个固定的分组作为footer 232 lastSection: SectionOptions = { 233 itemsCount: 1, 234 crossCount: 1, 235 }; 236 237 // 计算FlowItem宽/高 238 getSize() { 239 let ret = Math.floor(Math.random() * this.maxSize); 240 return (ret > this.minSize ? ret : this.minSize); 241 } 242 243 // 设置FlowItem的宽/高数组 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 // 所有分组的itemCount之和需要和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 // 即将触底时提前增加数据 290 .onScrollIndex((first: number, last: number) => { 291 if (last + 20 >= this.dataSource.totalCount()) { 292 setTimeout(() => { 293 this.dataSource.addNewItems(100); 294 // 增加数据后同步调整对应分组的itemCount 295 this.twoColumnSection.itemsCount += 100; 296 this.sections.update(1, this.twoColumnSection); 297 }, 1000); 298 } 299 }) 300 .margin(10) 301 } 302} 303``` 304 305>**说明:** 306> 307>使用分组混合布局时不支持单独设置footer,可以使用最后一个分组作为尾部组件。 308> 309>增加或删除数据后需要同步修改对应分组的itemCount。 310 311## 相关实例 312 313针对瀑布流开发,有以下实例可供参考: 314 315[主页瀑布流实现](https://gitee.com/harmonyos-cases/cases/blob/master/CommonAppDevelopment/feature/functionalscenes/README.md)