All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fxmisc.flowless.VirtualFlow Maven / Gradle / Ivy

The newest version!
package org.fxmisc.flowless;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;

import org.reactfx.collection.MemoizationList;
import org.reactfx.util.Lists;
import org.reactfx.value.Val;
import org.reactfx.value.Var;

/**
 * A VirtualFlow is a memory-efficient viewport that only renders enough of its content to completely fill up the
 * viewport through its {@link Navigator}. Based on the viewport's {@link Gravity}, it sequentially lays out the
 * {@link javafx.scene.Node}s of the {@link Cell}s until the viewport is completely filled up or it has no additional
 * cell's nodes to render.
 *
 * 

* Since this viewport does not fully render all of its content, the scroll values are estimates based on the nodes * that are currently displayed in the viewport. If every node that could be rendered is the same width or same * height, then the corresponding scroll values (e.g., scrollX or totalX) are accurate. * Note: the VirtualFlow does not have scroll bars by default. These can be added by wrapping this object * in a {@link VirtualizedScrollPane}. *

* *

* Since the viewport can be used to lay out its content horizontally or vertically, it uses two * orientation-agnostic terms to refer to its width and height: "breadth" and "length," respectively. The viewport * always lays out its {@link Cell cell}'s {@link javafx.scene.Node}s from "top-to-bottom" or from "bottom-to-top" * (these terms should be understood in reference to the viewport's {@link OrientationHelper orientation} and * {@link Gravity}). Thus, its length ("height") is independent as the viewport's bounds are dependent upon * its parent's bounds whereas its breadth ("width") is dependent upon its length. *

* * @param the model content that the {@link Cell#getNode() cell's node} renders * @param the {@link Cell} that can render the model with a {@link javafx.scene.Node}. */ public class VirtualFlow> extends Region implements Virtualized { /** * Determines how the cells in the viewport should be laid out and where any extra unused space should exist * if there are not enough cells to completely fill up the viewport */ public static enum Gravity { /** * If using a {@link VerticalHelper vertical viewport}, lays out the content from top-to-bottom. The first * visible item will appear at the top and the last visible item (or unused space) towards the bottom. *

* If using a {@link HorizontalHelper horizontal viewport}, lays out the content from left-to-right. The first * visible item will appear at the left and the last visible item (or unused space) towards the right. *

*/ FRONT, /** * If using a {@link VerticalHelper vertical viewport}, lays out the content from bottom-to-top. The first * visible item will appear at the bottom and the last visible item (or unused space) towards the top. *

* If using a {@link HorizontalHelper horizontal viewport}, lays out the content from right-to-left. The first * visible item will appear at the right and the last visible item (or unused space) towards the left. *

*/ REAR } /** * Creates a viewport that lays out content horizontally from left to right */ public static > VirtualFlow createHorizontal( ObservableList items, Function cellFactory) { return createHorizontal(items, cellFactory, Gravity.FRONT); } /** * Creates a viewport that lays out content horizontally */ public static > VirtualFlow createHorizontal( ObservableList items, Function cellFactory, Gravity gravity) { return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), gravity); } /** * Creates a viewport that lays out content vertically from top to bottom */ public static > VirtualFlow createVertical( ObservableList items, Function cellFactory) { return createVertical(items, cellFactory, Gravity.FRONT); } /** * Creates a viewport that lays out content vertically from top to bottom */ public static > VirtualFlow createVertical( ObservableList items, Function cellFactory, Gravity gravity) { return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), gravity); } private final ObservableList items; private final OrientationHelper orientation; private final CellListManager cellListManager; private final SizeTracker sizeTracker; private final CellPositioner cellPositioner; private final Navigator navigator; private final StyleableObjectProperty gravity = new StyleableObjectProperty() { @Override public Object getBean() { return VirtualFlow.this; } @Override public String getName() { return "gravity"; } @Override public CssMetaData getCssMetaData() { return GRAVITY; } }; // non-negative private final Var breadthOffset0 = Var.newSimpleVar(0.0); private final Var breadthOffset = breadthOffset0.asVar(this::setBreadthOffset); public Var breadthOffsetProperty() { return breadthOffset; } public Val totalBreadthEstimateProperty() { return sizeTracker.maxCellBreadthProperty(); } private final Var lengthOffsetEstimate; public Var lengthOffsetEstimateProperty() { return lengthOffsetEstimate; } private VirtualFlow( ObservableList items, Function cellFactory, OrientationHelper orientation, Gravity gravity) { this.getStyleClass().add("virtual-flow"); this.items = items; this.orientation = orientation; this.cellListManager = new CellListManager<>(this, items, cellFactory); this.gravity.set(gravity); MemoizationList cells = cellListManager.getLazyCellList(); this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells); this.cellPositioner = new CellPositioner<>(cellListManager, orientation, sizeTracker); this.navigator = new Navigator<>(cellListManager, cellPositioner, orientation, this.gravity, sizeTracker); getChildren().add(navigator); clipProperty().bind(Val.map( layoutBoundsProperty(), b -> new Rectangle(b.getWidth(), b.getHeight()))); lengthOffsetEstimate = new StableBidirectionalVar<>( sizeTracker.lengthOffsetEstimateProperty(), this::setLengthOffset ); // scroll content by mouse scroll this.addEventHandler(ScrollEvent.ANY, se -> { scrollXBy(-se.getDeltaX()); scrollYBy(-se.getDeltaY()); se.consume(); }); } public void dispose() { navigator.dispose(); sizeTracker.dispose(); cellListManager.dispose(); } /** * If the item is out of view, instantiates a new cell for the item. * The returned cell will be properly sized, but not properly positioned * relative to the cells in the viewport, unless it is itself in the * viewport. * * @return Cell for the given item. The cell will be valid only until the * next layout pass. It should therefore not be stored. It is intended to * be used for measurement purposes only. */ public C getCell(int itemIndex) { Lists.checkIndex(itemIndex, items.size()); return cellPositioner.getSizedCell(itemIndex); } /** * This method calls {@link #layout()} as a side-effect to insure * that the VirtualFlow is up-to-date in light of any changes */ public Optional getCellIfVisible(int itemIndex) { // insure cells are up-to-date in light of any changes layout(); return cellPositioner.getCellIfVisible(itemIndex); } /** * This method calls {@link #layout()} as a side-effect to insure * that the VirtualFlow is up-to-date in light of any changes */ public ObservableList visibleCells() { // insure cells are up-to-date in light of any changes layout(); return cellListManager.getLazyCellList().memoizedItems(); } public Val totalLengthEstimateProperty() { return sizeTracker.totalLengthEstimateProperty(); } public Bounds cellToViewport(C cell, Bounds bounds) { return cell.getNode().localToParent(bounds); } public Point2D cellToViewport(C cell, Point2D point) { return cell.getNode().localToParent(point); } public Point2D cellToViewport(C cell, double x, double y) { return cell.getNode().localToParent(x, y); } @Override protected void layoutChildren() { // navigate to the target position and fill viewport while(true) { double oldLayoutBreadth = sizeTracker.getCellLayoutBreadth(); orientation.resize(navigator, oldLayoutBreadth, sizeTracker.getViewportLength()); navigator.layout(); if(oldLayoutBreadth == sizeTracker.getCellLayoutBreadth()) { break; } } double viewBreadth = orientation.breadth(this); double navigatorBreadth = orientation.breadth(navigator); double totalBreadth = breadthOffset0.getValue(); double breadthDifference = navigatorBreadth - totalBreadth; if (breadthDifference < viewBreadth) { // viewport is scrolled all the way to the end of its breadth. // but now viewport size (breadth) has increased double adjustment = viewBreadth - breadthDifference; orientation.relocate(navigator, -(totalBreadth - adjustment), 0); breadthOffset0.setValue(totalBreadth - adjustment); } else { orientation.relocate(navigator, -breadthOffset0.getValue(), 0); } } @Override protected final double computePrefWidth(double height) { switch(getContentBias()) { case HORIZONTAL: // vertical flow return computePrefBreadth(); case VERTICAL: // horizontal flow return computePrefLength(height); default: throw new AssertionError("Unreachable code"); } } @Override protected final double computePrefHeight(double width) { switch(getContentBias()) { case HORIZONTAL: // vertical flow return computePrefLength(width); case VERTICAL: // horizontal flow return computePrefBreadth(); default: throw new AssertionError("Unreachable code"); } } private double computePrefBreadth() { return 100; } private double computePrefLength(double breadth) { return 100; } @Override public final Orientation getContentBias() { return orientation.getContentBias(); } void scrollLength(double deltaLength) { setLengthOffset(lengthOffsetEstimate.getValue() + deltaLength); } void scrollBreadth(double deltaBreadth) { setBreadthOffset(breadthOffset0.getValue() + deltaBreadth); } /** * Scroll the content horizontally by the given amount. * * @param deltaX positive value scrolls right, negative value scrolls left */ @Override public void scrollXBy(double deltaX) { orientation.scrollHorizontallyBy(this, deltaX); } /** * Scroll the content vertically by the given amount. * * @param deltaY positive value scrolls down, negative value scrolls up */ @Override public void scrollYBy(double deltaY) { orientation.scrollVerticallyBy(this, deltaY); } /** * Scroll the content horizontally to the pixel * * @param pixel - the pixel position to which to scroll */ @Override public void scrollXToPixel(double pixel) { orientation.scrollHorizontallyToPixel(this, pixel); } /** * Scroll the content vertically to the pixel * * @param pixel - the pixel position to which to scroll */ @Override public void scrollYToPixel(double pixel) { orientation.scrollVerticallyToPixel(this, pixel); } @Override public Val totalWidthEstimateProperty() { return orientation.widthEstimateProperty(this); } @Override public Val totalHeightEstimateProperty() { return orientation.heightEstimateProperty(this); } @Override public Var estimatedScrollXProperty() { return orientation.estimatedScrollXProperty(this); } @Override public Var estimatedScrollYProperty() { return orientation.estimatedScrollYProperty(this); } /** * Hits this virtual flow at the given coordinates. * * @param x x offset from the left edge of the viewport * @param y y offset from the top edge of the viewport * @return hit info containing the cell that was hit and coordinates * relative to the cell. If the hit was before the cells (i.e. above a * vertical flow content or left of a horizontal flow content), returns * a hit before cells containing offset from the top left corner * of the content. If the hit was after the cells (i.e. below a vertical * flow content or right of a horizontal flow content), returns a * hit after cells containing offset from the top right corner of * the content of a horizontal flow or bottom left corner of the content of * a vertical flow. */ public VirtualFlowHit hit(double x, double y) { double bOff = orientation.getX(x, y); double lOff = orientation.getY(x, y); bOff += breadthOffset0.getValue(); if(items.isEmpty()) { return orientation.hitAfterCells(bOff, lOff); } layout(); int firstVisible = getFirstVisibleIndex(); firstVisible = navigator.fillBackwardFrom0(firstVisible, lOff); C firstCell = cellPositioner.getVisibleCell(firstVisible); int lastVisible = getLastVisibleIndex(); lastVisible = navigator.fillForwardFrom0(lastVisible, lOff); C lastCell = cellPositioner.getVisibleCell(lastVisible); if(lOff < orientation.minY(firstCell)) { return orientation.hitBeforeCells(bOff, lOff - orientation.minY(firstCell)); } else if(lOff >= orientation.maxY(lastCell)) { return orientation.hitAfterCells(bOff, lOff - orientation.maxY(lastCell)); } else { for(int i = firstVisible; i <= lastVisible; ++i) { C cell = cellPositioner.getVisibleCell(i); if(lOff < orientation.maxY(cell)) { return orientation.cellHit(i, cell, bOff, lOff - orientation.minY(cell)); } } throw new AssertionError("unreachable code"); } } /** * Forces the viewport to acts as though it scrolled from 0 to {@code viewportOffset}). Note: the * viewport makes an educated guess as to which cell is actually at {@code viewportOffset} if the viewport's * entire content was completely rendered. * * @param viewportOffset See {@link OrientationHelper} and its implementations for explanation on what the offset * means based on which implementation is used. */ public void show(double viewportOffset) { if(viewportOffset < 0) { navigator.scrollCurrentPositionBy(viewportOffset); } else if(viewportOffset > sizeTracker.getViewportLength()) { navigator.scrollCurrentPositionBy(viewportOffset - sizeTracker.getViewportLength()); } else { // do nothing, offset already in the viewport } } /** * Forces the viewport to show the given item by "scrolling" to it */ public void show(int itemIdx) { navigator.setTargetPosition(new MinDistanceTo(itemIdx)); } /** * Forces the viewport to show the given item as the first visible item as determined by its {@link Gravity}. */ public void showAsFirst(int itemIdx) { navigator.setTargetPosition(new StartOffStart(itemIdx, 0.0)); } /** * Forces the viewport to show the given item as the last visible item as determined by its {@link Gravity}. */ public void showAsLast(int itemIdx) { navigator.setTargetPosition(new EndOffEnd(itemIdx, 0.0)); } /** * Forces the viewport to show the given item by "scrolling" to it and then further "scrolling" by {@code offset} * in one layout call (e.g., this method does not "scroll" twice) * * @param offset the offset value as determined by the viewport's {@link OrientationHelper}. */ public void showAtOffset(int itemIdx, double offset) { navigator.setTargetPosition(new StartOffStart(itemIdx, offset)); } /** * Forces the viewport to show the given item by "scrolling" to it and then further "scrolling," so that the * {@code region} is visible, in one layout call (e.g., this method does not "scroll" twice). */ public void show(int itemIndex, Bounds region) { navigator.showLengthRegion(itemIndex, orientation.minY(region), orientation.maxY(region)); showBreadthRegion(orientation.minX(region), orientation.maxX(region)); } /** * Get the index of the first visible cell (at the time of the last layout). * * @return The index of the first visible cell */ public int getFirstVisibleIndex() { return navigator.getFirstVisibleIndex(); } /** * Get the index of the last visible cell (at the time of the last layout). * * @return The index of the last visible cell */ public int getLastVisibleIndex() { return navigator.getLastVisibleIndex(); } private void showBreadthRegion(double fromX, double toX) { double bOff = breadthOffset0.getValue(); double spaceBefore = fromX - bOff; double spaceAfter = sizeTracker.getViewportBreadth() - toX + bOff; if(spaceBefore < 0 && spaceAfter > 0) { double shift = Math.min(-spaceBefore, spaceAfter); setBreadthOffset(bOff - shift); } else if(spaceAfter < 0 && spaceBefore > 0) { double shift = Math.max(spaceAfter, -spaceBefore); setBreadthOffset(bOff - shift); } } void setLengthOffset(double pixels) { double total = totalLengthEstimateProperty().getOrElse(0.0); double length = sizeTracker.getViewportLength(); double max = Math.max(total - length, 0); double current = lengthOffsetEstimate.getValue(); if(pixels > max) pixels = max; if(pixels < 0) pixels = 0; double diff = pixels - current; if(diff == 0) { // do nothing } else if(Math.abs(diff) <= length) { // distance less than one screen navigator.scrollCurrentPositionBy(diff); } else { jumpToAbsolutePosition(pixels); } } void setBreadthOffset(double pixels) { double total = totalBreadthEstimateProperty().getValue(); double breadth = sizeTracker.getViewportBreadth(); double max = Math.max(total - breadth, 0); double current = breadthOffset0.getValue(); if(pixels > max) pixels = max; if(pixels < 0) pixels = 0; if(pixels != current) { breadthOffset0.setValue(pixels); requestLayout(); // TODO: could be safely relocated right away? // (Does relocation request layout?) } } private void jumpToAbsolutePosition(double pixels) { if(items.isEmpty()) { return; } // guess the first visible cell and its offset in the viewport double avgLen = sizeTracker.getAverageLengthEstimate().orElse(0.0); if(avgLen == 0.0) return; int first = (int) Math.floor(pixels / avgLen); double firstOffset = -(pixels % avgLen); if(first < items.size()) { navigator.setTargetPosition(new StartOffStart(first, firstOffset)); } else { navigator.setTargetPosition(new EndOffEnd(items.size() - 1, 0.0)); } } /** * The gravity of the virtual flow. When there are not enough cells to fill * the full height (vertical virtual flow) or width (horizontal virtual flow), * the cells are placed either at the front (vertical: top, horizontal: left), * or rear (vertical: bottom, horizontal: right) of the virtual flow, depending * on the value of the gravity property. * * The gravity can also be styled in CSS, using the "-flowless-gravity" property, * for example: *
.virtual-flow { -flowless-gravity: rear; }
*/ public ObjectProperty gravityProperty() { return gravity; } public Gravity getGravity() { return gravity.get(); } public void setGravity(Gravity gravity) { this.gravity.set(gravity); } @SuppressWarnings("unchecked") // Because of the cast we have to perform, below private static final CssMetaData GRAVITY = new CssMetaData( "-flowless-gravity", // JavaFX seems to have an odd return type on getEnumConverter: "? extends Enum", not E as the second generic type. // Even though if you look at the source, the EnumConverter type it uses does have the type E. // To get round this, we cast on return: (StyleConverter) StyleConverter.getEnumConverter(Gravity.class), Gravity.FRONT) { @Override public boolean isSettable(VirtualFlow virtualFlow) { return !virtualFlow.gravity.isBound(); } @Override public StyleableProperty getStyleableProperty(VirtualFlow virtualFlow) { return virtualFlow.gravity; } }; private static final List> STYLEABLES; static { List> styleables = new ArrayList<>(Region.getClassCssMetaData()); styleables.add(GRAVITY); STYLEABLES = Collections.unmodifiableList(styleables); } public static List> getClassCssMetaData() { return STYLEABLES; } @Override public List> getCssMetaData() { return getClassCssMetaData(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy