-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathCellListManager.java
More file actions
176 lines (150 loc) · 6.01 KB
/
CellListManager.java
File metadata and controls
176 lines (150 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package org.fxmisc.flowless;
import java.util.Optional;
import java.util.function.Function;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.IndexRange;
import javafx.scene.input.ScrollEvent;
import org.reactfx.EventStreams;
import org.reactfx.Subscription;
import org.reactfx.collection.LiveList;
import org.reactfx.collection.MemoizationList;
import org.reactfx.collection.QuasiListModification;
/**
* Tracks all of the cells that the viewport can display ({@link #cells}) and which cells the viewport is currently
* displaying ({@link #presentCells}).
*/
final class CellListManager<T, C extends Cell<T, ? extends Node>> {
private final Node owner;
private final CellPool<T, C> cellPool;
private final MemoizationList<C> cells;
private final LiveList<C> presentCells;
private final LiveList<Node> cellNodes;
private final Subscription presentCellsSubscription;
public CellListManager(
Node owner,
ObservableList<T> items,
Function<? super T, ? extends C> cellFactory) {
this.owner = owner;
this.cellPool = new CellPool<>(cellFactory);
this.cells = LiveList.map(items, this::cellForItem).memoize();
this.presentCells = cells.memoizedItems();
this.cellNodes = presentCells.map(Cell::getNode);
this.presentCellsSubscription = presentCells.observeQuasiModifications(this::presentCellsChanged);
}
public void dispose() {
// return present cells to pool *before* unsubscribing,
// because stopping to observe memoized items may clear memoized items
presentCells.forEach(cellPool::acceptCell);
presentCellsSubscription.unsubscribe();
cellPool.dispose();
}
/** Gets the list of nodes that the viewport is displaying */
public ObservableList<Node> getNodes() {
return cellNodes;
}
public MemoizationList<C> getLazyCellList() {
return cells;
}
public boolean isCellPresent(int itemIndex) {
return cells.isMemoized(itemIndex);
}
public C getPresentCell(int itemIndex) {
// both getIfMemoized() and get() may throw
return cells.getIfMemoized(itemIndex).get();
}
public Optional<C> getCellIfPresent(int itemIndex) {
if (itemIndex>=cells.size()||itemIndex<0) {
return Optional.empty();
}
return cells.getIfMemoized(itemIndex); // getIfMemoized() may throw
}
public C getCell(int itemIndex) {
return cells.get(itemIndex);
}
/**
* This is a noop on non visible items. For reusable Cells this will cause
* updateItem to be invoked on the next available pooled Cell. If a Cell is
* not available or reusable, a new Cell is created via the cell factory.
* @param fromItem - the start index, inclusive.
* @param toItem - the end index, exclusive.
*/
public void refreshCells(int fromItem, int toItem) {
if (fromItem >= toItem) throw new IllegalArgumentException(
String.format("To must be greater than from. from=%s to=%s", fromItem, toItem)
);
IndexRange memorizedRange = cells.getMemoizedItemsRange();
fromItem = Math.max( fromItem, memorizedRange.getStart() );
toItem = Math.min( toItem, memorizedRange.getEnd() );
if (fromItem < toItem) {
cells.forget(fromItem, toItem);
}
}
/**
* Updates the list of cells to display
*
* @param fromItem the index of the first item to display
* @param toItem the index of the last item to display
*/
public void cropTo(int fromItem, int toItem) {
fromItem = Math.max(fromItem, 0);
toItem = Math.max(Math.min(toItem, cells.size()), 0);
IndexRange memorizedRange = cells.getMemoizedItemsRange();
if (memorizedRange.getStart() < fromItem) {
cells.forget(0, fromItem);
}
if (memorizedRange.getEnd() > toItem) {
cells.forget(toItem, cells.size());
}
}
private C cellForItem(T item) {
C cell = cellPool.getCell(item);
// apply CSS when the cell is first added to the scene
Node node = cell.getNode();
EventStreams.nonNullValuesOf(node.sceneProperty())
.subscribeForOne(scene -> {
node.applyCss();
});
// Make cell initially invisible.
// It will be made visible when it is positioned.
node.setVisible(false);
if (cell.isReusable()) {
// if cell is reused i think adding event handler
// would cause resource leakage.
node.setOnScroll(this::pushScrollEvent);
node.setOnScrollStarted(this::pushScrollEvent);
node.setOnScrollFinished(this::pushScrollEvent);
} else {
node.addEventHandler(ScrollEvent.ANY, this::pushScrollEvent);
}
return cell;
}
/**
* Push scroll events received by cell nodes directly to
* the 'owner' Node. (Generally likely to be a VirtualFlow
* but not required.)
*
* Normal bubbling of scroll events gets interrupted during
* a scroll gesture when the Cell's Node receiving the event
* 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.
*/
private void pushScrollEvent(ScrollEvent se) {
owner.fireEvent(se);
se.consume();
}
private void presentCellsChanged(QuasiListModification<? extends C> mod) {
// add removed cells back to the pool
for(C cell: mod.getRemoved()) {
cellPool.acceptCell(cell);
}
// update indices of added cells and cells after the added cells
for(int i = mod.getFrom(); i < presentCells.size(); ++i) {
presentCells.get(i).updateIndex(cells.indexOfMemoizedItem(i));
}
}
}