Skip to content

事件与回调

虚拟列表通过 IVirtualListCallbacks 接口暴露核心事件,支持节点渲染、滚动监听、下拉刷新等场景的自定义处理。

回调接口

基础回调

回调触发时机常见用途
onItemInit(node, index)节点首次创建时触发初始化组件、绑定事件、设置静态数据
onItemUpdate(node, index)节点重新进入可见区域时触发更新文本、图片、状态等动态数据
onScrolling(scrollRatio)滚动过程中持续触发更新进度条、吸顶效果、懒加载触发
onLoadFinished()初始化或刷新完成后触发隐藏加载动画、播放入场动画

下拉刷新和上拉加载

回调触发场景返回值
onPullDownRefresh()在列表顶部向下拉动 > 50pxPromise<PullRefreshResult>void
onPullUpLoad()在列表底部向上拉动 > 50pxPromise<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: 检查以下条件:

  1. 是否已调用 SetCallbacks 设置 onPullDownRefresh
  2. 列表是否已完成首次加载(ReloadData 之后)
  3. 是否滚动到了列表顶部
  4. 拉动距离是否超过 50px
  5. 距离上次触发是否超过 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);
}

更多性能优化技巧请参考 性能优化