Ale*_*isi 1 javafx ui-virtualization
我正在尝试实现自己的 VirtualFlow,但遇到了一些性能问题,并且我不明白瓶颈可能在哪里以及我可以采取哪些措施来改进它。
这是一个包含两个列表的展示,一个包含简单单元格,另一个也包含复选框,两者都有 100_000 个项目(抱歉 gif 质量):
正如您所看到的,我在滚动浏览仅带有标签的简单单元格时没有任何问题,但带有复选框的列表在开始时可能会相当滞后。
为了达到这样的性能(否则会更糟),我记住了我的细胞工厂功能,以便在需要时所有单元都在内存中准备就绪,我想知道是否还有其他方法可以使其更快/更平滑。
我用来显示单元格的代码是:
long start = System.currentTimeMillis();
builtNodes.clear();
for (int i = from; i <= to; i++) {
C cell = cellForIndex.apply(i);
Node node = cell.getNode();
builtNodes.add(node);
}
manager.virtualFlow.container.getChildren().setAll(builtNodes);
long elapsed = System.currentTimeMillis() - start;
System.out.println("Show elapsed: " + elapsed);
Run Code Online (Sandbox Code Playgroud)
我什至添加了这个小日志,最重要的部分似乎是当我打电话时getChildren().setAll(...),单元建筑和布局几乎是立即的
哦,关于from和to参数的另一个注释。当我滚动时,我获取滚动条的值并计算第一个和最后一个可见索引,如下所示:
public int firstVisible() {
return (int) Math.floor(scrolled / cellHeight);
}
public int lastVisible() {
return (int) Math.ceil((scrolled + virtualFlow.getHeight()) / cellHeight - 1);
}
Run Code Online (Sandbox Code Playgroud)
编辑回答一些问题:
为什么要实施我自己的 VirtualFlow?
最终目标是制作我自己的 ListView 实现,为了做到这一点,我还需要一个 VirtualFlow,我也知道这是一个相当低水平且艰巨的任务,但我认为这将是了解更多编程知识的好方法概述以及关于 JavaFX 的信息
为什么要记忆?
通过将单元工厂结果缓存在内存中,它使后续滚动速度更快,因为节点已经构建完毕,它们只需要进行布局,这相当容易。问题是在一开始,因为由于某种原因,VirtualFlow 对于更复杂的单元来说会滞后,我不明白为什么,因为流行的库 Flowless 也使用记忆并将节点缓存在内存中,但它从一开始就非常流畅。JavaFX 的 VirtualFlow(从 JFX16 修订而来)在无需记忆的情况下也非常高效,但理解起来要复杂得多
我终于找到了一个快速且内存高效的解决方案。
我不是每次视图滚动时都构建新单元格并更新子列表,这非常昂贵,而是只构建填充视口所需数量的单元格。滚动时,单元格会进行布局和更新。更新单元格而不是创建和更改子列表要快得多。
因此,新的实现与 Android 的 RecyclerView 非常相似,也许也与 JavaFX 的 VirtualFlow 非常相似,唯一的区别是 Cells 是愚蠢的,引用 Flowless 的话:
这是最重要的区别。对于 Flowless,单元只是节点,不封装任何有关虚拟流的逻辑。单元格甚至不一定存储它正在显示的项目的索引。这使得 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() {}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
也许它仍然不完美,但它有效,我仍然需要针对单元格太多、单元格不足、容器大小发生变化等情况进行适当的测试,因此必须重新计算单元格数量......
现在表现:
| 归档时间: |
|
| 查看次数: |
589 次 |
| 最近记录: |