diff --git a/src/main/java/org/fxmisc/flowless/Cell.java b/src/main/java/org/fxmisc/flowless/Cell.java
index 662cd3b..e1db7de 100644
--- a/src/main/java/org/fxmisc/flowless/Cell.java
+++ b/src/main/java/org/fxmisc/flowless/Cell.java
@@ -33,6 +33,20 @@ default boolean isReusable() {
return false;
}
+ /**
+ * If this cell is reusable (as indicated by {@link #isReusable()}),
+ * this method is called to display a different item. {@link #reset()}
+ * will have been called before a call to this method.
+ *
+ *
The default implementation calls {@link #updateItem(Object)}
+ *
+ * @param index the item's position in the VirtualFlow list
+ * @param item the new item to display
+ */
+ default void update(Integer index, T item) {
+ updateItem(item);
+ }
+
/**
* If this cell is reusable (as indicated by {@link #isReusable()}),
* this method is called to display a different item. {@link #reset()}
diff --git a/src/main/java/org/fxmisc/flowless/CellListManager.java b/src/main/java/org/fxmisc/flowless/CellListManager.java
index 5394466..681f117 100644
--- a/src/main/java/org/fxmisc/flowless/CellListManager.java
+++ b/src/main/java/org/fxmisc/flowless/CellListManager.java
@@ -1,7 +1,7 @@
package org.fxmisc.flowless;
import java.util.Optional;
-import java.util.function.Function;
+import java.util.function.BiFunction;
import javafx.collections.ObservableList;
import javafx.scene.Node;
@@ -31,10 +31,10 @@ final class CellListManager> {
public CellListManager(
Node owner,
ObservableList items,
- Function super T, ? extends C> cellFactory) {
+ BiFunction cellFactory) {
this.owner = owner;
this.cellPool = new CellPool<>(cellFactory);
- this.cells = LiveList.map(items, this::cellForItem).memoize();
+ this.cells = new IndexedMappedList<>(items, this::cellForItem).memoize();
this.presentCells = cells.memoizedItems();
this.cellNodes = presentCells.map(Cell::getNode);
this.presentCellsSubscription = presentCells.observeQuasiModifications(this::presentCellsChanged);
@@ -95,8 +95,8 @@ public void cropTo(int fromItem, int toItem) {
}
}
- private C cellForItem(T item) {
- C cell = cellPool.getCell(item);
+ private C cellForItem(Integer index, T item) {
+ C cell = cellPool.getCell(index, item);
// apply CSS when the cell is first added to the scene
Node node = cell.getNode();
@@ -132,7 +132,7 @@ private C cellForItem(T item) {
* has moved out of the viewport and is thus removed from
* the Navigator's children list. This breaks expected trackpad
* scrolling behaviour, at least on macOS.
- *
+ *
* So here we take over event-bubbling duties for ScrollEvent
* and push them ourselves directly to the given owner.
*/
diff --git a/src/main/java/org/fxmisc/flowless/CellPool.java b/src/main/java/org/fxmisc/flowless/CellPool.java
index d3cddf1..499b27c 100644
--- a/src/main/java/org/fxmisc/flowless/CellPool.java
+++ b/src/main/java/org/fxmisc/flowless/CellPool.java
@@ -2,17 +2,17 @@
import java.util.LinkedList;
import java.util.Queue;
-import java.util.function.Function;
+import java.util.function.BiFunction;
/**
* Helper class that stores a pool of reusable cells that can be updated via {@link Cell#updateItem(Object)} or
* creates new ones via its {@link #cellFactory} if the pool is empty.
*/
final class CellPool> {
- private final Function super T, ? extends C> cellFactory;
+ private final BiFunction cellFactory;
private final Queue pool = new LinkedList<>();
- public CellPool(Function super T, ? extends C> cellFactory) {
+ public CellPool(BiFunction cellFactory) {
this.cellFactory = cellFactory;
}
@@ -20,12 +20,17 @@ public CellPool(Function super T, ? extends C> cellFactory) {
* Returns a reusable cell that has been updated with the current item if the pool has one, or returns a
* newly-created one via its {@link #cellFactory}.
*/
- public C getCell(T item) {
+ public C getCell(Integer index, T item) {
C cell = pool.poll();
if(cell != null) {
- cell.updateItem(item);
+ // Note that update(index,item) invokes
+ // cell.updateItem(item) by default
+ cell.update(index, item);
} else {
- cell = cellFactory.apply(item);
+ // Note that cellFactory may just be a wrapper:
+ // (index, item) -> wrappedFactory.apply(item)
+ // See the various VirtualFlow creation methods
+ cell = cellFactory.apply(index, item);
}
return cell;
}
diff --git a/src/main/java/org/fxmisc/flowless/IndexedMappedList.java b/src/main/java/org/fxmisc/flowless/IndexedMappedList.java
new file mode 100644
index 0000000..90e5718
--- /dev/null
+++ b/src/main/java/org/fxmisc/flowless/IndexedMappedList.java
@@ -0,0 +1,68 @@
+package org.fxmisc.flowless;
+
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.reactfx.Subscription;
+import org.reactfx.collection.LiveList;
+import org.reactfx.collection.LiveListBase;
+import org.reactfx.collection.QuasiListChange;
+import org.reactfx.collection.QuasiListModification;
+import org.reactfx.collection.UnmodifiableByDefaultLiveList;
+import org.reactfx.util.Lists;
+
+import javafx.collections.ObservableList;
+
+public class IndexedMappedList extends LiveListBase implements UnmodifiableByDefaultLiveList
+{
+ private final ObservableList extends E> source;
+ private final BiFunction mapper;
+
+ public IndexedMappedList(ObservableList extends E> source,
+ BiFunction mapper) {
+ this.source = source;
+ this.mapper = mapper;
+ }
+
+ @Override
+ public F get(int index) {
+ return mapper.apply(index, source.get(index));
+ }
+
+ @Override
+ public int size() {
+ return source.size();
+ }
+
+ @Override
+ protected Subscription observeInputs() {
+ return LiveList.observeQuasiChanges(source, this::sourceChanged);
+ }
+
+ protected void sourceChanged(QuasiListChange extends E> change) {
+ notifyObservers(mappedChangeView(change));
+ }
+
+ private QuasiListChange mappedChangeView(QuasiListChange extends E> change) {
+ return () -> {
+ List extends QuasiListModification extends E>> mods = change.getModifications();
+ return Lists., QuasiListModification>mappedView(mods, mod -> new QuasiListModification<>() {
+
+ @Override
+ public int getFrom() {
+ return mod.getFrom();
+ }
+
+ @Override
+ public int getAddedSize() {
+ return mod.getAddedSize();
+ }
+
+ @Override
+ public List extends F> getRemoved() {
+ return Lists.mappedView(mod.getRemoved(), elem -> mapper.apply(mod.getFrom(), elem));
+ }
+ });
+ };
+ }
+}
diff --git a/src/main/java/org/fxmisc/flowless/VirtualFlow.java b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
index 3de50c9..8f6e3dc 100644
--- a/src/main/java/org/fxmisc/flowless/VirtualFlow.java
+++ b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
@@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javafx.beans.property.ObjectProperty;
@@ -94,7 +95,17 @@ public static enum Gravity {
ObservableList items,
Function super T, ? extends C> cellFactory,
Gravity gravity) {
- return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), gravity);
+ return new VirtualFlow<>(items, (ndx,item) -> cellFactory.apply(item), new HorizontalHelper(), gravity);
+ }
+
+ /**
+ * Creates a viewport that lays out content horizontally from left to right
+ * but with a cellFactory that also receives the item index.
+ */
+ public static > VirtualFlow createHorizontal(
+ ObservableList items,
+ BiFunction cellFactory) {
+ return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), Gravity.FRONT);
}
/**
@@ -113,7 +124,17 @@ public static enum Gravity {
ObservableList items,
Function super T, ? extends C> cellFactory,
Gravity gravity) {
- return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), gravity);
+ return new VirtualFlow<>(items, (ndx,item) -> cellFactory.apply(item), new VerticalHelper(), gravity);
+ }
+
+ /**
+ * Creates a viewport that lays out content vertically from top to bottom
+ * but with a cellFactory that also receives the item index.
+ */
+ public static > VirtualFlow createVertical(
+ ObservableList items,
+ BiFunction cellFactory) {
+ return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), Gravity.FRONT);
}
private final ObservableList items;
@@ -162,13 +183,13 @@ public Var lengthOffsetEstimateProperty() {
private VirtualFlow(
ObservableList items,
- Function super T, ? extends C> cellFactory,
+ BiFunction cellFactory,
OrientationHelper orientation,
Gravity gravity) {
this.getStyleClass().add("virtual-flow");
this.items = items;
this.orientation = orientation;
- this.cellListManager = new CellListManager<>(this, items, cellFactory);
+ this.cellListManager = new CellListManager(this, items, cellFactory);
this.gravity.set(gravity);
MemoizationList cells = cellListManager.getLazyCellList();
this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells);
diff --git a/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java b/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java
new file mode 100644
index 0000000..157e098
--- /dev/null
+++ b/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java
@@ -0,0 +1,86 @@
+package org.fxmisc.flowless;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+public class CellCreationWithIndexTest extends FlowlessTestBase {
+
+ private ObservableList items;
+ private Counter cellCreations = new Counter();
+ private VirtualFlow flow;
+
+ @Override
+ public void start(Stage stage) {
+ // set up items
+ items = FXCollections.observableArrayList();
+ for(int i = 0; i < 20; ++i) {
+ items.addAll("red", "green", "blue", "purple");
+ }
+
+ // set up virtual flow
+ flow = VirtualFlow.createVertical(
+ items,
+ (index, color) -> {
+ cellCreations.inc();
+ Region reg = new Label( " "+ index +"\t"+ color );
+ reg.setPrefHeight(16.0);
+ reg.setStyle("-fx-background-color: " + color);
+ return Cell.wrapNode(reg);
+ });
+
+ StackPane stackPane = new StackPane();
+ // 25 cells (each 16px high) fit into the viewport
+ stackPane.getChildren().add(flow);
+ stage.setScene(new Scene(stackPane, 200, 400));
+ stage.show();
+ }
+
+ @Before
+ public void setup() {
+ cellCreations.reset();
+ }
+
+ @Test
+ public void updating_an_item_in_viewport_only_creates_cell_once() {
+ // update an item in the viewport
+ interact(() -> items.set(10, "yellow"));
+ assertEquals(1, cellCreations.getAndReset());
+ assertEquals("10", getCellText(10));
+ }
+
+ private String getCellText(int index) {
+ return ((Label) flow.getCell(index).getNode()).getText().substring(2,4);
+ }
+
+ @Test
+ public void updating_an_item_outside_viewport_does_not_create_cell() {
+ // update an item outside the viewport
+ interact(() -> items.set(30, "yellow"));
+ assertEquals(0, cellCreations.getAndReset());
+ }
+
+ @Test
+ public void deleting_an_item_in_viewport_only_creates_cell_once() {
+ // delete an item in the middle of the viewport
+ interact(() -> items.remove(12));
+ assertEquals(1, cellCreations.getAndReset());
+ }
+
+ @Test
+ public void adding_an_item_in_viewport_only_creates_cell_once() {
+ // add an item in the middle of the viewport
+ interact(() -> items.add(12, "yellow"));
+ assertEquals(1, cellCreations.getAndReset());
+ assertEquals("12", getCellText(12));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java b/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java
new file mode 100644
index 0000000..2f58319
--- /dev/null
+++ b/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java
@@ -0,0 +1,83 @@
+package org.fxmisc.flowless;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.reactfx.collection.ListModification;
+import org.reactfx.collection.LiveList;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class IndexedMappedListTest
+{
+ @Test
+ public void testIndexedList() {
+ ObservableList strings = FXCollections.observableArrayList("1", "22", "333");
+ // Live map receives index,item and returns %d-%d index item
+ LiveList lengths = new IndexedMappedList<>(strings, (index, item) -> String.format("%d-%d", index, item.length()));
+ assertArrayEquals(new String[] {"0-1", "1-2", "2-3"}, lengths.stream().toArray());
+
+ List removed = new ArrayList<>();
+ List added = new ArrayList<>();
+ lengths.observeChanges(ch -> {
+ for(ListModification extends String> mod: ch.getModifications()) {
+ removed.addAll(mod.getRemoved());
+ added.addAll(mod.getAddedSubList());
+ }
+ });
+
+ // Set a value in the list and check changes
+ strings.set(1, "4444");
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-3"}, lengths.stream().toArray());
+ assertEquals(Collections.singletonList("1-4"), added);
+ assertEquals(Collections.singletonList("1-2"), removed);
+
+ // Add an entry to the list and check changes
+ strings.add("7777777");
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-3", "3-7"}, lengths.stream().toArray());
+ assertEquals(Arrays.asList("1-4", "3-7"), added);
+ assertEquals(Collections.singletonList("1-2"), removed);
+
+ // Remove an entry to the list and check changes (note that 3-7 becomes 2-7)
+ strings.remove(2);
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-7"}, lengths.stream().toArray());
+ assertEquals(Arrays.asList("1-4", "3-7"), added);
+ assertEquals(Arrays.asList("1-2", "2-3"), removed);
+ }
+
+ @Test
+ public void testLazyIndexedList() {
+ ObservableList strings = FXCollections.observableArrayList("1", "22", "333");
+ IntegerProperty evaluationsCounter = new SimpleIntegerProperty(0);
+ LiveList lengths = new IndexedMappedList<>(strings, (index, elem) -> {
+ evaluationsCounter.set(evaluationsCounter.get() + 1);
+ return String.format("%d-%d", index, elem.length());
+ });
+
+ lengths.observeChanges(ch -> {});
+ strings.remove(1);
+
+ assertEquals(0, evaluationsCounter.get());
+
+ // Get the first element and the counter has increased
+ assertEquals("0-1", lengths.get(0));
+ assertEquals(1, evaluationsCounter.get());
+
+ // Get the second element, it will evaluate one item
+ assertEquals("1-3", lengths.get(1));
+ assertEquals(2, evaluationsCounter.get());
+
+ // Get again the first, it will reevaluate it
+ assertEquals("0-1", lengths.get(0));
+ assertEquals(3, evaluationsCounter.get());
+ }
+}