# VirtualListView

> 高性能虚拟化列表，回收利用组件，风格类似 RecyclerView。

Source: https://crossfly.dev/zh/docs/ui/widgets/virtuallistview — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# VirtualListView

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

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

> 🔴 · **包：** `dev.crossfly.ui` · **继承：** `Component` · **样式：** `VirtualListViewStyle`（通过 `getSkin().virtualListView()`）

## 何时使用
- 大型列表（数十万行），为每个项创建一个组件不可行的场景。
- 带增量加载的无限滚动（`onLoadMore`）。
- 带缓存的远程分页 → 与 [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter) 结合使用。

## 何时不要使用
- 小型文本列表 → [`ListView`](/docs/ui/widgets/listview) 更简单且能测量精确高度。
- 真正可变的项高度 —— 这里的高度是固定的（选择一个能容纳最大项的槽位）。

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

## 完整示例（复制、粘贴并运行）
```java
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` 骨架中。

## 基本用法
```java
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`](/docs/ui/widgets/paged-data-adapter)，构造函数会自动将 `this` 注入其中。

### DataAdapter
```java
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 项列表
```java
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，请创建一个子类：
```java
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/`](/docs/ui) · 食谱：[`cookbook/`](/docs/cookbook)
- 相关组件：[`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter) · [`ListView`](/docs/ui/widgets/listview)
