Search code examples
javajavafxjavafx-8

Javafx Timeline stops playing upon passing through multiple listeners


I'm a beginner who recently picked up JavaFX, I've been trying to make a program that executes an animation on a node upon clicking on it. I've tried to see if anyone else has tried something similar but I have not been able to find a similar case to what I've tried to do.

The Timeline animates Panes, which contain a Rectangle and Text, horizontally across the screen, upon finishing the execution the stage will change scenes. When the user clicks a button to go back to the original scene the buttons should perform another animation.

I've verified that it isn't the scene transition that is causing it to stop and I have also verified that the program is indeed executing .play() on the timeline. The timelines are executed within the custom listeners so the listeners are not an issue as far as I know.

Here is the code sample to recreate the issue I have, run the script and just press on. Here you can see that done is being printed to the console which is right underneath Main_Button_Animation.play(); however, nothing is being animated.

import java.util.ArrayList;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class main extends Application{
    Scene scene1;

    @Override
    public void start(Stage primaryStage) {

        primaryStage.setTitle("test");
        Main_Screen MainScreen = new Main_Screen(800, 480);

        scene1 = MainScreen.MainScreen(primaryStage);
        MainScreen.getInitiater().Calibration_Listener(() -> {
            MainScreen.getInitiater().Back_Button_Pressed();
        });

        primaryStage.setScene(scene1);
        primaryStage.show();
    }
    public static void main(String[] args) {
        Application.launch(args);
    }
}
//custom listeners
interface Calibration {
    void menu();
}
interface Back {
    void back();
}
class Initiater {
    private List<Calibration> calilist = new ArrayList<Calibration>();;
    private List<Back> Go_Back = new ArrayList<Back>();

    public void Calibration_Listener(Calibration toAdd) {
        calilist.add(toAdd);
    }
    public void Back_Listener(Back toAdd) {
        Go_Back.add(toAdd);
    }

    public void Calibration_Pressed() {
        System.out.println("Calibration Pressed");
        for (Calibration hl : calilist)
            hl.menu();
    }
    public void Back_Button_Pressed() {
        System.out.println("Back Pressed");
        for (Back hl : Go_Back)
            hl.back();
    }
}

//animations and setup
class Main_Screen {
    Initiater initiater;
    private int X;
    private int Y;

    public Main_Screen(int X, int Y) {
        this.initiater = new Initiater();
        this.X = X;
        this.Y = Y;
    }

    private Pane UI_Button(String name, int[] pos, int[] size, Color color) {
        final Text text = new Text(0, 0, name);
        text.setFont(new Font(20));

        final Rectangle outline = new Rectangle(0, 0, size[0], size[1]);
        outline.setFill(color);

        final StackPane stack = new StackPane();
        stack.getChildren().addAll(outline, text);
        stack.setLayoutX(pos[0]);
        stack.setLayoutY(pos[1]);

        return stack;
    }

    public int getX() {
        return this.X;
    }

    public int getY() {
        return this.Y;
    }

    public Initiater getInitiater() {
        return this.initiater;
    }

    public Scene MainScreen(Stage stage) {
        Scene scene = new Scene(new Group(), getX(), getY());
        scene.setFill(Color.WHITE);

        Pane Start = UI_Button("Start", new int[] { getX() - getX() / 4, 0 }, new int[] { getX() / 4, getY() / 4 }, Color.rgb(255, 0, 0));
        Pane Calibration = UI_Button("Callibration", new int[] { 0, ((getY() + (getY() / 8)) / 4) * 0 }, new int[] { getX() / 2, getY() / 4 }, Color.rgb(60, 208, 230));

        System.out.println(Calibration.boundsInLocalProperty());
        ((Group) scene.getRoot()).getChildren().addAll(Calibration, Start);

        final Timeline Main_Button_Animation = new Timeline();
        final KeyFrame Cali_kf = new KeyFrame(Duration.millis(500), new KeyValue(Calibration.translateXProperty(), getX() / 2));
        final KeyFrame Start_kf = new KeyFrame(Duration.millis(500), new KeyValue(Start.translateXProperty(), -getX() / 4));
        Main_Button_Animation.getKeyFrames().addAll(Cali_kf, Start_kf);
        Main_Button_Animation.setRate(-1);
        Main_Button_Animation.jumpTo(Main_Button_Animation.getTotalDuration());
        Main_Button_Animation.play();

        Calibration.setOnMouseClicked(MouseEvent -> {
            Calibration.setDisable(true);
            Start.setDisable(true);
            System.out.println(Calibration.boundsInLocalProperty());
            Main_Button_Animation.setRate(1);
            Main_Button_Animation.jumpTo(Duration.millis(0));
            Main_Button_Animation.play();
            Main_Button_Animation.setOnFinished(event -> {
                Main_Button_Animation.play();
                System.out.println("done");
            });
        });     
        return scene;
    }

}

