Saltar al contenido
CrossFly es 100% open sourceSíguenos en GitHub
CrossFlyCrossFly
Empezar

Esta página fue traducida automáticamente del portugués y puede contener errores. La versión en portugués es la canónica.

VirtualListView

VirtualListView es una lista virtualizada: crea y renderiza solamente los elementos visibles en la pantalla y recicla los componentes que salen del viewport, al estilo del RecyclerView de Android. Soporta miles (o millones) de elementos con apenas unas pocas decenas de componentes vivos, manteniendo el uso de memoria constante.

La fuente de datos es la interfaz DataAdapter (getCount / createView / bindView). La altura de cada elemento es fija (Relative itemHeight). Incluye callback de “cargar más” (onLoadMore) y spinner de carga.

🔴 · Paquete: dev.crossfly.ui · Extiende: Component · Estilo: VirtualListViewStyle (vía getSkin().virtualListView())

Cuándo usar

  • Listas grandes (cientos de miles de filas) donde crear un componente por elemento es inviable.
  • Desplazamiento infinito con carga incremental (onLoadMore).
  • Paginación remota con caché → combina con PagedDataAdapter.

Cuándo NO usar

  • Listas pequeñas de texto → ListView es más simple y mide la altura exacta.
  • Alturas de elemento realmente variables — la altura aquí es fija (elige un slot que albergue el elemento más grande).

Apariencia

Columna vertical desplazable de elementos de altura uniforme. Durante el desplazamiento, los elementos fuera de pantalla se recogen en un pool y se reaprovechan. Un spinner opcional aparece en el pie (load-more) o en el centro (carga de página).

Ejemplo completo (copiar, pegar y ejecutar)

package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*;   // Relative
import java.util.*;                    // List, ArrayList

/** Pantalla principal ejecutable. Apunta el launcher (classname) y corre: 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);   // las listas usan LAY_FILL (absorben la altura)
   }
}

Los fragmentos siguientes muestran solo el núcleo (lo que va dentro de onShow()). Para ejecutarlos, colócalos en el esqueleto MainWindow de arriba.

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);

Constructores

Constructor Descripción
VirtualListView(Relative itemHeight, DataAdapter adapter) Único constructor. itemHeight es la altura fija de cada elemento; adapter es la fuente de datos

No existe constructor sin argumentos. Si adapter es null, el constructor llama a createAdapter() (que sobrescribes en una subclase). Si el adapter es un PagedDataAdapter, el constructor le inyecta this automáticamente.

DataAdapter

public interface DataAdapter {
   int getCount();                                   // total de ítems
   Component createView() throws Exception;          // crea view en blanco (solo cuando el pool está vacío)
   void bindView(Component view, int index) throws Exception; // vincula datos del índice a la view
}
  • getCount() informa el total — la lista finge una altura count * itemHeight sin materializar todos los elementos.
  • createView() solo se llama cuando el pool de reciclaje está vacío.
  • bindView() se ejecuta muchas veces durante el desplazamiento: mantenlo idempotente y barato.

Propiedades y personalización

Propiedad / método Tipo Predeterminado Qué hace
onLoadMore(Runnable) VirtualListView null Callback disparado al desplazarse cerca del final
loadMoreThreshold(int) VirtualListView 5 Cuántos elementos antes del final disparan el load-more (graba en vlvStyle())
notifyDataChanged() void — Llámalo tras agregar datos (nueva página); re-clampa el scroll y re-vincula el rango
reloadData() void — Llámalo tras alterar el contenido bajo los mismos índices (búsqueda/filtro/sort); limpia caché, desplaza al inicio, re-vincula todo
scrollToIndex(int) void — Centra el elemento en el viewport
scrollToY(float) void — Desplaza a un offset en píxeles (sub-píxel/animable)
getScrollY() float — Offset vertical actual (útil para header colapsable)
setItemHeight(Relative) void — Cambia la altura del elemento y llama a notifyDataChanged()
getItemHeight() int — Altura del elemento en px
replaceView(int, Component) void — Sustituye la view activa de un índice por un componente totalmente nuevo

Callbacks y eventos

Callback Cuándo se dispara
onLoadMore(Runnable) Cuando lastVisible >= count - threshold y no hay carga pendiente

Los eventos de scroll (onPan/onFling/onMouseWheel) son bloqueados mientras el adapter paginado espera datos (isWaitingForData()), evitando desplazarse al vacío.

Estados

  • Reciclando — los elementos fuera del viewport vuelven al pool; al reingresar, son re-vinculados vía bindView.
  • Cargando (load-more) — spinner en el pie, loadMorePending = true hasta notifyDataChanged().
  • Esperando página (con PagedDataAdapter) — scroll trabado hasta que llegue la página.

Lista de 10.000 elementos con 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();   // agrega ítems a la fuente
   list.notifyDataChanged();  // avisa a la lista
});
layout.add(LAY_FILL, list);

Cambio dinámico de adapter vía subclase

No hay setAdapter público en la clase base (el campo adapter es protected). Para cambiar de adapter en tiempo de ejecución, haz una subclase:

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));

Buenas prácticas

  • La altura de elemento es fija — elige un slot que albergue el elemento más grande.
  • Tras agregar datos, llama a notifyDataChanged(); tras alterar el contenido bajo los mismos índices, llama a reloadData() (de lo contrario las filas ya visibles quedan con datos viejos).
  • Si bindView crea un componente nuevo por índice en vez de mutar el reciclado, usa replaceView(...) — o acepta que esas views no vuelven al pool.

Errores comunes

  • Asignar recursos pesados en bindView — se ejecuta en cada evento de scroll. Haz un binding barato.
  • Olvidar notifyDataChanged() tras agregar página — la nueva altura de contenido no se reconoce y el load-more no se resetea.
  • Usar reloadData() cuando solo agregaste — desplaza al inicio; para append usa notifyDataChanged().

Ver también

¿Te resultó útil esta página?