Issue
I am currently developing a small JavaFX library to show svg content. I am in the process of implementing animations. They work correctly, except for animate
or animateTransform
for x and y positions, and I don't understand why.
When I am doing this, it works flawlessly:
public class TimelineTest extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
Group root = new Group();
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
Rectangle rect = new Rectangle(100, 100, 50, 50);
root.getChildren().add(rect);
rect.setFill(Color.BLUE);
Timeline timeline = new Timeline();
KeyFrame kf0 = new KeyFrame(Duration.ZERO, new KeyValue(rect.xProperty(), 0));
KeyFrame kf1 = new KeyFrame(Duration.seconds(10), new KeyValue(rect.xProperty(), 100));
timeline.getKeyFrames().addAll(kf0, kf1);
timeline.play();
primaryStage.show();
}
}
But when I am doing what I think is exactly the same thing in my library, the rectangle seems to twitch a little but not really move.
My code is:
WritableValue value = null;
switch (nodeName) {
case "rect":
Rectangle rect = (Rectangle) node;
switch (attrName) {
case X:
value = rect.xProperty();
break;
case Y:
value = rect.yProperty();
break;
case WIDTH:
value = rect.widthProperty();
break;
case HEIGHT:
value = rect.heightProperty();
break;
}
break;
}
Timeline timeline = new Timeline();
KeyValue fromValue = new KeyValue(value, 10);
KeyValue toValue = new KeyValue(value, 100);
KeyFrame fromFrame = new KeyFrame(Duration.ZERO, fromValue);
KeyFrame toFrame = new KeyFrame(Duration.seconds(10), toValue);
timeline.getKeyFrames().addAll(fromFrame, toFrame);
Strangely when I'm doing the same thing with the width or height property, it works without any problem.
I suspect that my scene is not created correctly (I checked that I am doing all of this in the platform Thread), but everything else is working without any problem.
If I am trying to use a TranslateTransition
instead, I have exactly the same problem.
After some comments on this question here, I now undestand why I have this problem (but not how to fix it, at least for now). I put the JavaFX content in a ScrollPane. I did not think that it would be relevant in this case, but it is. The code is:
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
Group group = new Group(vBox );
StackPane content = new StackPane(group);
group.layoutBoundsProperty().addListener((observable, oldBounds, newBounds) -> {
content.setMinWidth(newBounds.getWidth());
content.setMinHeight(newBounds.getHeight());
});
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setPannable(true);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setPrefSize(500, 500);
I used the following StackOverflow answer for the ScrollPane: JAVAFX zoom, scroll in ScrollPane
Here is a reproductible example showing everything:
public class TestAnimationInScroll extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100, 50, 50);
rect.setFill(Color.BLUE);
Group root = new Group();
root.getChildren().add(rect);
VBox vBox = new VBox(root);
vBox.setAlignment(Pos.CENTER);
Group group = new Group(root);
StackPane content = new StackPane(group);
group.layoutBoundsProperty().addListener((observable, oldBounds, newBounds) -> {
content.setMinWidth(newBounds.getWidth());
content.setMinHeight(newBounds.getHeight());
});
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setPannable(true);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setPrefSize(500, 500);
Timeline timeline = new Timeline();
KeyFrame kf0 = new KeyFrame(Duration.ZERO, new KeyValue(rect.xProperty(), 0));
KeyFrame kf1 = new KeyFrame(Duration.seconds(10), new KeyValue(rect.xProperty(), 100));
timeline.getKeyFrames().addAll(kf0, kf1);
timeline.play();
content.setOnScroll(new EventHandler<ScrollEvent>() {
public void handle(ScrollEvent event) {
double zoomFactor = 1.05;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 1 / zoomFactor;
}
Bounds groupBounds = group.getBoundsInLocal();
final Bounds viewportBounds = scrollPane.getViewportBounds();
double valX = scrollPane.getHvalue() * (groupBounds.getWidth() - viewportBounds.getWidth());
double valY = scrollPane.getVvalue() * (groupBounds.getHeight() - viewportBounds.getHeight());
group.setScaleX(group.getScaleX() * zoomFactor);
group.setScaleY(group.getScaleY() * zoomFactor);
Point2D posInZoomTarget = group.parentToLocal(new Point2D(event.getX(), event.getY()));
Point2D adjustment = group.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
scrollPane.layout();
scrollPane.setViewportBounds(groupBounds);
groupBounds = group.getBoundsInLocal();
scrollPane.setHvalue((valX + adjustment.getX()) / (groupBounds.getWidth() - viewportBounds.getWidth()));
scrollPane.setVvalue((valY + adjustment.getY()) / (groupBounds.getHeight() - viewportBounds.getHeight()));
}
});
Scene scene = new Scene(scrollPane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
As you can see, it is possible to zoom in the ScrollPane, but the effect of the animation is not visible.
If I use a ScaleTransition in the same context, it works, such as:
public class TestAnimationInScroll extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100, 50, 50);
rect.setFill(Color.BLUE);
Group root = new Group();
root.getChildren().add(rect);
VBox vBox = new VBox(root);
vBox.setAlignment(Pos.CENTER);
Group group = new Group(root);
StackPane content = new StackPane(group);
group.layoutBoundsProperty().addListener((observable, oldBounds, newBounds) -> {
content.setMinWidth(newBounds.getWidth());
content.setMinHeight(newBounds.getHeight());
});
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setPannable(true);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setPrefSize(500, 500);
ScaleTransition transition = new ScaleTransition(Duration.seconds(5), rect);
transition.setFromX(1d);
transition.setFromY(1d);
transition.setToX(3d);
transition.setToY(3d);
transition.play();
content.setOnScroll(new EventHandler<ScrollEvent>() {
public void handle(ScrollEvent event) {
double zoomFactor = 1.05;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 1 / zoomFactor;
}
Bounds groupBounds = group.getBoundsInLocal();
final Bounds viewportBounds = scrollPane.getViewportBounds();
double valX = scrollPane.getHvalue() * (groupBounds.getWidth() - viewportBounds.getWidth());
double valY = scrollPane.getVvalue() * (groupBounds.getHeight() - viewportBounds.getHeight());
group.setScaleX(group.getScaleX() * zoomFactor);
group.setScaleY(group.getScaleY() * zoomFactor);
Point2D posInZoomTarget = group.parentToLocal(new Point2D(event.getX(), event.getY()));
Point2D adjustment = group.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
scrollPane.layout();
scrollPane.setViewportBounds(groupBounds);
groupBounds = group.getBoundsInLocal();
scrollPane.setHvalue((valX + adjustment.getX()) / (groupBounds.getWidth() - viewportBounds.getWidth()));
scrollPane.setVvalue((valY + adjustment.getY()) / (groupBounds.getHeight() - viewportBounds.getHeight()));
}
});
Scene scene = new Scene(scrollPane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Do somebody have a clue of where is my mistake? Perhaps there is a way to still be able to pan and zoom in the scrollPane but still seeing correctly the animations?
Solution
I understood the problem: in fact it is because both the StackPane
and Group
will automatically update their layout when the children position or translation change. The solution is to not allow that.
It is simple for the Group
because it has a setAutoSizeChildren(boolean)
method.
But I had to create a subclass of StackPane
to do the same thing:
private class MyStackPane extends StackPane {
private boolean allowLayoutChildren = true;
private MyStackPane(Node root) {
super(root);
}
private void allowLayoutChildren(boolean allow) {
this.allowLayoutChildren = allow;
}
@Override
public void layoutChildren() {
if (allowLayoutChildren) {
super.layoutChildren();
}
}
}
Then the full (working) code is:
public class TestAnimationInScroll extends Application {
private Group root = null;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100, 50, 50);
rect.setFill(Color.BLUE);
root = new Group();
root.getChildren().add(rect);
MyStackPane content = new MyStackPane(root);
root.layoutBoundsProperty().addListener((observable, oldBounds, newBounds) -> {
content.setMinWidth(newBounds.getWidth());
content.setMinHeight(newBounds.getHeight());
});
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setPannable(true);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setPrefSize(500, 500);
root.setAutoSizeChildren(false);
content.allowLayoutChildren(false);
Timeline timeline = new Timeline();
KeyFrame kf0 = new KeyFrame(Duration.ZERO, new KeyValue(rect.xProperty(), 0));
KeyFrame kf1 = new KeyFrame(Duration.seconds(10), new KeyValue(rect.xProperty(), 100));
timeline.getKeyFrames().addAll(kf0, kf1);
timeline.play();
content.setOnScroll(new EventHandler<ScrollEvent>() {
@Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.05;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 1 / zoomFactor;
}
Bounds groupBounds = root.getBoundsInLocal();
final Bounds viewportBounds = scrollPane.getViewportBounds();
double valX = scrollPane.getHvalue() * (groupBounds.getWidth() - viewportBounds.getWidth());
double valY = scrollPane.getVvalue() * (groupBounds.getHeight() - viewportBounds.getHeight());
root.setScaleX(root.getScaleX() * zoomFactor);
root.setScaleY(root.getScaleY() * zoomFactor);
Point2D posInZoomTarget = root.parentToLocal(new Point2D(event.getX(), event.getY()));
Point2D adjustment = root.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
scrollPane.layout();
scrollPane.setViewportBounds(groupBounds);
groupBounds = root.getBoundsInLocal();
scrollPane.setHvalue((valX + adjustment.getX()) / (groupBounds.getWidth() - viewportBounds.getWidth()));
scrollPane.setVvalue((valY + adjustment.getY()) / (groupBounds.getHeight() - viewportBounds.getHeight()));
}
});
Scene scene = new Scene(scrollPane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private class MyStackPane extends StackPane {
private boolean allowLayoutChildren = true;
private MyStackPane(Node root) {
super(root);
}
private void allowLayoutChildren(boolean allow) {
this.allowLayoutChildren = allow;
}
@Override
public void layoutChildren() {
if (allowLayoutChildren) {
super.layoutChildren();
}
}
}
}
Answered By - Hervé Girod
Answer Checked By - Terry (JavaFixing Volunteer)