Edit1 : For clarity, The example above doesn't use any scene transition. Here is a example with a scene transition.

import java.util.ArrayList;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class main extends Application{
    Scene scene1, scene2;

    @Override
    public void start(Stage primaryStage) {

        primaryStage.setTitle("test");
        Main_Screen MainScreen = new Main_Screen(800, 480);

        scene1 = MainScreen.MainScreen(primaryStage);
        MainScreen.getInitiater().Calibration_Listener(() -> {
            primaryStage.setScene(scene2);
            primaryStage.show();
        });

        Label label2 = new Label("This is the second scene");
        Button button2 = new Button("Go to scene 1");
        button2.setOnAction(e -> {
            MainScreen.getInitiater().Back_Button_Pressed();
            primaryStage.setScene(scene1);
            primaryStage.show();
        });
        VBox layout2 = new VBox(20);
        layout2.getChildren().addAll(label2, button2);
        scene2 = new Scene(layout2, 800, 480);

        primaryStage.setScene(scene1);
        primaryStage.show();
    }
    public static void main(String[] args) {
        Application.launch(args);
    }
}
//custom listeners
interface Calibration {
    void menu();
}
interface Back {
    void back();
}
class Initiater {
    private List<Calibration> calilist = new ArrayList<Calibration>();;
    private List<Back> Go_Back = new ArrayList<Back>();

    public void Calibration_Listener(Calibration toAdd) {
        calilist.add(toAdd);
    }
    public void Back_Listener(Back toAdd) {
        Go_Back.add(toAdd);
    }

    public void Calibration_Pressed() {
        System.out.println("Calibration Pressed");
        for (Calibration hl : calilist)
            hl.menu();
    }
    public void Back_Button_Pressed() {
        System.out.println("Back Pressed");
        for (Back hl : Go_Back)
            hl.back();
    }
}

//animations and setup
class Main_Screen {
    Initiater initiater;
    private int X;
    private int Y;

    public Main_Screen(int X, int Y) {
        this.initiater = new Initiater();
        this.X = X;
        this.Y = Y;
    }

    private Pane UI_Button(String name, int[] pos, int[] size, Color color) {
        final Text text = new Text(0, 0, name);
        text.setFont(new Font(20));

        final Rectangle outline = new Rectangle(0, 0, size[0], size[1]);
        outline.setFill(color);

        final StackPane stack = new StackPane();
        stack.getChildren().addAll(outline, text);
        stack.setLayoutX(pos[0]);
        stack.setLayoutY(pos[1]);

        return stack;
    }

    public int getX() {
        return this.X;
    }

    public int getY() {
        return this.Y;
    }

    public Initiater getInitiater() {
        return this.initiater;
    }

