Search code examples
javaanimationjavafxdelaytransition

How to create a continuous TranslateTransition in JavaFX


I'm using JavaFX to create a Java application which is able to apply a TranslateTransition to a generic node and recall it continuously. I retrieved a simple right arrow from this url https://www.google.it/search?q=arrow.png&espv=2&source=lnms&tbm=isch&sa=X&ved=0ahUKEwiGheeJvYrTAhWMB5oKHU3-DxgQ_AUIBigB&biw=1600&bih=764#imgrc=rH0TbMkQY2kUaM: and used it to create the node to translate. This is my AnimatedNode class:

package application.model.utils.addon;

import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.util.Duration;

public class AnimatedNode {

    private Node node;
    private double positionY;
    private TranslateTransition translateTransition;
    private boolean animated;

    private int reverse = 1;

    public AnimatedNode(Node node, double animationTime) {
        setPositionY(0.0);
        setNode(node);
        setTranslateTransition(animationTime);
    }

    public void play() {
        if(translateTransition != null && !isAnimated()) {

            setAnimated(true);

            new Thread() {
                @Override
                public void run() {
                    while(isAnimated()) {
                        translateTransition.setToY(positionY + 50 * reverse);
                        translateTransition.play();

                        reverse = -reverse;
                        setPositionY(translateTransition.getToY());
                    }
                }
            }.start();
        }
    }

    public void stop() {
        setAnimated(false);
    }

    public Node getNode() {
        return node;
    }

    private void setNode(Node node) {
        this.node = node;
    }

    public TranslateTransition getTranslateTransition() {
        return translateTransition;
    }

    private void setTranslateTransition(double animationTime) {
        translateTransition = new TranslateTransition();

        if(node != null) {
            translateTransition.setDuration(Duration.seconds(animationTime));
            translateTransition.setNode(node);
        }
    }

    public double getPositionY() {
        return positionY;
    }

    private void setPositionY(double positionY) {
        this.positionY = positionY;
    }

    public boolean isAnimated() {
        return animated;
    }

    private void setAnimated(boolean animated) {
        this.animated = animated;
    }
}

and this is the Application class

package test;

import application.model.utils.addon.AnimatedNode;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class Test extends Application {

    private final String TITLE = "Test application";
    private final double WIDTH = 600;
    private final double HEIGHT = 400;
    private final String ARROW_PATH = "file:resources/png/arrow.png";

    private BorderPane rootPane;

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle(TITLE);

        rootPane = new BorderPane();
        rootPane.setPrefSize(WIDTH, HEIGHT);

        Image image = new Image(ARROW_PATH);
        ImageView imageView = new ImageView(image);
        imageView.setFitWidth(WIDTH);
        imageView.setFitHeight(HEIGHT);
        imageView.setPreserveRatio(true);
        AnimatedNode animatedNode = new AnimatedNode(imageView, 0.7);

        Pane pane = new Pane();
        pane.getChildren().add(animatedNode.getNode());

        pane.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent arg0) {
                if(arg0.getButton().equals(MouseButton.PRIMARY))
                    animatedNode.play();

                if(arg0.getButton().equals(MouseButton.SECONDARY))
                    animatedNode.stop();
            }
        });

        rootPane.setCenter(pane);
        Scene scene = new Scene(rootPane, WIDTH, HEIGHT);

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

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

The node is added to a generic pane; the pane has a MouseListener. I can start the TranslateTransition by using the primary button of the mouse and stop it with the secondary one. I used a Thread in the play() method of AnimatedNode but I still have a continuous delay in the transition. Is this the best way to perform the transition? Can I improve my code?

Thanks a lot for your support.


Solution

  • Sample

    This is a simplified example which demonstrates a continuous animation started and stopped by left and right mouse clicks.

    import javafx.animation.*;
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.image.*;
    import javafx.scene.layout.Pane;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class BouncingCat extends Application {    
        private static final double WIDTH = 100;
        private static final double HEIGHT = 100;
        private final String ARROW_PATH =
                "http://icons.iconarchive.com/icons/iconka/meow-2/64/cat-rascal-icon.png";
                // image source: http://www.iconka.com
    
        @Override
        public void start(Stage stage) {
            Image image = new Image(ARROW_PATH);
            ImageView imageView = new ImageView(image);
            TranslateTransition animation = new TranslateTransition(
                    Duration.seconds(0.7), imageView
            );
            animation.setCycleCount(Animation.INDEFINITE);
            animation.setFromY(0);
            animation.setToY(50);
            animation.setAutoReverse(true);
    
            Pane pane = new Pane(imageView);
            Scene scene = new Scene(pane, WIDTH, HEIGHT);
    
            scene.setOnMouseClicked(e -> {
                switch (e.getButton()) {
                    case PRIMARY:
                        animation.play();
                        break;
    
                    case SECONDARY:
                        animation.pause();
                        break;
                }
            });
    
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Advice

    You don't need a Thread when you have a Transition. JavaFX will render updated transition frames automatically each pulse.

    I don't advise keeping track of properties in a class, when those same values are already represented in the underlying tools you use.

    For example:

    I wouldn't advise calling your class AnimatedNode unless it extended node, otherwise it is confusing, instead call it something like AnimationControl.

    import javafx.animation.TranslateTransition;
    import javafx.scene.Node;
    import javafx.util.Duration;
    
    public class AnimationControl {
        private final TranslateTransition translateTransition;
    
        public AnimationControl(Duration duration, Node node) {
            translateTransition = new TranslateTransition(duration, node);
        }
    
        public TranslateTransition getTranslateTransition() {
            return translateTransition;
        }
    }
    

    You only need to encapsulate the node and the transition in the AnimationControl and not other fields unless you need further functionality not apparent in your question and not already provided by Node or Transition. If you have that extra functionality then you can enhance the AnimationControl class above to add it.

    Exposing the node and the translate transition is enough, as if the user wants to manage the animation, such as starting and stopping it, then the user can just get it from the AnimationControl class. Depending on your use case, the entire AnimationControl class might be unnecessary as you might not need the encapsulation it provides and might instead prefer to just work directly with the node and the transition (as demoed in the sample).