Pular para o conteúdo
CrossFly é 100% open sourceSiga no GitHub
CrossFlyCrossFly
Começar

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 (via getSkin().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 esqueleto MainWindow acima.

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 adapter for null, o construtor chama createAdapter() (que você sobrescreve numa subclasse). Se o adapter for um PagedDataAdapter, o construtor injeta this nele 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 altura count * itemHeight sem 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 = true até 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, chame reloadData() (senão as linhas já visíveis ficam com dados velhos).
  • Se bindView criar um componente novo por índice em vez de mutar o reciclado, use replaceView(...) — 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 use notifyDataChanged().

Veja também

Esta página foi útil?