Layout animation

The underlying FlowPane in JavaFX exposes elements within each window when the window is resized. However, there is no animation, and the result is pretty sharp.

I connected the change listener to the layoutX and layoutY properties of each Node inside the FlowPane, and the result works more or less, but sometimes when I quickly resize the window, the elements remain in inconsistent places.

What am I missing?

package javafxapplication1; import java.util.HashMap; import java.util.List; import java.util.Map; import javafx.animation.Transition; import javafx.animation.TranslateTransition; import javafx.beans.property.DoubleProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.util.Duration; /** * Animates an object when its position is changed. For instance, when * additional items are added to a Region, and the layout has changed, then the * layout animator makes the transition by sliding each item into its final * place. */ public class LayoutAnimator implements ChangeListener, ListChangeListener<Node> { private Map<Node, Transition> nodesInTransition; public LayoutAnimator() { this.nodesInTransition = new HashMap<>(); } /** * Animates all the children of a Region. * <code> * VBox myVbox = new VBox(); * LayoutAnimator animator = new LayoutAnimator(); * animator.observe(myVbox.getChildren()); * </code> * * @param nodes */ public void observe(ObservableList<Node> nodes) { for (Node node : nodes) { this.observe(node); } nodes.addListener(this); } public void unobserve(ObservableList<Node> nodes) { nodes.removeListener(this); } public void observe(Node n) { n.layoutXProperty().addListener(this); n.layoutYProperty().addListener(this); } public void unobserve(Node n) { n.layoutXProperty().removeListener(this); n.layoutYProperty().removeListener(this); } @Override public void changed(ObservableValue ov, Object oldValue, Object newValue) { final Double oldValueDouble = (Double) oldValue; final Double newValueDouble = (Double) newValue; final Double changeValueDouble = newValueDouble - oldValueDouble; DoubleProperty doubleProperty = (DoubleProperty) ov; Node node = (Node) doubleProperty.getBean(); final TranslateTransition t; if ((TranslateTransition) nodesInTransition.get(node) == null) { t = new TranslateTransition(Duration.millis(150), node); } else { t = (TranslateTransition) nodesInTransition.get(node); } if (doubleProperty.getName().equals("layoutX")) { Double orig = node.getTranslateX(); if (Double.compare(t.getFromX(), Double.NaN) == 0) { t.setFromX(orig - changeValueDouble); t.setToX(orig); } } if (doubleProperty.getName().equals("layoutY")) { Double orig = node.getTranslateY(); if (Double.compare(t.getFromY(), Double.NaN) == 0) { t.setFromY(orig - changeValueDouble); t.setToY(orig); } } t.play(); } @Override public void onChanged(ListChangeListener.Change change) { while (change.next()) { if (change.wasAdded()) { for (Node node : (List<Node>) change.getAddedSubList()) { this.observe(node); } } else if (change.wasRemoved()) { for (Node node : (List<Node>) change.getRemoved()) { this.unobserve(node); } } } } } 

A Gist is available here for readability and comes with a test case: https://gist.github.com/teyc/5668517

+8
javafx-2
source share
1 answer

This is a really neat idea. I like it. You should consider introducing your new layout manager into jfxtras .

Why is your original solution not working

Your problem is the logic associated with trying to write the original value for the translateX / Y values ​​and completing the translation to the original value.

When a TranslationTransition is being executed, it changes the values ​​of translateX / Y. With your current code, if you quickly resize the screen before the animation finishes, you set the toX / Y properties of your TranslateTransition to the current translateX / Y values. This makes the final resting place of the animation some preliminary intermediate point, and not the desired final resting place for the node (the desired place is just the point where the translateX / Y values ​​for the node are 0).

How to fix it

The fix is ​​simple - toX / Y for TranslateTransition should always be zero, so the transition always ends with the node switching to the fact that it should not have the current layout position without translation.

Code snippet code example

