# ListView

> 可滚动、可嵌入其他布局的文本项列表，支持单选与多选两种模式。

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

# ListView

`ListView` 是一个可滚动的**文本**项选择列表，可随时嵌入任意容器。每一项都会变成一个可点击的“卡片”。选择模式在构造函数中通过 `ItemType` 固定：`SINGLE`（点击即选择并确认）或 `MULTIPLE`（复选框 + “确认”按钮）。

与 `VirtualListView` 不同，`ListView` **为每一项创建一个组件**（不回收）—— 适合小/中型列表（数十项），而不适合数千行。

> 🟡 · **包：** `dev.crossfly.ui` · **继承自：** `Component` · **样式：** `ListViewStyle`（项/表头/按钮）+ `WindowStyle`（背景，通过 `selectionWindowStyle()`）

## 何时使用
- 屏幕内嵌入的选择器（选项菜单、筛选器、类别选择）。
- 数十个文本项，且无需滚动的精确高度很有用（`computeIdealHeight`）。
- 需要显式确认的多选（`MULTIPLE`）。

## 何时不要使用
- 大型列表（数百/数千项）→ 使用会回收组件的 [`VirtualListView`](/docs/ui/widgets/virtuallistview)。
- 模态弹出选择 → 使用 [`ListViewWindow`](/docs/ui/widgets/listviewwindow)，它将 `ListView` 包裹在 `Window` 中。
- 带图片/多字段的丰富行 → 使用 `itemFactory(...)` 或用[列表项](/docs/ui/widgets/list-items)手动构建列表。

## 外观
居中文本卡片的垂直列（column），带可选表头（标题 + 返回图标），在 `MULTIPLE` 模式下底部还有一个“确认”按钮。在 `SINGLE` 模式下每个卡片是 `LabelItem`；在 `MULTIPLE` 模式下是 `CheckLabelItem`（复选框 + 标签）。

## 完整示例（复制、粘贴并运行）
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;

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

      ListView list = new ListView(new String[]{"Um", "Dois", "Três"}, ListView.ItemType.SINGLE)
         .showHeader(false);
      list.onConfirm(e -> System.out.println("Escolhido: " + list.getSelectedText()));

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

> 下面的片段**只展示核心部分**（放在 `onShow()` 里的内容）。要运行它们，请放入上面的 `MainWindow` 骨架中。

## 基本用法
```java
// SINGLE，无表头，自动高度（不滚动）
ListView list = new ListView(new String[]{"Um", "Dois", "Três"}, ListView.ItemType.SINGLE)
   .showHeader(false);
list.onConfirm(e -> System.out.println("Escolhido: " + list.getSelectedText()));
contents.add(.95f, list);
```

## 构造函数
| 构造函数 | 描述 |
|---|---|
| `ListView(String[] items, ItemType itemType)` | 无标题列表 |
| `ListView(String title, String[] items, ItemType itemType)` | 表头带标题的列表 |

`ItemType`（定义于 `UIConstants`）：`SINGLE` 或 `MULTIPLE`。构造之后该模式**不可变**。

## 属性与自定义
| 属性 / 方法 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| `addItem(String)` | `ListView` | — | 添加一项（惰性重建；忽略 `null`） |
| `addItems(String...)` | `ListView` | — | 添加多项（仅一次失效） |
| `showHeader(boolean)` | `ListView` | `true` | 显示/隐藏表头（标题 + 返回） |
| `title(String)` | `ListView` | `null` | 设置标题 |
| `confirmText(String)` | `ListView` | `"Confirmar"` | 按钮文本（仅 `MULTIPLE`） |
| `itemStyle(ComponentStyle)` | `ListView` | 主题 | 覆盖所有项的样式 |
| `confirmButtonStyle(ComponentStyle)` | `ListView` | 主题 | 确认按钮的样式 |
| `headerStyle(LabelStyle)` | `ListView` | 主题 | 表头样式 |
| `itemFactory(ItemFactory)` | `ListView` | `null` | 自定义项工厂（接收 `text`、`index`） |
| `closeAction(Runnable)` | `ListView` | `null` | 关闭动作（`ListViewWindow` 将其指向 `this::hide`） |
| `computeIdealHeight(int width)` | `int` | — | 无滚动的精确高度，若存在 `itemFactory` 则返回 `-1` |

### 选择查询
`getSelectedCount()`、`getSelectedIndex()`、`getSelectedText()`、`getSelectedIndices()`、`getSelectedTexts()`、`isSelected(int)`、`getSelectionState()`（`boolean[]`）。

## 回调与事件
| 回调 | 触发时机 |
|---|---|
| `onConfirm(Component.Action)` | `SINGLE`：点击某项时。`MULTIPLE`：点击“确认”时 |

在 `SINGLE` 模式下，点击会清除之前的选择、标记该索引、触发 `onConfirm` 并调用 `close()`。在 `MULTIPLE` 模式下，点击会切换该项的复选框；确认由按钮发出。

## 状态
- **SINGLE** —— 一次只标记一项；点击即确认并关闭。
- **MULTIPLE** —— 可标记多项；即使在确认前也可通过 `getSelectionState()` 查询状态。
- 表头和底部按钮是可选的（取决于 `showHeader`/`title` 和 `ItemType`）。

### 带自动高度的 MULTIPLE
```java
ListView multi = new ListView(new String[]{"Vermelho", "Verde", "Azul"},
      ListView.ItemType.MULTIPLE)
   .showHeader(false)
   .confirmText("Aplicar");
multi.getStyle().autoHeight(Relative.ofFont(9f));
multi.onConfirm(e -> {
   for (String t : multi.getSelectedTexts())
      System.out.println("marcado: " + t);
});
contents.add(.95f, multi);
```

### 使用 `itemFactory` 的自定义行
```java
ListView list = new ListView(labels, ListView.ItemType.SINGLE).showHeader(false);
list.itemFactory((text, index) -> {
   Component row = new Component();
   row.style(Styles.component().pressable().ingap(Gap.ofFont(.3f)));
   row.setLayout(new GridLayout(Position.START, Position.CENTER));
   row.add(new Label(text));
   return row;
});
list.onConfirm(e -> System.out.println("idx = " + list.getSelectedIndex()));
contents.add(.95f, list);
```

## 最佳实践
- 优先使用 `addItems(...)` 而不是多次 `addItem(...)` —— 只触发一次布局失效。
- 当你想要一个**无滚动**且高度精确的列表时使用 `computeIdealHeight(width)`（若存在 `itemFactory` 则返回 `-1`，因为自定义项无法被通用地度量）。
- 项的重建是惰性的（发生在下一次布局时），包括宽度变化时。

## 常见错误
- **将 `ListView` 用于数千项** —— 它不回收；内存呈线性增长。请迁移到 [`VirtualListView`](/docs/ui/widgets/virtuallistview)。
- **期望 `MULTIPLE` 每次点击都触发 `onConfirm`** —— 在 `MULTIPLE` 模式下回调仅在按钮“确认”时触发；用 `getSelectionState()` 来响应每次标记。
- **假设存在无项的构造函数** —— 构造函数要求提供 `items` 和 `ItemType`；请以 `new String[]{}` 起步并使用 `addItems(...)`。

## 另请参阅
- 领域指南：[`ui/`](/docs/ui) · 实用配方：[`cookbook/`](/docs/cookbook)
- 相关组件：[`ListViewWindow`](/docs/ui/widgets/listviewwindow) · [`VirtualListView`](/docs/ui/widgets/virtuallistview) · [列表项（`LabelItem`/`CheckLabelItem`）](/docs/ui/widgets/list-items)
