我终于找到了一个既快速又高效的解决方案。
不是每次视图滚动并更新超级昂贵的子列表时都构建新单元格,而是构建所需数量的单元格来填充视口。在滚动时,单元格被布置和更新。更新单元格而不是创建和更改子列表要快得多。
因此,新的实现与 Android 的 RecyclerView 非常相似,也可能与 JavaFX 的 VirtualFlow 非常相似,唯一的区别是 Cell 是愚蠢的,引用 Flowless:
这是最重要的区别。对于 Flowless,cell 只是 Node,不封装任何关于虚拟流的逻辑。一个单元格甚至不一定存储它正在显示的项目的索引。这允许 VirtualFlow 完全控制何时创建和/或更新单元。
每个人都可以扩展接口并定义自己的逻辑。
显示一点代码:
细胞界面:
public interface Cell<T> {
/**
* Returns the cell's node.
* The ideal way to implement a cell would be to extend a JavaFX's pane/region
* and override this method to return "this".
*/
Node getNode();
/**
* Automatically called by the VirtualFlow
* <p>
* This method must be implemented to correctly
* update the Cell's content on scroll.
* <p>
* <b>Note:</b> if the Cell's content is a Node this method should
* also re-set the Cell's children because (quoting from JavaFX doc)
* `A node may occur at most once anywhere in the scene graph` and it's
* possible that a Node may be removed from a Cell to be the content
* of another Cell.
*/
void updateItem(T item);
/**
* Automatically called by the VirtualFlow.
* <p>
* Cells are dumb, they have no logic, no state.
* This method allow cells implementations to keep track of a cell's index.
* <p>
* Default implementation is empty.
*/
default void updateIndex(int index) {}
/**
* Automatically called after the cell has been laid out.
* <p>
* Default implementation is empty.
*/
default void afterLayout() {}
/**
* Automatically called before the cell is laid out.
* <p>
* Default implementation is empty.
*/
default void beforeLayout() {}
}
CellsManager 类,负责显示和更新单元格:
/*
* Initialization, creates num cells
*/
protected void initCells(int num) {
int diff = num - cellsPool.size();
for (int i = 0; i <= diff; i++) {
cellsPool.add(cellForIndex(i));
}
// Add the cells to the container
container.getChildren().setAll(cellsPool.stream().map(C::getNode).collect(Collectors.toList()));
// Ensure that cells are properly updated
updateCells(0, num);
}
/*
* Update the cells in the given range.
* CellUpdate is a simple bean that contains the cell, the item and the item's index.
* The update() method calls updateIndex() and updateItem() of the Cell's interface
*/
protected void updateCells(int start, int end) {
// If the items list is empty return immediately
if (virtualFlow.getItems().isEmpty()) return;
// If the list was cleared (so start and end are invalid) cells must be rebuilt
// by calling initCells(numOfCells), then return since that method will re-call this one
// with valid indexes.
if (start == -1 || end == -1) {
int num = container.getLayoutManager().lastVisible();
initCells(num);
return;
}
// If range not changed or update is not triggered by a change in the list, return
NumberRange<Integer> newRange = NumberRange.of(start, end);
if (lastRange.equals(newRange) && !listChanged) return;
// If there are not enough cells build them and add to the container
Set<Integer> itemsIndexes = NumberRange.expandRangeToSet(newRange);
if (itemsIndexes.size() > cellsPool.size()) {
supplyCells(cellsPool.size(), itemsIndexes.size());
} else if (itemsIndexes.size() < cellsPool.size()) {
int overFlow = cellsPool.size() - itemsIndexes.size();
for (int i = 0; i < overFlow; i++) {
cellsPool.remove(0);
container.getChildren().remove(0);
}
}
// Items index can go from 0 to size() of items list,
// cells can go from 0 to size() of the cells pool,
// Use a counter to get the cells and itemIndex to
// get the correct item and index to call
// updateIndex() and updateItem() later
updates.clear();
int poolIndex = 0;
for (Integer itemIndex : itemsIndexes) {
T item = virtualFlow.getItems().get(itemIndex);
CellUpdate update = new CellUpdate(item, cellsPool.get(poolIndex), itemIndex);
updates.add(update);
poolIndex++;
}
// Finally, update the cells, the layout and the range of items processed
updates.forEach(CellUpdate::update);
processLayout(updates);
lastRange = newRange;
}
也许它仍然不完美,但它有效,我仍然需要对它进行适当的测试,以应对单元格过多、单元格不足、容器大小发生变化等情况,因此必须重新计算单元格的数量......
现在的表现: