跳到内容
CrossFly 是 100% 开源在 GitHub 关注我们
CrossFlyCrossFly
开始使用

本页面由葡萄牙语机器翻译,可能存在错误。葡萄牙语版本为准。

VirtualListView

VirtualListView 是一个虚拟化列表:它仅创建并渲染屏幕上可见的项,并回收离开视口的组件,风格类似 Android 的 RecyclerView。它以仅数十个活跃组件即可支持数千(甚至数百万)个项,同时保持内存占用恒定。

数据源是 DataAdapter 接口(getCount / createView / bindView)。每个项的高度是固定的(Relative itemHeight)。它包含“加载更多”回调(onLoadMore)和加载中的旋转指示器。

🔴 · 包: dev.crossfly.ui · 继承: Component · 样式: VirtualListViewStyle(通过 getSkin().virtualListView())

何时使用

  • 大型列表(数十万行),为每个项创建一个组件不可行的场景。
  • 带增量加载的无限滚动(onLoadMore)。
  • 带缓存的远程分页 → 与 PagedDataAdapter 结合使用。

何时不要使用

  • 小型文本列表 → ListView 更简单且能测量精确高度。
  • 真正可变的项高度 —— 这里的高度是固定的(选择一个能容纳最大项的槽位)。

外观

一列可滚动的、高度统一的垂直项列表。滚动过程中,离开屏幕的项被收集到一个池中并被复用。可选的旋转指示器出现在底部(加载更多)或中央(页面加载)。

完整示例(复制、粘贴并运行)

package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*;   // Relative
import java.util.*;                    // List, ArrayList

/** 可执行的主界面。将 launcher 指向此类(classname)并运行:gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      final List<String> items = new ArrayList<>();
      for (int i = 0; i < 10_000; i++)
         items.add("Item " + i);

      VirtualListView list = new VirtualListView(Relative.ofFont(2f),
         new VirtualListView.DataAdapter() {
            public int getCount() { return items.size(); }
            public Component createView() { return new Label(""); }
            public void bindView(Component v, int index) {
               ((Label) v).setText(items.get(index));
            }
         });

      add(LAY_FILL, list);   // 列表使用 LAY_FILL(占满高度)
   }
}

下面的片段仅展示核心部分(放入 onShow() 内的内容)。要运行它们,请将其放入上面的 MainWindow 骨架中。

基本用法

VirtualListView list = new VirtualListView(Relative.ofFont(2f),
   new VirtualListView.DataAdapter() {
      public int getCount() { return items.size; }
      public Component createView() { return new Label(""); }
      public void bindView(Component v, int index) {
         ((Label) v).setText("Item " + index);
      }
   });
layout.add(LAY_FILL, list);

构造函数

构造函数 描述
VirtualListView(Relative itemHeight, DataAdapter adapter) 唯一的构造函数。itemHeight 是每个项的固定高度;adapter 是数据源

不存在无参数构造函数。 如果 adapter 为 null,构造函数会调用 createAdapter()(你在子类中重写它)。如果 adapter 是 PagedDataAdapter,构造函数会自动将 this 注入其中。

DataAdapter

public interface DataAdapter {
   int getCount();                                   // 项目总数
   Component createView() throws Exception;          // 创建空白 view(仅在 pool 为空时)
   void bindView(Component view, int index) throws Exception; // 将索引处的数据绑定到 view
}
  • getCount() 告知总数 —— 列表假装具有 count * itemHeight 的高度,而无需将所有项实体化。
  • createView() 仅在回收池为空时被调用。
  • bindView() 在滚动过程中会运行很多次:保持它幂等且轻量。

属性与自定义

属性 / 方法 类型 默认值 作用
onLoadMore(Runnable) VirtualListView null 滚动接近末尾时触发的回调
loadMoreThreshold(int) VirtualListView 5 距末尾多少个项时触发加载更多(写入 vlvStyle())
notifyDataChanged() void — 在追加数据(新页)后调用;重新钳制滚动并重新绑定可见范围
reloadData() void — 在相同索引下更改内容后调用(搜索/过滤/排序);清除缓存、滚动到顶部、重新绑定全部
scrollToIndex(int) void — 将该项在视口中居中
scrollToY(float) void — 滚动到以像素为单位的偏移量(亚像素/可动画)
getScrollY() float — 当前垂直偏移量(对可折叠头部很有用)
setItemHeight(Relative) void — 更换项高度并调用 notifyDataChanged()
getItemHeight() int — 项高度(以像素为单位)
replaceView(int, Component) void — 用一个全新的组件替换某个索引的活跃视图

回调与事件

回调 触发时机
onLoadMore(Runnable) 当 lastVisible >= count - threshold 且无待处理的加载时

滚动事件(onPan/onFling/onMouseWheel)在分页 adapter 等待数据时被阻断(isWaitingForData()),避免滚动到空白处。

状态

  • 回收中 —— 视口外的项返回池中;重新进入时通过 bindView 重新绑定。
  • 加载中(加载更多) —— 底部有旋转指示器,loadMorePending = true 直到 notifyDataChanged()。
  • 等待页面(使用 PagedDataAdapter)—— 滚动锁定直到页面到达。

带加载更多的 10,000 项列表

VirtualListView list = new VirtualListView(Relative.ofFont(2f),
   new VirtualListView.DataAdapter() {
      public int getCount() { return items.size; }
      public Component createView() { return new Label(""); }
      public void bindView(Component v, int index) {
         ((Label) v).setText("Item " + index);
      }
   });
list.onLoadMore(() -> {
   carregarProximaPagina();   // 向数据源追加项目
   list.notifyDataChanged();  // 通知列表
});
layout.add(LAY_FILL, list);

通过子类动态更换 adapter

基类中没有公开的 setAdapter(adapter 字段是 protected)。要在运行时更换 adapter,请创建一个子类:

class RowList extends VirtualListView {
   RowList() { super(Relative.ofFont(2f), null); }
   void setAdapter(DataAdapter a, Relative h) { adapter = a; setItemHeight(h); }
}

RowList vsc = new RowList();
final int count = 2000;
vsc.setAdapter(new VirtualListView.DataAdapter() {
   public int getCount() { return count; }
   public Component createView() { return new Label(""); }
   public void bindView(Component v, int index) {
      ((Label) v).setText("Row id: " + index);
   }
}, Relative.ofPixels(100));

最佳实践

  • 项高度是固定的 —— 选择一个能容纳最大项的槽位。
  • 追加数据后,调用 notifyDataChanged();在相同索引下更改内容后,调用 reloadData()(否则已可见的行会保留旧数据)。
  • 如果 bindView 为每个索引创建一个新组件而不是变更回收的组件,请使用 replaceView(...) —— 或者接受这些视图不会返回池中。

常见错误

  • 在 bindView 中分配重量级资源 —— 它在每个滚动事件时运行。让绑定保持轻量。
  • 添加页面后忘记 notifyDataChanged() —— 新的内容高度不被识别,且加载更多不会重置。
  • 仅追加时却使用 reloadData() —— 它会滚动到顶部;追加请使用 notifyDataChanged()。

另请参阅

这个页面有帮助吗?