Search code examples
javaanimationjavafxtranslate-animation

Finding the distance between two nodes for TranslateTransition


In my program, I want objects to appear that they are moving from one spot to another. I used the TranslateTransition. For it to work, I need to give it a starting location, in my case, the distance to the starting node.

enter image description here

The problem, is the two nodes are in separate containers, so simply using layoutX and layoutY in the calculation will not work.

In my code below, I used the localToScene method for converting coordinates relative to the local node to the scene. Then I did a little subtraction to get the distance between the two nodes relative to the moving node.

import com.neonorb.commons.log.Log;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import javafx.scene.Parent;
import sun.reflect.generics.reflectiveObjects.LazyReflectiveObjectGenerator;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

/**
 * Created by chris on 8/23/15.
 */
public class TranslateUtils {
    public static DoubleProperty getTranslateX(Node translatingNode, Node otherNode) {
        DoubleProperty ret = new SimpleDoubleProperty();
        Platform.runLater(() -> {
            List<Observable> dependencies = getAllParents(translatingNode).stream().map(Node::layoutXProperty).collect(Collectors.toList());
            dependencies.addAll(getAllParents(otherNode).stream().map(Node::layoutXProperty).collect(Collectors.toList()));
            ret.bind(Bindings.createDoubleBinding(new Callable<Double>() {
                @Override
                public Double call() throws Exception {
                    Log.debug("recalculating x");
                    return otherNode.localToScene(otherNode.getBoundsInLocal()).getMinX() - translatingNode.localToScene(translatingNode.getBoundsInLocal()).getMinX();
                }
            }, dependencies.toArray(new Observable[dependencies.size()])));
        });
        return ret;
    }

    public static DoubleProperty getTranslateY(Node translatingNode, Node otherNode) {
        DoubleProperty ret = new SimpleDoubleProperty();
        Platform.runLater(() -> {
            List<Observable> dependencies = getAllParents(translatingNode).stream().map(Node::layoutXProperty).collect(Collectors.toList());
            dependencies.addAll(getAllParents(otherNode).stream().map(Node::layoutXProperty).collect(Collectors.toList()));
            ret.bind(Bindings.createDoubleBinding(new Callable<Double>() {
                @Override
                public Double call() throws Exception {
                    Log.debug("recalculating y");
                    return otherNode.localToScene(otherNode.getBoundsInLocal()).getMinY() - translatingNode.localToScene(translatingNode.getBoundsInLocal()).getMinY();
                }
            }, dependencies.toArray(new Observable[dependencies.size()])));
        });
        return ret;
    }

    private static List<Parent> getAllParents(Node node) {
        List<Parent> parents = new ArrayList<Parent>();
        Parent parent = node.getParent();
        if (parent != null) {
            parents.addAll(getAllParents(parent));
            parents.add(parent);
        }
        return parents;
    }
}

You can test the code with this:

import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.controlsfx.tools.Borders;

import java.io.IOException;

/**
 * Created by chris on 7/20/15.
 */
public class Test extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws IOException {
        BorderPane borderPane = new BorderPane();
        Label translatingNode = new Label("translating node");
        HBox translatingHBox = new HBox(translatingNode);
        translatingHBox.setAlignment(Pos.CENTER);
        borderPane.setTop(translatingHBox);
        Label referenceNode = new Label("reference node");
        HBox referenceHBox = new HBox(referenceNode);
        referenceHBox.setAlignment(Pos.CENTER_RIGHT);
        borderPane.setBottom(referenceHBox);
        TranslateTransition translateTransition = new TranslateTransition(Duration.seconds(3), translatingNode);
        translateTransition.fromXProperty().bind(TranslateUtils.getTranslateX(translatingNode, referenceNode));
        translateTransition.fromYProperty().bind(TranslateUtils.getTranslateY(translatingNode, referenceNode));
        translateTransition.setToX(0);
        translateTransition.setToY(0);
        Button playButton = new Button("play");
        playButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                translateTransition.play();
            }
        });
        borderPane.setCenter(playButton);
        primaryStage.setScene(new Scene(borderPane));
        primaryStage.show();
    }
}

You can successfully play the animation the first time, but try resizing the window. The translate locations don't get updated.


Solution

  • I ended up using custom methods to find the global location:

    private static double getGlobalX(Node node) {
        if (node == null) {
            return 0.0;
        }
        double parentGlobalX = getGlobalX(node.getParent());
        return node.getLayoutX() - parentGlobalX;
    }
    
    private static double getGlobalY(Node node) {
        if (node == null) {
            return 0.0;
        }
        double parentGlobalY = getGlobalY(node.getParent());
        return parentGlobalY - node.getLayoutY();
    }