Class ListView

java.lang.Object
limn.scene.Widget
limn.components.ListView
All Implemented Interfaces:
Scrollable

public class ListView extends Widget implements Scrollable
A vertically scrolling list that virtualizes its rows like a JTable/RecyclerView: only the rows in the viewport get a widget, so a list of a million rows costs the same as one of twenty (layout and paint are O(visible)).

Rows may have different heights: e.g. small grouping headers among normal cards. There is no global height cache and no binary search: the list keeps an anchor (which row sits where) and walks a handful of rows from it each frame, measuring each on demand. Scroll-thumb size/position are estimated from the average measured height (good enough for a scroll indicator).

You supply and cache the widgets through an ListView.Adapter: the list asks ListView.Adapter.rowAt(int) for the (already-populated) widget of a row, and hands it back via ListView.Adapter.recycle(limn.scene.Widget) when it scrolls out, so the adapter can pool per row type and rebind, or just create fresh (simplest, no pooling). The list owns only the tree/positioning; the caching policy is yours.

Interaction: the wheel scrolls (with the shared ScrollBar); the list is focusable and, while focused, Up/Down/Home/End/PageUp/PageDown move a highlighted selection (auto-scrolling to reveal it) and Enter activates it; clicking a row selects it (clicks on a row's own buttons reach those buttons).

Size steps propagate rather than being imposed. Rows are adapter-supplied widgets in this list's subtree, so they resolve the ControlSize themselves and list.setControlSize(SMALL) shortens them because they re-measure. Only three metrics are the list's own: the frame-0 row-height seed used before anything has been measured, the intrinsic width under an unbounded constraint, and the selection ring's corner radius.

The scroll bar does not take part in the size axis (ScrollBar.thickness() is 15 pt at every step), and it overlays the rows rather than insetting them, so at a compact step it covers a larger fraction of a shorter row. An accepted cost of one scrollbar geometry process-wide.

  • Constructor Details

    • ListView

      public ListView(ListView.Adapter adapter)
      A list driven by adapter, which supplies and recycles the row widgets.
  • Method Details

    • setBarLayout

      public ListView setBarLayout(ScrollGutters.Layout layout)
      Sets whether the bar floats over the rows or reserves a strip of its own (default ScrollGutters.Layout.OVERLAY). Reserved is what a list of records usually wants: the trailing edge of a row is where a count, a date or a status chip goes, and a thumb over it is a defect. Trailing and not right: the bar and the row's last column are both on the left of a list that reads right to left, and they move there together.
    • barLayout

      public ScrollGutters.Layout barLayout()
      Whether the scrollbar overlays the rows or reserves a gutter.
    • setScrollbarPolicy

      public ListView setScrollbarPolicy(ScrollBar.Policy policy)
      Sets when the vertical scrollbar is shown (default ScrollBar.Policy.AUTO).
    • onSelect

      public ListView onSelect(IntConsumer handler)
      Called with the row index when the selection moves, by click or keyboard.
      Throws:
      NullPointerException - if handler is null, as everywhere else in this set
    • onActivate

      public ListView onActivate(IntConsumer handler)
      Called with the row index on Enter or a double activation, the "open this" gesture.
      Throws:
      NullPointerException - if handler is null
    • selectedIndex

      public int selectedIndex()
      The selected row, or -1 when nothing is selected. A list is one of the two widgets in this set that genuinely has no-selection as a state: clearSelection() reaches it, and a fresh list is in it.
    • rowCount

      public int rowCount()
      Row count as the adapter currently reports it.
    • firstVisibleIndex

      public int firstVisibleIndex()
      Returns:
      the first fully-or-partly visible row index (tests/inspection)
    • refresh

      public void refresh()
      Re-reads the adapter and re-lays out (call after the data changes). If the adapter shrank past the selected row the selection moves to the last row (or is dropped when the list is now empty) and onSelect is told, because a listener showing the selected record would otherwise still be showing a deleted one. UI thread only.
    • setSelectedIndex

      public ListView setSelectedIndex(int index)
      Selects a row, scrolls it into view and fires onSelect; code and a click take the same path, so a listener sees every change either way. Selecting the row that is already selected changes nothing, reveals nothing and fires nothing; that early return is what keeps two controls bound to each other from recursing, so do not remove it. UI thread only.
      Parameters:
      index - a row in [0, rowCount()). -1 is not an argument even though it is what selectedIndex() reports for an empty selection: clearSelection() is how that state is reached.
      Throws:
      IndexOutOfBoundsException - if index is outside that range, an empty list included, where every index is. An index that came from a search which found nothing, or from state saved against longer data, is a caller's bug here, exactly as it is for List.get.
    • clearSelection

      public ListView clearSelection()
      Drops the selection: selectedIndex() becomes -1 and onSelect is fired with it. No-op when nothing is selected. UI thread only.
    • activate

      public void activate()
      Fires onActivate for the selected row, as Enter does.
    • scrollBy

      public void scrollBy(float dy)
      Scrolls by a delta in logical points (positive = toward the end). UI thread only.
    • revealRect

      public void revealRect(float x, float y, float rectWidth, float rectHeight)
      Scrolls the minimum so the rect (in viewport coordinates) becomes visible.
      Specified by:
      revealRect in interface Scrollable
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.

      Resolve the ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget
    • clipsChildren

      protected boolean clipsChildren()
      Description copied from class: Widget
      Whether Widget.paintChildren(limn.graphics.Canvas) clips its children to this widget's bounds (scroll views, list viewports, tab strips). Partial rendering uses it to clamp a descendant's Widget.invalidate() damage to the visible region; a widget scrolled out of view damages nothing. Any override that clips in paintChildren should also override this.
      Overrides:
      clipsChildren in class Widget
    • paintChildren

      protected void paintChildren(Canvas canvas)
      Description copied from class: Widget
      Children pass; override to clip (e.g. scroll views).
      Overrides:
      paintChildren in class Widget
    • hitTest

      public Widget hitTest(float localX, float localY)
      Description copied from class: Widget
      Deepest visible/enabled descendant containing the point (local coords), or this widget itself; null when outside. Later children win because they paint on top.
      Overrides:
      hitTest in class Widget
    • onMouseEvent

      protected void onMouseEvent(MouseEvent event)
      Description copied from class: Widget
      Mouse events (bubbling); call event.consume() when handled.
      Overrides:
      onMouseEvent in class Widget
    • onKeyEvent

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget
    • onFocusGained

      protected void onFocusGained()
      Description copied from class: Widget
      Called when this widget takes keyboard focus. Default: nothing.
      Overrides:
      onFocusGained in class Widget
    • onFocusLost

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget