Search code examples
listviewanimationjavafxscrolltimeline

JavaFX ListView Timeline Scroll Animation is Jumpy (choppy)


I'm using a JavaFX ListView and Timeline to animate scrolling of a list. When the scroll animation is slow the text is jumpy (choppy). I have tried using an AnimationTimer to scroll the text. The text was also jumpy (choppy) during slow scrolling. The ListView control is necessary for the virtual viewport characteristics. Following is an example that recreates the problem on my Mac using Java Version 1.8.

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.List;

public class Jumpy extends Application {

    ListView listView;
    Timeline timeline = new Timeline();
    double speed = 0.0000005;

    @Override
    public void start(Stage stage) throws Exception {
        List list = new ArrayList();
        for (int i = 0; i < 2000; i++) {
            Text text = new Text("Random line of text to show how it is choppy during scroll animation");
            text.setStyle("-fx-font-size: " + 4 + "em");
            list.add(text);
        }
        ObservableList observableList = FXCollections.observableList(list);
        listView = new ListView((observableList));
        listView.setPrefWidth(600);

        AnchorPane root = new AnchorPane();
        root.getChildren().addAll(listView, buttons());

        stage.setScene(new Scene(root));
        stage.show();
    }

    public void scroll() {
        ScrollBar scrollBar = getVerticalScrollBar();
        EventHandler scroll = new EventHandler<ActionEvent>() {
            public void handle(ActionEvent t) {
                scrollBar.setValue(scrollBar.getValue() + speed);
            }
        };

        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.setAutoReverse(false);
        KeyValue kv = new KeyValue(scrollBar.valueProperty(), 1);
        KeyFrame kf = new KeyFrame(Duration.seconds(0.017), scroll);
        timeline.getKeyFrames().add(kf);
        timeline.play();
    }

    public ScrollBar getVerticalScrollBar() {
        ScrollBar scrollBar = null;
        for (Node node : listView.lookupAll(".scroll-bar")) {
            if (node instanceof ScrollBar) {
                scrollBar = (ScrollBar) node;
                if ((scrollBar.getOrientation().compareTo(Orientation.VERTICAL)) == 0) {
                    break;
                }
            }
        }
        return scrollBar;
    }

    public HBox buttons() {
        HBox hBox = new HBox();
        Button start = new Button("start");
        start.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                scroll();
            }
        });
        Button slower = new Button("slower");
        slower.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                ScrollBar scrollBar = getVerticalScrollBar();
                EventHandler scroll = new EventHandler<ActionEvent>() {
                    public void handle(ActionEvent t) {
                        scrollBar.setValue(scrollBar.getValue() - 0.000001);
                    }
                };
                KeyFrame kf = new KeyFrame(Duration.millis(10.0D), scroll);
                timeline.getKeyFrames().add(kf);
            }

        });
        Button faster = new Button("faster");
        faster.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                ScrollBar scrollBar = getVerticalScrollBar();
                EventHandler scroll = new EventHandler<ActionEvent>() {
                    public void handle(ActionEvent t) {
                        scrollBar.setValue(scrollBar.getValue() + 0.000001);
                    }
                };
                KeyFrame kf = new KeyFrame(Duration.millis(10.0D), scroll);
                timeline.getKeyFrames().add(kf);

            }
        });
        hBox.getChildren().addAll(start, slower, faster);
        return hBox;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution

  • There's no point in using a virtualized control if you're just going to populate it with 2000 Node instances: you completely destroy almost all the benefits of using the virtualization in the first place.

    Populate the control with data (e.g., in this case, Strings) and either style the ListView or use a cell factory to control how the values are displayed.

    The following performs much better for me:

    ListView<String> listView;
    Timeline timeline = new Timeline();
    double speed = 0.0000005;
    
    @Override
    public void start(Stage stage) throws Exception {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 2000; i++) {
            String text = "Random line of text to show how it is choppy during scroll animation";
            //  text.setStyle("-fx-font-size: " + 4 + "em");
            list.add(text);
        }
        ObservableList<String> observableList = FXCollections.observableList(list);
        listView = new ListView<String>((observableList));
        listView.setPrefWidth(600);
    
        listView.setStyle("-fx-font-size: 4em; ");
    
        AnchorPane root = new AnchorPane();
        root.getChildren().addAll(listView, buttons());
    
        stage.setScene(new Scene(root));
        stage.show();
    }
    

    After this change, using an AnimationTimer seems slightly smoother still. Here's an example using this approach (and with all the redundant code removed):

    import java.util.ArrayList;
    import java.util.List;
    
    import javafx.animation.AnimationTimer;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Orientation;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.ListView;
    import javafx.scene.control.ScrollBar;
    import javafx.scene.layout.AnchorPane;
    import javafx.scene.layout.HBox;
    import javafx.stage.Stage;
    
    public class Jumpy extends Application {
    
        ListView<String> listView;
        Timeline timeline = new Timeline();
        double increment = 2e-5 ;
        double speed = 5*increment ;
    
        AnimationTimer timer = new AnimationTimer() {
    
            private long lastUpdate = -1 ;
            private ScrollBar scrollbar ;
    
            @Override
            public void start() {
                scrollbar = getVerticalScrollBar();
                super.start();
            }
    
            @Override
            public void handle(long now) {
                if (lastUpdate < 0) {
                    lastUpdate = now ;
                    return ;
                }
    
                long elapsedNanos = now - lastUpdate ;
                double delta = speed * elapsedNanos / 1_000_000_000 ;
                scrollbar.setValue(scrollbar.getValue() + delta);
    
                lastUpdate = now ;
            }
        };
    
        @Override
        public void start(Stage stage) throws Exception {
            List<String> list = new ArrayList<>();
            for (int i = 0; i < 2000; i++) {
                String text = "Random line of text to show how it is choppy during scroll animation";
                list.add(text);
            }
            ObservableList<String> observableList = FXCollections.observableList(list);
            listView = new ListView<String>((observableList));
            listView.setPrefWidth(600);
    
            listView.setStyle("-fx-font-size: 4em; ");
    
            AnchorPane root = new AnchorPane();
            root.getChildren().addAll(listView, buttons());
    
            stage.setScene(new Scene(root));
            stage.show();
        }
    
        private ScrollBar getVerticalScrollBar() {
            ScrollBar scrollBar = null;
            for (Node node : listView.lookupAll(".scroll-bar")) {
                if (node instanceof ScrollBar) {
                    scrollBar = (ScrollBar) node;
                    if (scrollBar.getOrientation() == Orientation.VERTICAL) {
                        break;
                    }
                }
            }
            return scrollBar;
        }
    
        private HBox buttons() {
            HBox hBox = new HBox();
            Button start = new Button("start");
            start.setOnAction(event -> timer.start());
            Button slower = new Button("slower");
            slower.setOnAction(event -> speed -= increment);
            Button faster = new Button("faster");
            faster.setOnAction(event -> speed += increment);
            hBox.getChildren().addAll(start, slower, faster);
            return hBox;
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }