事件与回调
虚拟列表通过 IVirtualListCallbacks 接口暴露核心事件,支持节点渲染、滚动监听、下拉刷新等场景的自定义处理。
回调接口
基础回调
| 回调 | 触发时机 | 常见用途 |
|---|---|---|
onItemInit(node, index) | 节点首次创建时触发 | 初始化组件、绑定事件、设置静态数据 |
onItemUpdate(node, index) | 节点重新进入可见区域时触发 | 更新文本、图片、状态等动态数据 |
onScrolling(scrollRatio) | 滚动过程中持续触发 | 更新进度条、吸顶效果、懒加载触发 |
onLoadFinished() | 初始化或刷新完成后触发 | 隐藏加载动画、播放入场动画 |
下拉刷新和上拉加载
| 回调 | 触发场景 | 返回值 |
|---|---|---|
onPullDownRefresh() | 在列表顶部向下拉动 > 50px | Promise<PullRefreshResult> 或 void |
onPullUpLoad() | 在列表底部向上拉动 > 50px | Promise<PullRefreshResult> 或 void |
PullRefreshResult 数据结构:
ts
interface PullRefreshResult {
types: Array<string | number>; // 新数据的模板类型列表
sizes?: number[]; // 可选的自定义尺寸列表
}使用示例
基础回调设置
ts
import { VirtualViewList } from 'assets/Script/Core/VirtualList/VirtualViewList';
const list = this.node.getComponent(VirtualViewList)!;
list.SetCallbacks({
onItemInit: (node, index) => {
// 首次创建:初始化组件引用
const item = node.getComponent(ItemComponent);
item.init(this.data[index]);
},
onItemUpdate: (node, index) => {
// 重新显示:更新数据
const item = node.getComponent(ItemComponent);
item.refresh(this.data[index]);
},
onScrolling: (ratio) => {
// 更新滚动进度(0-1)
this.progressBar.progress = ratio;
},
onLoadFinished: () => {
// 加载完成:隐藏骨架屏
this.loadingNode.active = false;
}
});下拉刷新示例
ts
list.SetCallbacks({
onPullDownRefresh: async () => {
try {
// 1. 请求最新数据
const newMessages = await this.fetchLatestMessages();
// 2. 返回新数据(自动调用 PrependData)
return {
types: newMessages.map(msg => msg.type),
sizes: newMessages.map(msg => msg.height)
};
} catch (error) {
console.error('刷新失败:', error);
ToastMgr.Show('刷新失败');
}
}
});上拉加载更多示例
ts
list.SetCallbacks({
onPullUpLoad: async () => {
// 检查是否还有更多数据
if (!this.hasMore) {
ToastMgr.Show('没有更多数据了');
return;
}
try {
// 1. 请求下一页数据
const moreData = await this.fetchNextPage();
// 2. 更新状态
this.hasMore = moreData.hasNext;
// 3. 返回新数据(自动调用 AppendData)
return {
types: moreData.items.map(item => item.type),
sizes: moreData.items.map(item => item.height)
};
} catch (error) {
console.error('加载失败:', error);
ToastMgr.Show('加载失败');
}
}
});滚动进度监听示例
ts
list.SetCallbacks({
onScrolling: (ratio) => {
// 1. 更新进度指示器
this.scrollIndicator.progress = ratio;
// 2. 吸顶效果
this.headerNode.active = ratio > 0.1;
// 3. 返回顶部按钮
this.backToTopBtn.active = ratio > 0.3;
// 4. 懒加载触发
if (ratio > 0.9 && !this.isLoading) {
this.loadMoreData();
}
}
});触发条件详解
下拉刷新/上拉加载触发条件
需同时满足以下所有条件:
| 条件 | 说明 | 默认值 |
|---|---|---|
| 拉动距离 | 超过阈值才触发 | 50px |
| 时间间隔 | 距离上次触发的间隔 | 1 秒 |
| 初始化状态 | 列表必须已完成首次加载 | - |
| 边界位置 | 必须在列表顶部/底部 | - |
触发场景:
- 下拉刷新:已滚动到顶部,继续向下拉动超过 50px
- 上拉加载:已滚动到底部,继续向上拉动超过 50px
重置机制:
- 松手后回到正常位置(拉动距离 < 10px)时自动重置
- 下次拉动可再次触发(需满足时间间隔)
高级特性
嵌套滚动支持
当虚拟列表嵌套在另一个 ScrollView 中时(如:横向滚动的 Tab 内包含纵向列表),需要正确处理触摸事件传递。
配置方式:
ts
// 外层列表:开启嵌套支持
outerList.enableNestedSupport = true;
// 内层列表:不开启
innerList.enableNestedSupport = false;工作原理:
- 记录触摸开始位置和滑动方向
- 根据滑动方向判断是否传递事件给父级
- 防止不同方向的滚动冲突
适用场景:
- Tab 页内嵌列表
- 横向滚动容器内的纵向列表
- 多层嵌套滚动结构
程序化滚动事件
调用滚动方法时的事件触发机制:
| 方法 | 触发 onScrolling | 触发 onLoadFinished | 说明 |
|---|---|---|---|
ScrollToIndex | ✅ | ✅ | 滚动过程中持续触发 |
ScrollToTop | ✅ | ✅ | 完成后触发一次 |
ScrollToBottom | ✅ | ✅ | 完成后触发一次 |
ReloadData | ❌ | ✅ | 仅加载完成时触发 |
性能节流机制
onScrolling 回调内置性能优化:
- 高频滚动时自动节流,避免卡顿
- 仅在可见区域变化时刷新节点
- 静止时恢复高刷新率(120fps)
加载状态管理
组件内部维护加载队列,自动处理:
- 分帧加载:数据量大时分多帧创建节点
- 加载队列:按优先级加载可见节点
- 完成回调:所有可见节点创建完毕后触发
onLoadFinished
最佳实践:
ts
list.SetCallbacks({
onItemInit: (node, index) => {
// ✅ 轻量级初始化
node.getComponent(ItemComponent).initStructure();
},
onLoadFinished: () => {
// ✅ 重量级操作放在加载完成后
this.hideLoadingAnimation();
this.playEnterAnimation();
}
});常见问题
Q: onItemInit 和 onItemUpdate 有什么区别?
A:
onItemInit:节点第一次创建时调用,适合做一次性初始化(绑定事件、创建组件引用)onItemUpdate:节点重新显示时调用(从对象池取出),适合更新动态数据(文本、图片、状态)
ts
onItemInit: (node, index) => {
// ✅ 一次性初始化
const item = node.getComponent(ItemComponent);
item.onClick = () => this.handleClick(index);
},
onItemUpdate: (node, index) => {
// ✅ 更新数据
const item = node.getComponent(ItemComponent);
item.setData(this.data[index]);
}Q: 下拉刷新不触发怎么办?
A: 检查以下条件:
- 是否已调用
SetCallbacks设置onPullDownRefresh - 列表是否已完成首次加载(
ReloadData之后) - 是否滚动到了列表顶部
- 拉动距离是否超过 50px
- 距离上次触发是否超过 1 秒
Q: 如何自定义拉动阈值和触发间隔?
A: 这些参数目前是内部配置,如需修改可以通过继承 VirtualViewList 类:
ts
// VirtualViewList.ts 内部
private mPullThreshold: number = 50; // 拉动阈值(px)
private mPullTriggerInterval: number = 1000; // 触发间隔(ms)Q: onScrolling 调用太频繁影响性能怎么办?
A: 组件已内置节流机制。如果仍需进一步优化,可以在回调中自行节流:
ts
private lastScrollUpdate = 0;
onScrolling: (ratio) => {
const now = Date.now();
if (now - this.lastScrollUpdate < 100) return; // 100ms 节流
this.lastScrollUpdate = now;
this.updateScrollUI(ratio);
}更多性能优化技巧请参考 性能优化。