VirtualListView
VirtualListView é uma lista virtualizada: ela cria e renderiza somente os itens visíveis na tela e recicla os componentes que saem da viewport, à moda do RecyclerView do Android. Suporta milhares (ou milhões) de itens com poucas dezenas de componentes vivos, mantendo o uso de memória constante.
A fonte de dados é a interface DataAdapter (getCount / createView / bindView). A altura de cada item é fixa (Relative itemHeight). Inclui callback de “carregar mais” (onLoadMore) e spinner de carregamento.
🔴 · Pacote:
dev.crossfly.ui· Estende:Component· Estilo:VirtualListViewStyle(viagetSkin().virtualListView())
Quando usar
- Listas grandes (centenas de milhares de linhas) onde criar um componente por item é inviável.
- Rolagem infinita com carregamento incremental (
onLoadMore). - Paginação remota com cache → combine com
PagedDataAdapter.
Quando NÃO usar
- Listas pequenas de texto →
ListViewé mais simples e mede altura exata. - Alturas de item variáveis de verdade — a altura aqui é fixa (escolha um slot que comporte o maior item).
Aparência
Coluna vertical rolável de itens de altura uniforme. Durante o scroll, itens fora de tela são recolhidos para um pool e reaproveitados. Um spinner opcional aparece no rodapé (load-more) ou no centro (carregamento de página).
Exemplo completo (copiar, colar e rodar)
package com.exemplo;
import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*; // Relative
import java.util.*; // List, ArrayList
/** Tela principal executável. Aponte o launcher (classname) e rode: 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); // listas usam LAY_FILL (absorvem a altura)
}
}
Os trechos abaixo mostram só o miolo (o que vai dentro de
onShow()). Para rodar, coloque-os no esqueletoMainWindowacima.
Uso básico
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);
Construtores
| Construtor | Descrição |
|---|---|
VirtualListView(Relative itemHeight, DataAdapter adapter) |
Único construtor. itemHeight é a altura fixa de cada item; adapter é a fonte de dados |
Não existe construtor sem argumentos. Se
adapterfornull, o construtor chamacreateAdapter()(que você sobrescreve numa subclasse). Se o adapter for umPagedDataAdapter, o construtor injetathisnele automaticamente.
DataAdapter
public interface DataAdapter {
int getCount(); // total de itens
Component createView() throws Exception; // cria view em branco (só quando o pool está vazio)
void bindView(Component view, int index) throws Exception; // vincula dados do índice à view
}
getCount()informa o total — a lista finge uma alturacount * itemHeightsem materializar todos os itens.createView()só é chamado quando o pool de reciclagem está vazio.bindView()roda muitas vezes durante o scroll: mantenha-o idempotente e barato.
Propriedades e personalização
| Propriedade / método | Tipo | Padrão | O que faz |
|---|---|---|---|
onLoadMore(Runnable) |
VirtualListView |
null |
Callback disparado ao rolar perto do fim |
loadMoreThreshold(int) |
VirtualListView |
5 |
Quantos itens antes do fim disparam o load-more (grava em vlvStyle()) |
notifyDataChanged() |
void |
— | Chame após acrescentar dados (nova página); re-clampa scroll e re-vincula a faixa |
reloadData() |
void |
— | Chame após alterar o conteúdo sob os mesmos índices (busca/filtro/sort); limpa cache, rola ao topo, re-vincula tudo |
scrollToIndex(int) |
void |
— | Centraliza o item na viewport |
scrollToY(float) |
void |
— | Rola para um offset em pixels (sub-pixel/animável) |
getScrollY() |
float |
— | Offset vertical atual (útil para header colapsável) |
setItemHeight(Relative) |
void |
— | Troca a altura do item e chama notifyDataChanged() |
getItemHeight() |
int |
— | Altura do item em px |
replaceView(int, Component) |
void |
— | Substitui a view ativa de um índice por um componente totalmente novo |
Callbacks e eventos
| Callback | Quando dispara |
|---|---|
onLoadMore(Runnable) |
Quando lastVisible >= count - threshold e não há carregamento pendente |
Eventos de scroll (onPan/onFling/onMouseWheel) são bloqueados enquanto o adapter paginado aguarda dados (isWaitingForData()), evitando rolar para o vazio.
Estados
- Reciclando — itens fora da viewport voltam ao pool; ao reentrarem, são re-vinculados via
bindView. - Carregando (load-more) — spinner no rodapé,
loadMorePending = trueaténotifyDataChanged(). - Aguardando página (com
PagedDataAdapter) — scroll travado até a página chegar.
Lista de 10.000 itens com load-more
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(); // acrescenta itens à fonte
list.notifyDataChanged(); // avisa a lista
});
layout.add(LAY_FILL, list);
Troca dinâmica de adapter via subclasse
Não há setAdapter público na classe base (o campo adapter é protected). Para trocar de adapter em tempo de execução, faça uma subclasse:
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));
Boas práticas
- Altura de item é fixa — escolha um slot que comporte o maior item.
- Após acrescentar dados, chame
notifyDataChanged(); após alterar o conteúdo sob os mesmos índices, chamereloadData()(senão as linhas já visíveis ficam com dados velhos). - Se
bindViewcriar um componente novo por índice em vez de mutar o reciclado, usereplaceView(...)— ou aceite que essas views não voltam ao pool.
Erros comuns
- Alocar recursos pesados em
bindView— ele roda a cada evento de scroll. Faça binding barato. - Esquecer
notifyDataChanged()após adicionar página — a nova altura de conteúdo não é reconhecida e o load-more não reseta. - Usar
reloadData()quando só acrescentou — ele rola ao topo; para append usenotifyDataChanged().
Veja também
- Guia da área:
ui/· Receitas:cookbook/ - Componentes relacionados:
PagedDataAdapter·ListView