Here is the basic updated conversion code:

 TranslateTransition t; switch (doubleProperty.getName()) { case "layoutX": t = nodeXTransitions.get(node); if (t == null) { t = new TranslateTransition(Duration.millis(150), node); t.setToX(0); nodeXTransitions.put(node, t); } t.setFromX(node.getTranslateX() - delta); node.setTranslateX(node.getTranslateX() - delta); break; default: // "layoutY" t = nodeYTransitions.get(node); if (t == null) { t = new TranslateTransition(Duration.millis(150), node); t.setToY(0); nodeYTransitions.put(node, t); } t.setFromY(node.getTranslateY() - delta); node.setTranslateY(node.getTranslateY() - delta); } t.playFromStart(); 

Sampling decision result

Output of the updated animator after adding a few more rectangles and resizing the screen to create new layouts (using the test harness that you pointed to gist):

layoutanimator

Debug animations and fix additional problems with your sample code

In debug animations, I find it easier to slow down the animation. To decide how to fix the published program, I switched TranslateTransition for a second to give enough time to see what was actually happening.

Slowing down the animation helped me understand that the actual animations created by your listeners do not seem to happen until the frame after the node is restored, making a brief glitch when the target position appears in it for a moment, then go back to starting position, then slowly plunge into the target position.

The fix for the initial smoothing of positioning is to put the node in the listener back to its original position before starting the animation.

Your code uses the nodeInTransition map to keep track of the current moving nodes, but it never puts anything on the map. I renamed it to nodeXTransitions and nodeYTransitions (because I use separate x and y transitions, not one transition for both, since using separate ones was easier). I put node transitions on the map as they are created so that I can stop the old transitions when creating new ones. It didn’t seem absolutely necessary, because everything seemed to work fine without map logic (maybe JavaFX is already doing something like this implicitly inside the animation frame), but it seems that it’s safe, so I saved it.

After I made the changes described above, everything seemed to work fine.

Possible additional improvements

There may be some improvements that can be made to synchronize animations, so if the animation is partially playing and relaying, you probably do not want to play the whole animation from the very beginning for the new repeater. Or perhaps you want all the animated nodes to move at a constant, uniform speed, rather than duration. But visually it did not seem to matter much, so I would not worry about it.

Test system

I tested on Java8b91 and OS X 10.8.

Full code for updated solution

Full code for the updated layout animator:

 import javafx.animation.TranslateTransition; import javafx.beans.property.DoubleProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.util.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Animates an object when its position is changed. For instance, when * additional items are added to a Region, and the layout has changed, then the * layout animator makes the transition by sliding each item into its final * place. */ public class LayoutAnimator implements ChangeListener<Number>, ListChangeListener<Node> { private Map<Node, TranslateTransition> nodeXTransitions = new HashMap<>(); private Map<Node, TranslateTransition> nodeYTransitions = new HashMap<>(); /** * Animates all the children of a Region. * <code> * VBox myVbox = new VBox(); * LayoutAnimator animator = new LayoutAnimator(); * animator.observe(myVbox.getChildren()); * </code> * * @param nodes */ public void observe(ObservableList<Node> nodes) { for (Node node : nodes) { this.observe(node); } nodes.addListener(this); } public void unobserve(ObservableList<Node> nodes) { nodes.removeListener(this); } public void observe(Node n) { n.layoutXProperty().addListener(this); n.layoutYProperty().addListener(this); } public void unobserve(Node n) { n.layoutXProperty().removeListener(this); n.layoutYProperty().removeListener(this); } @Override public void changed(ObservableValue<? extends Number> ov, Number oldValue, Number newValue) { final double delta = newValue.doubleValue() - oldValue.doubleValue(); final DoubleProperty doubleProperty = (DoubleProperty) ov; final Node node = (Node) doubleProperty.getBean(); TranslateTransition t; switch (doubleProperty.getName()) { case "layoutX": t = nodeXTransitions.get(node); if (t == null) { t = new TranslateTransition(Duration.millis(150), node); t.setToX(0); nodeXTransitions.put(node, t); } t.setFromX(node.getTranslateX() - delta); node.setTranslateX(node.getTranslateX() - delta); break; default: // "layoutY" t = nodeYTransitions.get(node); if (t == null) { t = new TranslateTransition(Duration.millis(150), node); t.setToY(0); nodeYTransitions.put(node, t); } t.setFromY(node.getTranslateY() - delta); node.setTranslateY(node.getTranslateY() - delta); } t.playFromStart(); } @Override public void onChanged(Change change) { while (change.next()) { if (change.wasAdded()) { for (Node node : (List<Node>) change.getAddedSubList()) { this.observe(node); } } else if (change.wasRemoved()) { for (Node node : (List<Node>) change.getRemoved()) { this.unobserve(node); } } } } } 

When making layout animation changes that are independent of TranslateX / Y user preferences

One of the drawbacks of the solution currently presented is that if the user of the custom layout manager applied the translateX / Y parameters directly to the laid out nodes, they will lose these values ​​because the layout manager transfers all the content (since the X / Y transformation ends with which is set to 0).

To save a custom X / Y translation, the solution can be updated to use custom transitions rather than translating transitions. Custom transitions can be applied to Translate properties on transform nodes. Then only an additional conversion on each node affects the internal operation of the layout animation - the original translateX / Y values ​​of the user are not affected.

I highlighted the point from your question in order to implement the advanced transition enhancement mentioned above .


If you were passionate, you could look at the openjfx code for the tabs, TitlesPanes and Accordions headers and see how the skins for these controls are handled animating layout changes for their child nodes.

+10
source share

All Articles