Create a path transition with absolute coordinates

2020-04-07 19:48发布

OrangeBlock is an orange block with text inside. It is implemented as a StackPane that contains text on top of a rectangle. (This approach is demonstrated in the documentation for StackPane.)

I've placed an OrangeBlock at coordinates (100, 80) and am now trying to make it travel smoothly to some target coordinates. Unfortunately I get a nasty bump in my path:

Bumpy PathTransition

For some reason the coordinates in the PathElements are interpreted relative to the orange block.

Why is this? And how can I make my OrangeBlock travel along a path with absolute coordinates? Minimal working example below.

import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class PathTransitionExample extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Group root = new Group();

        OrangeBlock block = new OrangeBlock(60, 40);
        block.relocate(100, 80);
        root.getChildren().add(block);

        PathTransition transition = newPathTransitionTo(block, 460, 320);

        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
        transition.play();
    }

    private static PathTransition newPathTransitionTo(OrangeBlock block,
            double toX, double toY) {
        double fromX = block.getLayoutX();
        double fromY = block.getLayoutY();

        Path path = new Path();
        path.getElements().add(new MoveTo(fromX, fromY));
        path.getElements().add(new LineTo(toX, toY));

        PathTransition transition = new PathTransition();
        transition.setPath(path);
        transition.setNode(block);
        transition.setDelay(Duration.seconds(1));
        transition.setDuration(Duration.seconds(2));

        return transition;
    }

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

    private static class OrangeBlock extends StackPane {
        public OrangeBlock(int width, int height) {
            Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
            Text text = new Text("Block");
            getChildren().addAll(rectangle, text);
        }
    }
}

标签: javafx
5条回答
beautiful°
2楼-- · 2020-04-07 20:21

Both @jdub1581 and @Lorand have given valid points:

  • Transition is applied modifying block's translateXProperty() and translateYProperty().
  • Transition is applied on the center of the block.

I'll add one more thing:

  • We are mixing two different things: the global path we want the block to follow, and the local path we have to apply to the transition, so the block follows the first one.

Let's add a pathScene to the group:

Path pathScene = new Path();
pathScene.getElements().add(new MoveTo( block.getLayoutX(),  block.getLayoutY()));
pathScene.getElements().add(new LineTo(460, 320));
root.getChildren().add(pathScene);

This will be our scene now (I've added two labels with the coordinates of the origin and end of the path for clarity):

Path in scene coordinates

Now we need to determine the local path, so we'll change pathScene elements to local coordinates of the block, and translate it to its center:

Path pathLocal = new Path();
pathScene.getElements().forEach(elem->{
    if(elem instanceof MoveTo){
        Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
        Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
        pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
    } else if(elem instanceof LineTo){
        Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
        Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
        pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
    }
});

As @Lorand also mentioned, this should be done after the stage is shown to compute the size of the block.

Now we can create the transition and play it.

PathTransition transition = new PathTransition();
transition.setPath(pathLocal);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));

transition.play();

Finally, this is all the code we need to make the block follow the desired path:

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

    OrangeBlock block = new OrangeBlock(60, 40);
    block.relocate(100, 80);
    root.getChildren().add(block);

    // Path in scene coordinates, added to group 
    // in order to visualize the transition path for the block to follow
    Path pathScene = new Path();
    pathScene.getElements().add(new MoveTo(block.getLayoutX(), block.getLayoutY()));
    pathScene.getElements().add(new LineTo(460, 320));
    root.getChildren().add(pathScene);

    primaryStage.setScene(new Scene(root, 600, 400));
    primaryStage.show();

    PathTransition transition = newPathTransitionTo(pathScene, block);
    transition.play();
}

private PathTransition newPathTransitionTo(Path pathScene, OrangeBlock block) {
    // Calculate the path in local coordinates of the block
    // so transition is applied to the block without bumps
    Path pathLocal = new Path();
    pathScene.getElements().forEach(elem->{
        if(elem instanceof MoveTo){
            Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
            Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
            pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
        } else if(elem instanceof LineTo){
            Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
            Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
            pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
        }
    });
    PathTransition transition = new PathTransition();
    transition.setPath(pathLocal);
    transition.setNode(block);
    transition.setDelay(Duration.seconds(1));
    transition.setDuration(Duration.seconds(2));

    return transition;
}

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

private static class OrangeBlock extends StackPane {
    public OrangeBlock(int width, int height) {
        Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
        Text text = new Text("Block");
        getChildren().addAll(rectangle, text);
    }
}

