Search code examples
asynchronousjavafxtimelineanonymous-types

JavaFX - making Timeline object on multiple objects execute EventHandler once


I am working in JavaFX right now, and have run into a problem. I created a simple example to demonstrate the issue I am facing.

My issue is, I have a Timeline object setup on multiple anonymous Circle objects and I want an action to happen after the Timeline has finished its play() method. To do this, I setup a setOnFinished event handler to execute something after the animation has finished playing, However, it is executing this logic multiple times because it is working on multiple objects.

Here I have a simple program that adds 3 anonymous objects to a VBox and there is a button that will call the flash() method to start a Timeline animation on the circles.

package sample;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        VBox root = new VBox();
        VBox circles = new VBox();

        Button btn = new Button("Touch me to make me flash ;)");
        btn.setOnAction(e -> flash(circles));

        for(int i = 0; i < 3; i++) {
            circles.getChildren().add(new Circle(25, Color.RED));
        }
        circles.setSpacing(10);
        circles.setAlignment(Pos.CENTER);

        root.getChildren().addAll(circles, btn);
        root.setSpacing(10);
        root.setAlignment(Pos.CENTER);

        primaryStage.setScene(new Scene(root, 500, 500));
        primaryStage.show();
    }


    private void flash(VBox root) {
        for(Node circle : root.getChildren()) {
            final Circle c = (Circle) circle;
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.seconds(0.5), e -> c.setFill(Color.GOLD)),
                    new KeyFrame(Duration.seconds(1.0), e -> c.setFill(Color.RED))
            );

            timeline.setCycleCount(5);
            timeline.play();
            timeline.setOnFinished(e -> System.out.println("Do something here"));
        }
    }
}

You can see in the flash() method that there is an EventHandler that executes this line of code:

System.out.println("Do something here")

I want it to only be executing one time, but it executes 3 times because the Timeline object is setup on 3 circles.

How can I make it so the EventHandler only executes one time?


Solution

  • Consider using one TimeLine object for all Circle objects:

    import javafx.animation.KeyFrame;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.geometry.Pos;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.layout.VBox;
    import javafx.scene.paint.Color;
    import javafx.scene.paint.Paint;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class Main extends Application {
    
        private ObjectProperty<Paint> colorProperty;
    
        @Override
        public void start(Stage primaryStage) throws Exception{
    
            colorProperty = new SimpleObjectProperty<>(Color.WHITE);
            VBox root = new VBox();
            VBox circles = new VBox();
    
            Button btn = new Button("Touch me to make me flash ;)");
            btn.setOnAction(e -> flash(circles));
    
            for(int i = 0; i < 3; i++) {
                circles.getChildren().add(new Circle(25, Color.RED));
            }
            circles.setSpacing(10);
            circles.setAlignment(Pos.CENTER);
    
            root.getChildren().addAll(circles, btn);
            root.setSpacing(10);
            root.setAlignment(Pos.CENTER);
    
            primaryStage.setScene(new Scene(root, 500, 500));
            primaryStage.show();
        }
    
        private void flash(VBox root) {
    
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.seconds(0.5), e -> colorProperty.set(Color.GOLD)),
                    new KeyFrame(Duration.seconds(1.0), e -> colorProperty.set(Color.RED))
            );
    
            timeline.setCycleCount(5);
            timeline.play();
            timeline.setOnFinished(e -> System.out.println("Do something here"));
    
            for(Node circle : root.getChildren()) {
                final Circle c = (Circle) circle;
                c.fillProperty().bind(colorProperty);
            }
        }
    
        public static void main(String[] args) {
            launch(null);
        }
    }