You may be able to achieve behavior close to what you want by using JavaFX 17.0.1 (that exact version) and explicitly calling scrollTo
to scroll to the end of the list after you append to it, and yes I know that is not exactly what you are asking.
IMO this isn't really a question suited to StackOverflow. To follow up on it, I suggest, posting to the openjfx-dev mailing list or filing a bug or feature request with the JavaFX project.
Reproducible example with runLater
A minimal reproducible example for this issue, or at least functionality related to it as I understand it.
It creates a ListView and in a separate thread makes a call to add an item to the ListView every 10 milliseconds, scrolling to the end of the ListView after each item is added (performing the addition and scrolling on the JavaFX application thread using Platform.runLater
).
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
public class ListScrollApp extends Application {
private final int MAX_LINES_TO_ADD = 1_000;
private final int INIT_LINES = 100_000;
@Override
public void start(Stage stage) {
ListView<String> listView = new ListView<>();
for (int i = 0; i < INIT_LINES; i++) {
listView.getItems().add(
"Item " + (i+1)
);
}
stage.setScene(new Scene(listView));
stage.show();
Thread populatorThread = new Thread(
() -> {
for (int i = INIT_LINES; i < INIT_LINES + MAX_LINES_TO_ADD; i++) {
final int curLineNum = i+1;
Platform.runLater(() -> {
listView.getItems().add(
"Item " + curLineNum
);
listView.scrollTo(
listView.getItems().size() - 1
);
});
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// no action required.
}
}
},
"populator"
);
populatorThread.setDaemon(true);
populatorThread.start();
}
public static void main(String[] args) {
launch(args);
}
}
Behavior with JavaFX 8
Performance
If run in Java 8 it will run well (no performance issue).
Scroll Behavior
If you comment out the scrollTo
call it will only exhibit the "autoscrolling" behavior referred to in the question if you drag the thumb to the end of the list immediately after starting the app.
From what I can see, with 8 it will equivalently tail the list as items are added (autoscroll) only if the scroll bar was shown (e.g. there were enough items in the list for there to be a scroll bar) and the scroll bar is positioned on the last item.
If the scroll bar is positioned at the front of the list, the list doesn't scroll.
Scrolling Bug
If the scroll bar is positioned in the middle of the list, items slowly move by about a pixel each time a new item is added (a bug that was fixed in later versions).
Behavior with JavaFX 17
Performance
If run in JavaFX 17.0.1 it will run well (no performance issue).
If you run in JavaFX 17.0.8 it will perform worse (see benchmarks below).
Scrolling behavior
If you comment out the scrollTo
, the visible item won't change as items are added to the list, the view will not autoscroll to the last item in the list.
IMO, this is correct behavior, though this is just an opinion and the desired or required behavior could be debated.
Behavior with JavaFX 18
Bug
If you run the example app in JavaFX 18 it is completely broken, doesn't render right, and logs to the console:
Jul 21, 2023 5:35:41 PM javafx.scene.control.skin.VirtualFlow addTrailingCells
INFO: index exceeds maxCellCount. Check size calculations for class javafx.scene.control.skin.ListViewSkin$2
Behavior with JavaFX 20
Performance
If you run in Java 20 (or 21-ea+17), it will perform quite badly. JavaFX takes longer to render the updated ListView. Eventually, all the runLater calls in the example flood the JavaFX event queue and the app becomes quite unresponsive. For a smaller number of INIT_LINES
, e.g. 1_000
, it will perform fine.
Scrolling behavior
Same as Java 17.
Additional thoughts
Most of these are just my opinions, it is OK if you don't agree with them.
Unless you plan to file a feature request or regression bug, it doesn’t matter how it behaved in JavaFX 8 or how and why it behaved that way it did, if you are now targeting a different version. Only achieving your goal in the desired version matters.
Calling runLater and scrollTo for every line is suboptimal (regardless of whether it worked for your use-case in the past).
You run the risk of overloading the JavaFX system and flooding the run queue (as you do when you try to port your code to the JavaFX 20 when the list view performs less well with large item numbers currently).
I advise using an alternate approach which does risk flooding the run queue, even if your previous approach worked OK for you in an earlier version and you don't want to change it.
One such approach is demonstrated in this list view based log viewer.
The behavior of the ListView, when items are added, was never documented, so it is an implementation detail and not a regression IMO.
The recent JavaFX ListView implementations perform worse but have fewer bugs, I think.
If you really want to be sure which item is shown, then use the documented API (scrollTo) to do that, rather than relying on undocumented behavior which can be subject to change without notice.
For performance in this situation with Java 8, there was a recent related fix:
I tried the JavaFX 21-ea+17 version to see if it addressed the performance issue I observed with the sample app I have provided here, and it did not (at least not adequately IMO).
FAQ
Eventually, all the runLater calls in the example flood the JavaFX event queue and the app becomes quite unresponsive"
In this example, and in the OP's code, that may be the cause of the unresponsiveness.
Yes, unrestrained flooding of the runLater queue is never a good idea.
But I don't think that's the root of the problem, which I believe is actually in the skin (unless the skin/flow calls runLater a lot?).
Yes, the poor ListView performance is independent of runLater, but is dependent on the number of items in the list backing the ListView.
I think that there were bugs in the rendering of list view cells in earlier JavaFX versions. The modifications to fix those introduced a performance regression sometime after JavaFX 17.0.1. There may have been some change that made the ListView performance depend in some way on the size of the backing list.
For later versions such as 20, the ListView renders OK and doesn't log to the console, but performance is worse.
Even without runLater, in recent JavaFX versions, there are issues rendering lists and tables which have large backing lists (e.g. 100,000 or more items) - which really should not be the case given the whole "Virtual Flow" concept.
I recall another recent question on performance with large backing lists which kleopatra also analyzed and confirmed. There has been some development work and bug fixes around performance of the virtual flows in recent releases, but performance still does not match JavaFX 8 performance. Indeed for the sample app, the performance of JavaFX 17.0.8 is worse than JavaFX 17.0.1 and JavaFX 20 performs worse than JavaFX 17 (benchmarks below).
AnimationTimer based example
This runs everything on the JavaFX application thread using an AnimationTimer rather than a multi-threaded solution that relies on Platform.runLater calls.
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
public class ListAnimatedScrollApp extends Application {
private final int MAX_LINES_TO_ADD = 1_000;
private final int INIT_LINES = 1_000;
@Override
public void start(Stage stage) {
ListView<String> listView = new ListView<>();
for (int i = 0; i < INIT_LINES; i++) {
listView.getItems().add(
"Item " + (i+1)
);
}
stage.setScene(new Scene(listView));
stage.show();
AnimationTimer populationTimer = new AnimationTimer() {
private int curLineNum = INIT_LINES;
private long last = 0;
@Override
public void handle(long now) {
if (curLineNum >= INIT_LINES + MAX_LINES_TO_ADD) {
stop();
}
if (last != 0) {
System.out.println((now - last) / 1_000_000.0);
}
last = now;
listView.getItems().add(
"Item " + curLineNum
);
listView.scrollTo(listView.getItems().size() - 1);
curLineNum++;
}
};
populationTimer.start();
}
public static void main(String[] args) {
launch(args);
}
}
Benchmark results for the AnimationTimer example
On my system:
- With JavaFX 20.0.1 or 21-ea+17, the above app will refresh the view around every 100-120 milliseconds
- With JavaFX 17.0.8 every ~50 milliseconds.
- With JavaFX 17.0.1 every 16-17 milliseconds.
If, instead, the INIT_LINES
value is changed to 1_000
, then the view will refresh every 16-17 milliseconds for all versions (e.g. the standard 60fps frame rate cap for JavaFX apps).