Note that this solution is equivalent to the one given by @Lorand.

If we monitorize the X, Y translate properties of the block, these go from (0,0) to (360,240), which are just the relative ones on the global path.

查看更多
小情绪 Triste *
3楼-- · 2020-04-07 20:35

So Basically,

the relocate(x,y) method sets the layout x/y values... the Transition uses the translateX/Y values...

I could be wrong but I believe that when either value gets invalidated the scene runs a "layout" pass on the nodes in scene.

When this happens it tries to set the node to it's known layoutX/Y values, which are set when you call relocate(x,y) (layout values are 0,0 by default).

This causes the node to be drawn in "layoutPosition" then "pathPosition" within the transition at each step, causing jitters and the node to be offset from where it should be.

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

    OrangeBlock block = new OrangeBlock(60, 40);
    System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
    block.relocate(100, 80);
    //block.setTranslateX(100);
    //block.setTranslateY(80);
    System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
    root.getChildren().add(block);

    PathTransition transition = newPathTransitionTo(block, 460, 320);
    transition.currentTimeProperty().addListener(e->{
        System.out.println("\nLayout Values: " + block.getLayoutX() + " : " + block.getLayoutY()
                +"\nTranslate Values:" + block.getTranslateX() + " : " + block.getTranslateY()
    );});
    primaryStage.setScene(new Scene(root, 600, 400));
    primaryStage.show();
    transition.play();


}

private static PathTransition newPathTransitionTo(OrangeBlock block,
        double toX, double toY) {
    double fromX = block.getLayoutX();//getTranslateX();
    double fromY = block.getLayoutY();//getTranslateY();

    Path path = new Path();
    path.getElements().add(new MoveTo(fromX, fromY));
    path.getElements().add(new LineTo(toX, toY));

    PathTransition transition = new PathTransition();
    transition.setPath(path);
    transition.setNode(block);
    transition.setDelay(Duration.seconds(1));
    transition.setDuration(Duration.seconds(2));

    return transition;
}

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

private static class OrangeBlock extends StackPane {
    public OrangeBlock(int width, int height) {
        Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
        Text text = new Text("Block");
        getChildren().addAll(rectangle, text);
    }
}

Not going to post pics, But my first result was like your first post, changing it to the above code gave me the second post result.

Usually it is always best to avoid "layout" values on a dynamic (moving) object for these reasons. If you like the convenience of the relocate method, I'd implement your own setting the translate values instead.

Cheers :)

EDIT: I edited some code to print what happens as the transition is running so you can see what happens in your original version..

查看更多
霸刀☆藐视天下
4楼-- · 2020-04-07 20:37

I debugged the JavaFX code out of curiosity. Seems like you are out of luck with a proper solution. Here's what happens:

The PathTransition code has a method interpolate(double frac) which includes:

cachedNode.setTranslateX(x - cachedNode.impl_getPivotX());
cachedNode.setTranslateY(y - cachedNode.impl_getPivotY());

The impl_getPivotX() and impl_getPivotY() methods contain this:

public final double impl_getPivotX() {
    final Bounds bounds = getLayoutBounds();
    return bounds.getMinX() + bounds.getWidth()/2;
}

public final double impl_getPivotY() {
    final Bounds bounds = getLayoutBounds();
    return bounds.getMinY() + bounds.getHeight()/2;
}

So the PathTransition always uses the center of your node for the calculation. In other words this works with e. g. a Circle node, but not with e. g. a Rectangle node. Moreover you need the layoutBounds, so the PathTransition must be created after the bounds were made available.

You can see in the PathTransition code that the calculations are all relative and already involve the layout position. So in your lineTo you have to consider this.

Worth noting is that the LineTo class has a method setAbsolut(boolean). However it doesn't solve your problem.

So my solution to your problem would be

  • creating the PathTransition after the primary stage was made visible
  • modification of the moveTo and lineTo parameters

This works for me (I added a Rectangle shape to identify the proper bounds visually):

public class PathTransitionExampleWorking2 extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {

        Group root = new Group();

        Rectangle rect = new  Rectangle( 100, 80, 460-100+60, 320-80+40);
        root.getChildren().add(rect);

        OrangeBlock block = new OrangeBlock(60, 40);
        block.relocate( 100, 80);

        root.getChildren().add(block);

        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();