    public Scene MainScreen(Stage stage) {
        Scene scene = new Scene(new Group(), getX(), getY());
        scene.setFill(Color.WHITE);

        Pane Start = UI_Button("Start", new int[] { getX() - getX() / 4, 0 }, new int[] { getX() / 4, getY() / 4 }, Color.rgb(255, 0, 0));
        Pane Calibration = UI_Button("Callibration", new int[] { 0, ((getY() + (getY() / 8)) / 4) * 0 }, new int[] { getX() / 2, getY() / 4 }, Color.rgb(60, 208, 230));

        System.out.println(Calibration.boundsInLocalProperty());
        ((Group) scene.getRoot()).getChildren().addAll(Calibration, Start);

        final Timeline Main_Button_Animation = new Timeline();
        final KeyFrame Cali_kf = new KeyFrame(Duration.millis(500), new KeyValue(Calibration.translateXProperty(), getX() / 2));
        final KeyFrame Start_kf = new KeyFrame(Duration.millis(500), new KeyValue(Start.translateXProperty(), -getX() / 4));
        Main_Button_Animation.getKeyFrames().addAll(Cali_kf, Start_kf);
        Main_Button_Animation.setRate(-1);
        Main_Button_Animation.jumpTo(Main_Button_Animation.getTotalDuration());
        Main_Button_Animation.play();

        Calibration.setOnMouseClicked(MouseEvent -> {
            Calibration.setDisable(true);
            Start.setDisable(true);
            System.out.println(Calibration.boundsInLocalProperty());
            Main_Button_Animation.setRate(1);
            Main_Button_Animation.jumpTo(Duration.millis(0));
            Main_Button_Animation.play();
            Main_Button_Animation.setOnFinished(event -> {
                this.initiater.Calibration_Pressed();
                System.out.println("done");
            });
        });
        this.initiater.Back_Listener(() -> {
            Main_Button_Animation.setRate(-1);
            Main_Button_Animation.jumpTo(Main_Button_Animation.getTotalDuration());
            Main_Button_Animation.play();
            Main_Button_Animation.setOnFinished(e -> {
                Calibration.setDisable(false);
                Start.setDisable(false);
            });
        });
        return scene;
    }

}

Solution

  • You only define KeyValues for the frame at the end of the Timeline animation. This only allows the Timeline to interpolate between the value at the start of the animation and the target value. Inserting another KeyFrame at the beginning of the animation should fix this issue:

    public Scene MainScreen(Stage stage) {
        final Group root = new Group();
    
        Scene scene = new Scene(root, getX(), getY());
        scene.setFill(Color.WHITE);
    
        Pane Start = UI_Button("Start", new int[] { getX() * 3 / 4, 0 }, new int[] { getX() / 4, getY() / 4 }, Color.RED);
        Pane Calibration = UI_Button("Callibration", new int[] { 0, 0 }, new int[] { getX() / 2, getY() / 4 }, Color.rgb(60, 208, 230));
    
        System.out.println(Calibration.boundsInLocalProperty());
        root.getChildren().addAll(Calibration, Start);
    
        final Timeline Main_Button_Animation = new Timeline(
                new KeyFrame(Duration.ZERO,
                        new KeyValue(Calibration.translateXProperty(), 0),
                        new KeyValue(Start.translateXProperty(), 0)),
                new KeyFrame(Duration.millis(500),
                        new KeyValue(Calibration.translateXProperty(), getX() / 2),
                        new KeyValue(Start.translateXProperty(), -getX() / 4)));
    
        Main_Button_Animation.setRate(-1);
        Main_Button_Animation.playFrom(Main_Button_Animation.getTotalDuration());
    
        Calibration.setOnMouseClicked(MouseEvent -> {
            Calibration.setDisable(true);
            Start.setDisable(true);
            System.out.println(Calibration.boundsInLocalProperty());
            Main_Button_Animation.playFromStart();
            Main_Button_Animation.setOnFinished(event -> {
                this.initiater.Calibration_Pressed();
                System.out.println("done");
            });
        });
        this.initiater.Back_Listener(() -> {
            Main_Button_Animation.setRate(-1);
            Main_Button_Animation.playFrom(Main_Button_Animation.getTotalDuration());
            Main_Button_Animation.setOnFinished(e -> {
                Calibration.setDisable(false);
                Start.setDisable(false);
            });
        });
        return scene;
    }
    

    Note that there have been a few additional changes here:

    • Combining KeyFrames at the same time to a single one with multiple KeyValues
    • Keep a reference to the scene root to avoid the use of the getter/cast
    • Using playFrom and playFromStart instead of jumpTo/play
    • Simplifications of some of the parameters passed to the UI_Button method (rounding could make the result differ by 1 though)
    • ...