本页面由葡萄牙语机器翻译,可能存在错误。葡萄牙语版本为准。
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()。
另请参阅
- 领域指南:
ui/· 食谱:cookbook/ - 相关组件:
PagedDataAdapter·ListView