        // layout bounds are used in path transition => PathTransition creation must happen when they are available
        PathTransition transition = newPathTransitionTo(block, 460, 320);
        transition.play();
    }

    private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {

        double fromX = block.getLayoutBounds().getWidth() / 2;
        double fromY = block.getLayoutBounds().getHeight() / 2;

        toX -= block.getLayoutX() - block.getLayoutBounds().getWidth() / 2;
        toY -= block.getLayoutY() - block.getLayoutBounds().getHeight() / 2;

        Path path = new Path();
        path.getElements().add(new MoveTo(fromX, fromY));
        path.getElements().add(new LineTo(toX, toY));

        PathTransition transition = new PathTransition();
        transition.setPath(path);
        transition.setNode(block);
        transition.setDelay(Duration.seconds(1));
        transition.setDuration(Duration.seconds(2));

        return transition;
    }

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

    private static class OrangeBlock extends StackPane {
        public OrangeBlock(int width, int height) {
            Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
            Text text = new Text("Block");
            getChildren().addAll(rectangle, text);
        }
    }
}

edit: another solution would be to use this instead of MoveTo and LineTo:

public static class MoveToAbs extends MoveTo {

    public MoveToAbs( Node node) {
        super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
    }

}

public static class LineToAbs extends LineTo {

    public LineToAbs( Node node, double x, double y) {
        super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
    }

}

Note: You still have to create the PathTransition after the primaryStage was created.

edit: here's another example with the block moving to the position of the mouse-click:

public class PathTransitionExample extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {

        Group root = new Group();

        OrangeBlock block = new OrangeBlock(60, 40);
        block.relocate(100, 80);
        root.getChildren().add(block);

        Label label = new Label( "Click on scene to set destination");
        label.relocate(0, 0);
        root.getChildren().add(label);

        Scene scene = new Scene(root, 600, 400);

        scene.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<Event>() {

            PathTransition transition;

            {
                transition = new PathTransition();
                transition.setNode(block);
                transition.setDuration(Duration.seconds(2));

            }

            @Override
            public void handle(Event event) {

                transition.stop();

                setPositionFixed(block.getLayoutX() + block.getTranslateX(), block.getLayoutY() + block.getTranslateY());

                double x = ((MouseEvent) event).getX();
                double y = ((MouseEvent) event).getY();

                Path path = new Path();
                path.getElements().add(new MoveToAbs( block));
                path.getElements().add(new LineToAbs( block, x, y));

                transition.setPath(path);
                transition.play();

            }

            private void setPositionFixed( double x, double y) {
                block.relocate(x, y);
                block.setTranslateX(0);
                block.setTranslateY(0);
            }

        });

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

        PathTransition transition = newPathTransitionTo(block, 460, 320);
        transition.play();

    }

    private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {

        Path path = new Path();
        path.getElements().add(new MoveToAbs( block));
        path.getElements().add(new LineToAbs( block, toX, toY));

        PathTransition transition = new PathTransition();
        transition.setPath(path);
        transition.setNode(block);
        transition.setDelay(Duration.seconds(1));
        transition.setDuration(Duration.seconds(2));

        return transition;
    }

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

    private static class OrangeBlock extends StackPane {
        public OrangeBlock(int width, int height) {
            Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
            Text text = new Text("Block");
            getChildren().addAll(rectangle, text);
        }
    }

    public static class MoveToAbs extends MoveTo {

        public MoveToAbs( Node node) {
            super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
        }

    }

    public static class LineToAbs extends LineTo {

        public LineToAbs( Node node, double x, double y) {
            super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
        }

    }
}
查看更多
▲ chillily
5楼-- · 2020-04-07 20:42

The solution I'm using now is to simply offset layoutX and layoutY of the Path in the opposite direction.

private static void offsetPathForAbsoluteCoords(Path path, OrangeBlock block) {
    Node rectangle = block.getChildren().iterator().next();
    double width = rectangle.getLayoutBounds().getWidth();
    double height = rectangle.getLayoutBounds().getHeight();

    path.setLayoutX(-block.getLayoutX() + width / 2);
    path.setLayoutY(-block.getLayoutY() + height / 2);
}

Inserting a call to this method immediately after the Path instantiation fixes the problem.

enter image description here

I'm not really satisfied with this solution. I don't understand why layoutX and layoutY need to be involved at all. Is there a neater way?

查看更多
The star\"
6楼-- · 2020-04-07 20:42

You're using relocate function to locate your Block. relocate function makes computation on x and y for locating your object. If you used setLayoutX to locate Block and then use getLayoutX, this problem might not be happened. Same explanations is valid for y property.

You can find some informations about your problem in here: http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#layoutXProperty

查看更多
登录 后发表回答