HarmonyOS NEXT 构建高性能瀑布流与懒加载列表
HarmonyOS NEXT 构建高性能瀑布流与懒加载列表
在HarmonyOS应用开发中,处理大量图文混排数据时,瀑布流(WaterFlow)结合懒加载(LazyForEach)是提升渲染性能和用户体验的核心方案。本文将深入探讨如何通过自定义数据源和组件,实现一个高性能的瀑布流列表。
核心实现思路
- 定义数据模型:构建描述瀑布流子项的数据接口。
- 实现数据源:基于
IDataSource接口封装数据管理逻辑,支持数据的动态增删与状态通知。 - 封装子项组件:开发可复用的UI组件,根据数据特征自适应布局。
- 页面组装:在页面中使用
WaterFlow容器配合LazyForEach实现按需渲染。
WaterFlow 组件特性
WaterFlow 是一种流式布局容器,通过行列网格划分,将不同尺寸的子项按照从上到下、从左到右的规则紧密排列,形成瀑布流效果。它仅允许包含 FlowItem 子组件,并全面支持 if/else、ForEach、LazyForEach 以及 Repeat 等渲染控制语法。
数据模型与数据源实现
首先定义子项的数据结构,然后实现 IDataSource 接口以驱动 LazyForEach。为了优化代码结构,我们将初始数据的生成逻辑进行了封装,并规范了数据变更的通知机制。
// 定义瀑布流子项的数据模型
export interface FlowItemData {
coverImg: Resource;
coverWidth: Length;
coverHeight: Length;
description: string;
actionText: string;
}
// 实现 IDataSource 接口,管理瀑布流数据
export class FlowListDataProvider implements IDataSource {
private itemList: FlowItemData[] = [];
private observers: DataChangeListener[] = [];
constructor() {
this.loadInitialData();
}
// 封装模拟数据生成逻辑
private generateMockItems(): FlowItemData[] {
return [
{ coverImg: $r('app.media.pic_1'), coverWidth: 162, coverHeight: 130, description: 'Explore the art of modern design...', actionText: 'Read More' },
{ coverImg: $r('app.media.pic_2'), coverWidth: '100%', coverHeight: 117, description: 'Daily routines for a healthier life...', actionText: 'Read More' },
{ coverImg: $r('app.media.pic_3'), coverWidth: '100%', coverHeight: 117, description: 'The science behind your morning coffee...', actionText: 'Read More' },
{ coverImg: $r('app.media.pic_4'), coverWidth: 162, coverHeight: 130, description: 'Trends shaping the fashion industry...', actionText: 'Read More' },
{ coverImg: $r('app.media.pic_5'), coverWidth: '100%', coverHeight: 206, description: 'Building beautiful UIs with ease...', actionText: 'Read More' }
];
}
private loadInitialData(): void {
this.itemList.push(...this.generateMockItems());
}
public getData(index: number): FlowItemData {
return this.itemList[index];
}
public totalCount(): number {
return this.itemList.length;
}
public registerDataChangeListener(listener: DataChangeListener): void {
if (!this.observers.includes(listener)) {
this.observers.push(listener);
}
}
public unregisterDataChangeListener(listener: DataChangeListener): void {
const idx = this.observers.indexOf(listener);
if (idx !== -1) {
this.observers.splice(idx, 1);
}
}
// 通知数据重载
private notifyReload(): void {
this.observers.forEach(obs => obs.onDataReloaded());
}
// 通知数据新增
private notifyAdd(index: number): void {
this.observers.forEach(obs => obs.onDataAdd(index));
}
// 通知数据删除
private notifyDelete(index: number): void {
this.observers.forEach(obs => obs.onDataDelete(index));
}
// 批量追加数据
public appendBatchData(): void {
const newItems = this.generateMockItems();
this.itemList.push(...newItems);
this.notifyReload();
}
// 在头部插入数据
public prependItem(item: FlowItemData): void {
this.itemList.unshift(item);
this.notifyAdd(0);
}
// 移除指定索引数据
public removeItemAt(index: number): void {
if (index >= 0 && index < this.itemList.length) {
this.itemList.splice(index, 1);
this.notifyDelete(index);
}
}
}
自定义瀑布流子项组件
子项组件需要支持组件复用(@Reusable),并在 aboutToReuse 生命周期中更新状态。布局上,根据封面图片的宽度属性动态切换垂直(Column)或水平(Row)排列。
import { FlowItemData } from './FlowListDataProvider';
@Reusable
@Component
export struct FlowCardView {
@Prop itemData: FlowItemData = {} as FlowItemData;
aboutToReuse(params: Record): void {
this.itemData = params.itemData;
}
build() {
if (this.itemData.coverWidth === '100%') {
// 垂直布局:图片在上,文本在下
Column() {
Image(this.itemData.coverImg)
.width(this.itemData.coverWidth)
.height(this.itemData.coverHeight)
.objectFit(ImageFit.Cover)
Column() {
Text(this.itemData.description)
.fontSize(14)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.itemData.actionText)
.fontSize(12)
.fontColor('#007DFF')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
} else {
// 水平布局:图片在左,文本在右
Row() {
Image(this.itemData.coverImg)
.width(this.itemData.coverWidth)
.height(this.itemData.coverHeight)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 8, bottomLeft: 8 })
Column() {
Text(this.itemData.description)
.fontSize(14)
.fontColor('#333333')
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.itemData.actionText)
.fontSize(12)
.fontColor('#007DFF')
.margin({ top: 8 })
}
.height('100%')
.layoutWeight(1)
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.borderRadius(8)
}
}
}
页面集成与 LazyForEach 渲染
在页面中,通过配置 WaterFlowSections 来定义瀑布流的分区规则(如列数、间距、子项高度等)。结合 LazyForEach 实现数据的按需加载,避免一次性渲染所有子项导致的内存和性能问题。
import { FlowItemData, FlowListDataProvider } from './FlowListDataProvider';
import { FlowCardView } from './FlowCardView';
@Entry
@Component
struct WaterFlowExamplePage {
private listProvider: FlowListDataProvider = new FlowListDataProvider();
private flowScroller: Scroller = new Scroller();
@State flowSections: WaterFlowSections = new WaterFlowSections();
aboutToAppear(): void {
this.initializeSections();
// 模拟加载多页数据
for (let i = 0; i < 5; i++) {
this.listProvider.appendBatchData();
}
}
// 配置瀑布流分区参数
private initializeSections(): void {
const baseMargin: Margin = { top: 10, left: 16, bottom: 10, right: 16 };
// 单列大卡片分区
this.flowSections.push({
itemsCount: 1,
crossCount: 1,
margin: { ...baseMargin, top: 20 },
onGetItemMainSizeByIndex: () => 140
});
// 双列标准卡片分区
this.flowSections.push({
itemsCount: 2,
crossCount: 2,
rowsGap: '16vp',
columnsGap: '16vp',
margin: baseMargin,
onGetItemMainSizeByIndex: () => 220
});
// 单列横向卡片分区
this.flowSections.push({
itemsCount: 1,
crossCount: 1,
margin: baseMargin,
onGetItemMainSizeByIndex: () => 140
});
// 双列长卡片分区
this.flowSections.push({
itemsCount: 2,
crossCount: 2,
rowsGap: '16vp',
columnsGap: '16vp',
margin: baseMargin,
onGetItemMainSizeByIndex: () => 280
});
}
build() {
Column() {
WaterFlow({ scroller: this.flowScroller, sections: this.flowSections }) {
LazyForEach(this.listProvider, (item: FlowItemData, index: number) => {
FlowItem() {
FlowCardView({ itemData: item })
}
.width('100%')
.padding({ bottom: 12 })
}, (item: FlowItemData, index: number) => {
// 生成唯一Key,确保组件复用的准确性
return `flow_item_${index}_${item.description.length}`;
})
}
.columnsGap(12)
.rowsGap(12)
.backgroundColor('#F1F3F5')
.width('100%')
.layoutWeight(1)
}
}
}