Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX: Binding Timeline's duration to a property

Tags:

java

javafx-2

I have an application that has a combobox with some double values. The user can select any of the values. The application has a "TimeLine" attached to it that will print a statement on the console. The sscce is below. What should happen is that is should print the text of the option selected from the combobox. Pls advise.

package just.to.test;

import javafx.animation.KeyFrame;
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.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class TimerSample extends Application  {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Text Fonts");

        Group g = new Group();
        Scene scene = new Scene(g, 150, 100);

        ObservableList<Double> data = FXCollections.observableArrayList();

        data.add(5.0);
        data.add(10.0);
        data.add(15.0);
        data.add(20.0);

        ComboBox<Double> timeOptions = new ComboBox<Double>(data);
        timeOptions.getSelectionModel().selectFirst();

        g.getChildren().addAll(timeOptions);

        primaryStage.setScene(scene);
        primaryStage.show();

        final double timerInterval = timeOptions.getSelectionModel().getSelectedItem();

        KeyFrame keyFrame = new KeyFrame(Duration.seconds(timerInterval),
            new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event) {
                    System.out.println("This is called every "
                        + timerInterval + " seconds");
                }
            });

        Timeline timerThread = new Timeline(keyFrame);
        timerThread.setCycleCount(Timeline.INDEFINITE);
        timerThread.play();
    }
}
like image 982
Aspirant Avatar asked Dec 16 '22 05:12

Aspirant


2 Answers

You can't bind a Timeline's duration to a property because the duration of a Keyframe in a Timeline is not a property and you can only bind properties to properties.

What you need to do instead is to listen for changes in the value of the combo-box and trigger create new key frames with the new durations when the user selects a new duration. You also cannot modify the key frames of a running timeline, so you have to stop the timeline before you set the new keyframes for the timeline, then start the timeline after the new keyframes have been set.

Sample Code

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class TimerSample extends Application  {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        Group g = new Group();
        Scene scene = new Scene(g, 150, 100);

        ComboBox<Double> timerOptions = createTimerOptions(
                0.5, 1.0, 1.5, 2.0
        );
        g.getChildren().addAll(timerOptions);

        createTimer(timerOptions);

        stage.setScene(scene);
        stage.show();
    }

    private ComboBox<Double> createTimerOptions(double... options) {
        ObservableList<Double> data = FXCollections.observableArrayList();

        for (Double option: options) {
            data.add(option);
        }

        return new ComboBox<Double>(data);
    }

    private void createTimer(ComboBox<Double> timeOptions) {
        final Timeline timer = new Timeline();
        timer.setCycleCount(Timeline.INDEFINITE);

        timeOptions.valueProperty().addListener(new ChangeListener<Double>() {
            @Override
            public void changed(ObservableValue<? extends Double> observable, Double oldValue, Double newValue) {
                resetTimer(timer, newValue);
            }
        });

        timeOptions.getSelectionModel().selectFirst();
    }

    private void resetTimer(Timeline timer, final double timerInterval) {
        KeyFrame keyFrame = new KeyFrame(
            Duration.seconds(timerInterval),
            new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event) {
                    System.out.println(
                            "This is called every "
                                    + timerInterval
                                    + " seconds"
                    );
                }
            }
        );

        timer.stop();
        timer.getKeyFrames().setAll(
                keyFrame
        );
        timer.play();
    }
}

just wondering if performance wise it is much heavier due to adding a new keyframe with every change in the combobox value.

Don't worry too much about performance in this case - this is a highly efficient operation.

There is no other solution than creating new key frames because key frames are immutable objects.

You could construct key frames up front or place them in something like a LRU cache as you lazily construct them, but in general the additional complexity involved will almost certainly not be worth it.

like image 186
jewelsea Avatar answered Dec 23 '22 17:12

jewelsea


All animations have a rateProperty() which can be changed on a running animation!

This seems like a much cleaner solution:

private void createTimer(ComboBox<Double> timeOptions) {
    Timeline timer = new Timeline(
            new KeyFrame(Duration.seconds(1),
            evt-> System.out.println(
                        "This is called every "
                                + timeOptions.getValue()
                                + " seconds"
                ));

    timer.setCycleCount(Timeline.INDEFINITE);
    timer.rateProperty()
         .bind(new SimpleDoubleProperty(1.0)
         .divide(timeOptions.valueProperty()));

    timeOptions.getSelectionModel().selectFirst();
}
like image 42
Mordechai Avatar answered Dec 23 '22 16:12

Mordechai