鸿蒙开发中瀑布流(WaterFlow)的全面解析
鸿蒙系统通过WaterFlow组件提供了强大的瀑布流布局实现能力,适用于电商、图库等场景。本文详细介绍了瀑布流布局的原理、WaterFlow组件的核心API、常用方法及性能优化技巧。瀑布流布局的核心特点是动态高度和智能填充,WaterFlow组件支持纵向和横向布局,提供丰富的布局控制API和事件处理功能。通过LazyForEach懒加载和IDataSource接口,开发者可以有效管理数据并优化性能
本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
瀑布流布局是现代移动应用中常见的UI设计模式,尤其在电商、图库、社交媒体等场景中广泛应用。鸿蒙系统通过WaterFlow组件提供了强大的瀑布流实现能力。本文将全面讲解鸿蒙开发中瀑布流的使用方法,包括核心API、常用方法、性能优化技巧。
一、瀑布流概念
1.1 瀑布流布局原理
瀑布流(WaterFlow)是一种非对称的网格布局,其核心特点是:
- 动态高度:每个子元素(FlowItem)根据内容自动调整高度
- 智能填充:新元素会自动填充到当前高度最小的列(纵向布局)或行(横向布局)
- 视觉层次:形成错落有致的视觉效果,提升浏览体验
在纵向布局中,第一行的子节点按从左到右顺序排列,从第二行开始,每个子节点将放置在当前总高度最小的列。如果多个列的总高度相同,则按照从左到右的顺序填充。
1.2 WaterFlow组件简介
WaterFlow是鸿蒙ArkUI提供的瀑布流容器组件,主要特点包括:
- 支持纵向和横向两种布局方向
- 支持条件渲染、循环渲染和懒加载
- 提供丰富的布局控制API
- 内置滚动和触底加载功能
- 从API Version 9开始支持
1.3 基本组成元素
一个完整的瀑布流实现通常包含以下部分:
- WaterFlow容器:承载所有子组件
- FlowItem子组件:瀑布流中的每一项内容
- 数据源:通常实现IDataSource接口
- 懒加载组件:LazyForEach优化性能
- 布局参数:控制列数、间距等
二、WaterFlow核心API详解
2.1 组件构造方法
WaterFlow组件提供两种构造方式:
// 基础构造
WaterFlow()
// 带参数的构造
WaterFlow(options?: {footer?: CustomBuilder, scroller?: Scroller})
参数说明:
footer
: 可选的底部构建器,常用于显示"加载更多"状态scroller
: 可滚动组件的控制器,目前仅支持scrollToIndex接口
示例:
WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {
// 子组件
}
2.2 布局控制属性
2.2.1 列/行模板
-
columnsTemplate
: 设置列数和列宽比例(纵向布局时有效).columnsTemplate('1fr 1fr 1fr') // 三等分 .columnsTemplate('1fr 2fr') // 第一列占1/3,第二列占2/3
-
rowsTemplate
: 设置行数和行高比例(横向布局时有效)
.rowsTemplate('1fr 1fr 1fr') // 三行等高
支持auto-fill
关键字自动填充可用空间:
.columnsTemplate('repeat(auto-fill, 100vp)') // 自动填充100vp宽的列
2.2.2 间距控制
-
columnsGap
: 列间距,默认0.columnsGap(10) // 10vp列间距
-
rowsGap
: 行间距,默认0
.rowsGap(8) // 8vp行间距
完整示例:
WaterFlow()
.columnsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(8)
.margin({left: 15, right: 15})
2.2.3 布局方向
layoutDirection
控制主轴方向,影响columnsTemplate/rowsTemplate的生效情况:
.layoutDirection(FlexDirection.Column) // 纵向布局(默认)
.layoutDirection(FlexDirection.Row) // 横向布局
注意:
- 纵向布局时
columnsTemplate
有效 - 横向布局时
rowsTemplate
有效 - 未设置时默认为FlexDirection.Column
2.2.4 尺寸约束
itemConstraintSize
可设置子组件的约束尺寸:
.itemConstraintSize({
minWidth: 0,
maxWidth: '100%',
minHeight: 0,
maxHeight: '100%'
})
2.3 事件API
2.3.1 触底事件
onReachEnd
在滚动到底部时触发,常用于实现无限滚动:
.onReachEnd(() => {
console.info('onReached')
setTimeout(() => {
for(let i = 0; i < 40; i++){
this.dataSource.addLastItem()
}
}, 1000)
})
2.3.2 触顶事件
onReachStart
在滚动到顶部时触发:
.onReachStart(() => {
console.info("onReachStart")
})
三、数据管理与性能优化
3.1 LazyForEach懒加载
LazyForEach是性能优化的关键,它按需创建组件而非一次性加载全部:
LazyForEach(
dataSource: IDataSource, // 数据源
itemGenerator: (item: any, index: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index: number) => string // 键值生成函数
)
关键特性:
- 只在可视区域加载组件
- 必须实现IDataSource接口
- 每次迭代只能创建一个子组件
- 需要提供唯一键值生成器
完整示例:
LazyForEach(this.dataSource, (item: Item, index) => {
FlowItem(){
Column(){
Text('第' + `${item.index.toString()}` + '个')
Image(item.image)
.objectFit(ImageFit.Fill)
}
.backgroundColor('#ffa4eaac')
.borderRadius(8)
}
}, (item: Item) => item.index.toString())
3.2 IDataSource接口实现
自定义数据源需要实现IDataSource接口:
export class WaterFlowDataSource implements IDataSource {
private dataArray: Item[] = []
private listeners: DataChangeListener[] = []
// 获取数据项
getData(index: number): Item {
return this.dataArray[index]
}
// 返回数据总数
totalCount(): number {
return this.dataArray.length
}
// 注册数据变化监听器
registerDataChangeListener(listener: DataChangeListener): void {
if(this.listeners.indexOf(listener) < 0){
this.listeners.push(listener)
}
}
// 注销数据变化监听器
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if(pos >= 0){
this.listeners.splice(pos, 1)
}
}
// 添加新项
public addLastItem(): void {
let newItem = this.createNewItem(this.dataArray.length)
this.dataArray.push(newItem)
this.notifyDataAdd(this.dataArray.length - 1)
}
// 创建新项
private createNewItem(index: number): Item {
let randomNumber = getRandomInt(1,6)
return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)
}
// 通知监听器数据变化
private notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
}
3.3 无限滚动实现
结合onReachEnd和动态数据加载实现无限滚动:
// 1. 定义footer组件
@Builder itemFoot() {
Row() {
LoadingProgress()
.color(Color.Blue).height(50).aspectRatio(1).width('20%')
Text('正在加载')
.fontSize(20)
.width('30%')
.height(50)
.align(Alignment.Center)
.margin({top: 2})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
// 2. 在build方法中使用
build() {
Column() {
WaterFlow({footer: this.itemFoot, scroller: this.scroller}) {
LazyForEach(this.dataSource, (item: number) => {
FlowItem() {
// 内容
}
}, (item: string) => item)
}
.onReachEnd(() => {
setTimeout(() => {
this.dataSource.addNewItems(10); // 加载10个新项
}, 1000);
})
}
}
// 3. 数据源中添加新项的方法
public addNewItems(count: number): void {
let len = this.dataArray.length;
for(let i = 0; i < count; i++) {
this.dataArray.push(this.dataArray.length);
}
this.listeners.forEach(listener => {
listener.onDatasetChange([{
type: DataOperationType.ADD,
index: len,
count: count
}]);
})
}
四、实战案例
4.1 瀑布流
完整实现一个电商商品列表,包含图片和标题:
// 商品接口定义
export interface GoodsItem {
title: string
imageUrl: string
}
// mock数据
export const mockGoodsList: GoodsItem[] = [
{
title: '宁雨昔美式复古字母三条杠圆领短袖T恤女纯棉宽松2024夏季新款学生上衣 白色 M',
imageUrl: 'https://img10.360buyimg.com/n2/s240x240_jfs/t1/145307/22/41197/71267/65b5d932Fb67b2c27/cd986ae610999146.jpg!q70.jpg.webp'
},
// 更多商品...
]
// 主页面实现
@Entry
@Component
struct WaterFlowGoodsPage {
@State goodsList: GoodsItem[] = mockGoodsList
@State isLoadMore: boolean = false
@Builder
getGoodsItemView(item: GoodsItem, index: number) {
Column({ space: 5 }) {
Image(item.imageUrl)
.height(index % 2 ? 120 : 180) // 交错高度
.borderRadius(8)
Text(item.title)
.fontSize(14)
.lineHeight(22)
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
@Builder
getFooter() {
Row() {
Text('加载更多...')
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Pink)
.height(60)
.width('100%')
}
build() {
WaterFlow({ footer: this.getFooter }) {
ForEach(this.goodsList, (item: GoodsItem, index: number) => {
FlowItem() {
this.getGoodsItemView(item, index)
}
})
}
.height('100%')
.columnsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.padding(10)
.onReachEnd(async () => {
if (!this.isLoadMore) {
try {
this.isLoadMore = true
await this.loadMore()
this.isLoadMore = false
} catch (error) {
promptAction.showToast({ message: JSON.stringify(error) })
}
}
})
}
loadMore() {
return new Promise<boolean>((resolve) => {
setTimeout(() => {
this.goodsList.push(...this.goodsList.slice(0, 5)) // 追加5个商品
resolve(true)
}, 2000)
})
}
}
4.2 图片瀑布流画廊
实现一个图片瀑布流,包含随机高度和点击预览:
@Entry
@Component
struct ImageWaterFlow {
private scroller: Scroller = new Scroller()
private dataSource: WaterFlowDataSource = new WaterFlowDataSource()
@Builder
itemFoot() {
Row() {
LoadingProgress()
Text('加载更多图片...')
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
build() {
Column() {
WaterFlow({ footer: this.itemFoot, scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: Item, index) => {
FlowItem() {
Column() {
Text('第' + `${index + 1}` + '张')
Image(item.image)
.objectFit(ImageFit.Fill)
.sharedTransition(`sharedImage${index}`, {
duration: 300,
curve: Curve.Linear,
delay: 50
})
}
.backgroundColor('#ffa4eaac')
.borderRadius(8)
}
.onClick(() => {
router.pushUrl({
url: 'pages/waterflow/Preview',
params: { item: item, index: index }
})
})
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(8)
.margin({left: 15, right: 15})
.onReachEnd(() => {
setTimeout(() => {
for(let i = 0; i < 10; i++) {
this.dataSource.addLastItem()
}
}, 1000)
})
}
.margin({top: 20})
}
}
// 数据源实现
export class WaterFlowDataSource implements IDataSource {
private dataArray: Item[] = []
private listeners: DataChangeListener[] = []
constructor() {
for(let i = 1; i <= 40; i++) {
let r = getRandomInt(1, 6)
this.dataArray.push(new Item($r(`app.media.img_gril_${r}`), i))
}
}
// ...其他必要接口方法实现
public addLastItem(): void {
let newItem = this.createNewItem(this.dataArray.length)
this.dataArray.push(newItem)
this.notifyDataAdd(this.dataArray.length - 1)
}
private createNewItem(index: number): Item {
let randomNumber = getRandomInt(1,6)
return new Item($r(`app.media.img_gril_${randomNumber}`), index+1)
}
}
五、高级技巧
5.1 性能优化策略
-
合理设置FlowItem尺寸
- 避免频繁测量布局
- 对于已知尺寸的内容,明确设置宽高
FlowItem() .width(this.itemWidthArray[item % 100]) .aspectRatio(this.itemHeightArray[item % 100] / this.itemWidthArray[item%100])
-
使用缓存策略
- 对于网络图片,实现内存和磁盘缓存
- 考虑使用ReusableFlowItem复用组件
-
虚拟化长列表
- 确保使用LazyForEach而非普通ForEach
- 设置合理的keyGenerator提高复用率
LazyForEach(this.dataSource, (item) => {
// ...
}, (item) => item.id.toString()) // 唯一键值
5.2 动态布局调整
根据屏幕方向或尺寸变化调整布局:
@State columnsTemplate: string = '1fr 1fr' // 默认两列
// 监听屏幕变化
.onAreaChange((oldValue, newValue) => {
if(newValue.width > 600) { // 宽屏设备
this.columnsTemplate = '1fr 1fr 1fr' // 三列
} else {
this.columnsTemplate = '1fr 1fr' // 两列
}
})
// 应用动态模板
WaterFlow()
.columnsTemplate(this.columnsTemplate)
六、常见问题与解决方案
6.1 布局错乱问题
现象:图片加载后布局跳动或重叠
解决方案:
- 预计算或固定宽高比
.aspectRatio(1) // 1:1比例
- 使用占位图保持布局稳定
- 实现图片加载完成回调后再显示
6.2 滚动卡顿问题
优化建议:
- 减少FlowItem内部组件复杂度
- 避免在FlowItem中使用深层次嵌套
- 对于复杂内容,考虑使用自定义绘制代替多个组件
6.3 内存占用过高
控制策略:
- 实现数据分页加载,不一次性加载所有数据
- 监听滚动位置,释放不可见区域的资源
- 对于大图,使用合适尺寸的缩略图
更多推荐
所有评论(0)