Add deck pane

This commit is contained in:
mkpaz 2023-02-19 16:00:05 +04:00
parent ea88de54a6
commit e415674cf7
2 changed files with 877 additions and 0 deletions

@ -0,0 +1,659 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.layout;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jetbrains.annotations.Nullable;
/**
* <p>DeckPane represents a pane that displays all of its child nodes in a deck,
* where only one node can be visible at a time. It does not maintain any sequence
* (model), but only cares about the top node, which can be changed by various
* transition effects.<p/>
*
* <h3>View Order</h3>
*
* <p>DeckPane manages {@link Node#viewOrderProperty()} of its children. Topmost
* visible node always has a higher view order value, while the rest of the nodes
* have the default value, which is zero. Following that logic, one must not set
* child nodes view order manually, because it will break the contract.<p/>
*
* <p>If all child nodes have the same view order value (default state after creating
* a new DeckPane), they are displayed in order specified by the root container, which
* is {@link AnchorPane}. When a node is removed from the pane, its view order is
* restored automatically.<p/>
*/
public class DeckPane extends AnchorPane {
protected static final Comparator<Node> Z_COMPARATOR = Comparator.comparingDouble(Node::getViewOrder);
// while transition lasts, animated nodes must be on the top
protected static final int Z_ANIMATED_IN = -20;
protected static final int Z_ANIMATED_OUT = -15;
// topmost deck node
protected static final int Z_DECK_TOP = -10;
// the rest of the nodes
protected static final int Z_DEFAULT = 0;
public DeckPane() {
super();
getChildren().addListener((ListChangeListener<Node>) change -> {
while (change.next()) {
// restore view order for removed nodes
if (change.wasRemoved()) {
change.getRemoved().forEach(node -> node.setViewOrder(Z_DEFAULT));
}
}
});
}
public DeckPane(Node... children) {
this();
getChildren().addAll(children);
}
/**
* Returns the node with the higher view order value or the last node
* if all child nodes have the same view order value.
*/
public @Nullable Node getTopNode() {
var size = getChildren().size();
if (size == 0) {
return null;
}
return getChildren().stream()
// if two elements have equal viewOrder, last wins
// unlike the default min() implementation
.reduce((o1, o2) -> Z_COMPARATOR.compare(o1, o2) >= 0 ? o2 : o1)
// if all equal, return the last node
.orElse(getChildren().get(size - 1));
}
/**
* Sets given node on top without playing any transition.
* Does nothing if that node isn't added to the pane.
*/
public void setTopNode(Node target) {
if (!getChildren().contains(target)) {
return;
}
var topNode = getTopNode();
if (topNode == null || topNode == target) {
return;
}
setViewOrder(topNode, Z_DEFAULT);
setViewOrder(target, Z_DECK_TOP);
}
/**
* Sets current top node view order to the default value.
*/
public void resetTopNode() {
var topNode = getTopNode();
if (topNode != null) {
setViewOrder(topNode, Z_DEFAULT);
}
}
/**
* Adds given nodes to the pane and binds them to the pane edges
* using the provided offsets. See {@link AnchorPane#setTopAnchor(Node, Double)}
* for the reference.
*/
public void addChildren(Insets offsets, Node... nodes) {
for (var node : nodes) {
AnchorPane.setTopAnchor(node, offsets.getTop());
AnchorPane.setRightAnchor(node, offsets.getRight());
AnchorPane.setBottomAnchor(node, offsets.getBottom());
AnchorPane.setLeftAnchor(node, offsets.getLeft());
}
getChildren().addAll(nodes);
}
/**
* Places target node on the top of the pane while playing the
* swipe transition from bottom to top. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void swipeUp(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = new ParallelTransition(
moveYUpFromTopBorderToOffCanvas(topNode), // out
moveYUpFromBottomBorderToTopBorder(target) // in
);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* swipe transition from top to bottom. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void swipeDown(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = new ParallelTransition(
moveYDownFromTopBorderToBottomBorder(topNode), // out
moveYDownFromOffCanvasToTopBorder(target) // in
);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* swipe transition from right to left. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void swipeLeft(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = new ParallelTransition(
moveXLeftFromLeftBorderToOffCanvas(topNode), // out
moveXLeftFromRightBorderToLeftBorder(target) // in
);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* swipe transition from left to right. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void swipeRight(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = new ParallelTransition(
moveXRightFromLeftBorderToRightBorder(topNode), // out
moveXRightFromOffCanvasToLeftBorder(target) // in
);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* slide transition from bottom to top. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void slideUp(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = moveYUpFromBottomBorderToTopBorder(target);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* slide transition from top to bottom. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void slideDown(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = moveYDownFromOffCanvasToTopBorder(target);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* slide transition from right to left. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void slideLeft(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = moveXLeftFromRightBorderToLeftBorder(target);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
/**
* Places target node on the top of the pane while playing the
* slide transition from left to right. If the pane doesn't contain
* that node, it will be added to the end before playing transition.
*/
public void slideRight(Node target) {
var topNode = getTopNode();
if (!prepareAndCheck(topNode, target)) {
return;
}
Objects.requireNonNull(topNode);
setViewOrder(topNode, Z_ANIMATED_OUT);
setViewOrder(target, Z_ANIMATED_IN);
var transition = moveXRightFromOffCanvasToLeftBorder(target);
transition.setOnFinished(e -> onTransitionFinished(topNode, target));
setAnimationActive(true);
transition.play();
}
///////////////////////////////////////////////////////////////////////////
// Properties //
///////////////////////////////////////////////////////////////////////////
protected final ObjectProperty<Duration> animationDuration =
new SimpleObjectProperty<>(this, "animationDuration", Duration.seconds(1));
public Duration getAnimationDuration() {
return animationDuration.get();
}
/**
* Duration of the transition effect which is played when changing the top node.
*/
public ObjectProperty<Duration> animationDurationProperty() {
return animationDuration;
}
public void setAnimationDuration(@Nullable Duration animationDuration) {
this.animationDuration.set(Objects.requireNonNullElse(animationDuration, Duration.ZERO));
}
// ~
protected final ReadOnlyBooleanWrapper animationActive =
new ReadOnlyBooleanWrapper(this, "animationActive");
public boolean isAnimationActive() {
return animationActive.get();
}
/**
* Returns whether transition is in progress. Subscribe to be notified
* when animation started or finished.
*/
public ReadOnlyBooleanProperty animationActiveProperty() {
return animationActive.getReadOnlyProperty();
}
protected void setAnimationActive(boolean animationActive) {
this.animationActive.set(animationActive);
}
// ~
protected final ObjectProperty<Consumer<Node>> beforeShowCallback =
new SimpleObjectProperty<>(this, "beforeShowCallback");
public @Nullable Consumer<Node> getBeforeShowCallback() {
return beforeShowCallback.get();
}
/**
* Callback action to be called before setting a node at the top of the deck.
*/
public ObjectProperty<Consumer<Node>> beforeShowCallbackProperty() {
return beforeShowCallback;
}
public void setBeforeShowCallback(@Nullable Consumer<Node> callback) {
this.beforeShowCallback.set(callback);
}
protected void runBeforeShowCallback(Node node) {
if (getBeforeShowCallback() != null) {
getBeforeShowCallback().accept(node);
}
}
// ~
protected final ObjectProperty<Consumer<Node>> afterHideCallback =
new SimpleObjectProperty<>(this, "afterHideCallback");
public @Nullable Consumer<Node> getAfterHideCallback() {
return afterHideCallback.get();
}
/**
* Callback action to be called after removing the top node from the top of the deck.
*/
public ObjectProperty<Consumer<Node>> afterHideCallbackProperty() {
return afterHideCallback;
}
public void setAfterHideCallback(@Nullable Consumer<Node> callback) {
this.afterHideCallback.set(callback);
}
protected void runAfterHideCallback(Node node) {
if (getAfterHideCallback() != null) {
getAfterHideCallback().accept(node);
}
}
///////////////////////////////////////////////////////////////////////////
// Internal API //
///////////////////////////////////////////////////////////////////////////
/**
* Verifies that transition is possible and prepares initial conditions.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected boolean prepareAndCheck(@Nullable Node topNode, @Nullable Node target) {
if (topNode == null || target == null || target == topNode || isAnimationActive()) {
return false;
}
if (!getChildren().contains(target)) {
getChildren().add(target);
}
runBeforeShowCallback(target);
return true;
}
/**
* Cleans-up properties after transition finished.
*/
protected void onTransitionFinished(Node topNode, Node target) {
resetNode(topNode);
resetNode(target);
setViewOrder(topNode, Z_DEFAULT);
setViewOrder(target, Z_DECK_TOP);
runAfterHideCallback(topNode);
setAnimationActive(false);
}
protected Timeline moveYUpFromTopBorderToOffCanvas(Node node) {
var clip = new Rectangle();
clip.setWidth(getWidth());
node.setClip(clip);
// animated properties
clip.setHeight(getHeight());
clip.translateYProperty().set(0);
node.translateYProperty().set(0);
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.heightProperty(), 0),
new KeyValue(clip.translateYProperty(), getHeight()),
new KeyValue(node.translateYProperty(), -getHeight())
));
return timeline;
}
protected Timeline moveYUpFromBottomBorderToTopBorder(Node node) {
var clip = new Rectangle();
clip.setWidth(getWidth());
node.setClip(clip);
// animated properties
clip.setHeight(0);
node.translateYProperty().set(getHeight());
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.heightProperty(), getHeight()),
new KeyValue(node.translateYProperty(), 0)
));
return timeline;
}
protected Timeline moveYDownFromTopBorderToBottomBorder(Node node) {
var clip = new Rectangle();
clip.setWidth(getWidth());
node.setClip(clip);
// animated properties
clip.setHeight(getHeight());
node.translateYProperty().set(0);
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.heightProperty(), 0),
new KeyValue(node.translateYProperty(), getHeight())
));
return timeline;
}
protected Timeline moveYDownFromOffCanvasToTopBorder(Node node) {
var clip = new Rectangle();
clip.setWidth(getWidth());
node.setClip(clip);
// animated properties
clip.setHeight(0);
clip.translateYProperty().set(getHeight());
node.translateYProperty().set(-getHeight());
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.heightProperty(), getHeight()),
new KeyValue(clip.translateYProperty(), 0),
new KeyValue(node.translateYProperty(), 0)
));
return timeline;
}
protected Timeline moveXLeftFromLeftBorderToOffCanvas(Node node) {
var clip = new Rectangle();
clip.setHeight(getHeight());
node.setClip(clip);
// animated properties
clip.setWidth(getWidth());
clip.translateXProperty().set(0);
node.translateXProperty().set(0);
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.widthProperty(), 0),
new KeyValue(clip.translateXProperty(), getWidth()),
new KeyValue(node.translateXProperty(), -getWidth())
));
return timeline;
}
protected Timeline moveXLeftFromRightBorderToLeftBorder(Node node) {
var clip = new Rectangle();
clip.setHeight(getHeight());
node.setClip(clip);
// animated properties
clip.setWidth(0);
node.translateXProperty().set(getWidth());
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.widthProperty(), getWidth()),
new KeyValue(node.translateXProperty(), 0)
));
return timeline;
}
protected Timeline moveXRightFromLeftBorderToRightBorder(Node node) {
var clip = new Rectangle();
clip.setHeight(getHeight());
node.setClip(clip);
// animated properties
clip.setWidth(getWidth());
node.translateXProperty().set(0);
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.widthProperty(), 0),
new KeyValue(node.translateXProperty(), getWidth())
));
return timeline;
}
protected Timeline moveXRightFromOffCanvasToLeftBorder(Node node) {
var clip = new Rectangle();
clip.setHeight(getHeight());
node.setClip(clip);
// animated properties
clip.setWidth(0);
clip.translateXProperty().set(getWidth());
node.translateXProperty().set(-getWidth());
var timeline = new Timeline();
timeline.getKeyFrames().add(
new KeyFrame(
animationDuration.getValue(),
new KeyValue(clip.widthProperty(), getWidth()),
new KeyValue(clip.translateXProperty(), 0),
new KeyValue(node.translateXProperty(), 0)
));
return timeline;
}
/**
* Resets given node to the default state.
*/
protected void resetNode(Node node) {
node.setClip(null);
node.setTranslateX(0);
node.setTranslateY(0);
}
/**
* Sets the node view order. Only accept predefined values
* to avoid messing up with magical numbers.
*/
protected void setViewOrder(Node node, int viewOrder) {
if (viewOrder == Z_DEFAULT
|| viewOrder == Z_DECK_TOP
|| viewOrder == Z_ANIMATED_IN
|| viewOrder == Z_ANIMATED_OUT) {
node.setViewOrder(viewOrder);
} else {
throw new IllegalArgumentException("Unknown view order value: " + viewOrder);
}
}
}

@ -0,0 +1,218 @@
package atlantafx.base.layout;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class DeckPaneTest {
@BeforeAll
public static void startup() {
Platform.startup(() -> {
});
}
@Test
public void testTopNodeOnNewlyCreatedDeck() {
var emptyDeck = new DeckPane();
assertThat(emptyDeck.getTopNode()).isNull();
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
}
@Test
public void testSettingTopNode() {
var deck = new DeckPane(
new Rectangle(10, 10),
new Rectangle(20, 20),
new Rectangle(30, 30)
);
assertThat(deck.getTopNode()).isEqualTo(deck.getChildren().get(2));
deck.setTopNode(deck.getChildren().get(0));
assertThat(deck.getTopNode()).isEqualTo(deck.getChildren().get(0));
deck.resetTopNode();
assertThat(deck.getTopNode()).isEqualTo(deck.getChildren().get(2));
}
@Test
public void testViewOrderRestoredForRemovedNodes() {
var deck = new DeckPane(
new Rectangle(10, 10),
new Rectangle(20, 20),
new Rectangle(30, 30)
);
deck.setAnimationDuration(Duration.ZERO);
var node = deck.getChildren().get(1);
deck.swipeUp(node);
assertThat(deck.getTopNode()).isEqualTo(node);
assertThat(node.getViewOrder()).isEqualTo(DeckPane.Z_DECK_TOP);
deck.getChildren().remove(node);
assertThat(deck.getChildren()).doesNotContain(node);
assertThat(node.getViewOrder()).isEqualTo(DeckPane.Z_DEFAULT);
}
@Test
public void testSwipeUp() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.swipeUp(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.swipeUp(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.swipeUp(newNode));
}
@Test
public void testSwipeDown() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.swipeDown(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.swipeDown(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.swipeDown(newNode));
}
@Test
public void testSwipeLeft() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.swipeLeft(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.swipeLeft(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.swipeLeft(newNode));
}
@Test
public void testSwipeRight() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.swipeRight(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.swipeRight(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.swipeRight(newNode));
}
@Test
public void testSlideUp() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.slideUp(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.slideUp(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.slideUp(newNode));
}
@Test
public void testSlideDown() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.slideDown(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.slideDown(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.slideDown(newNode));
}
@Test
public void testSlideLeft() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.slideLeft(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.slideLeft(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.slideLeft(newNode));
}
@Test
public void testSlideRight() {
var deck = new TestDeck();
assertThat(deck.pane.getTopNode()).isEqualTo(deck.r1);
deck.runAndAssert(deck.r2, pane -> pane.slideRight(deck.r2));
deck.runAndAssert(deck.r3, pane -> pane.slideRight(deck.r3));
var newNode = new Rectangle(40, 40);
deck.runAndAssert(newNode, pane -> pane.slideRight(newNode));
}
///////////////////////////////////////////////////////////////////////////
public static class TestDeck {
public final DeckPane pane;
public final Rectangle r1;
public final Rectangle r2;
public final Rectangle r3;
public int showCounter;
public int hideCounter;
public TestDeck() {
// size works as node identifier
r1 = new Rectangle(10, 10);
r2 = new Rectangle(20, 20);
r3 = new Rectangle(30, 30);
pane = new DeckPane(r3, r2, r1);
// to not make a pause between tests
pane.setAnimationDuration(Duration.ZERO);
pane.setBeforeShowCallback(node -> showCounter++);
pane.setAfterHideCallback(node -> hideCounter++);
}
public void runAndAssert(Node node, Consumer<DeckPane> action) {
var hideCounterBefore = hideCounter;
var showCounterBefore = showCounter;
action.accept(pane);
assertThat(isFresh()).isTrue();
assertThat(pane.getTopNode()).isEqualTo(node);
assertThat(hideCounter).isEqualTo(++hideCounterBefore);
assertThat(showCounter).isEqualTo(++showCounterBefore);
assertThat(node.getViewOrder()).isEqualTo(DeckPane.Z_DECK_TOP);
pane.getChildren().stream()
.filter(child -> child != node)
.forEach(child -> assertThat(child.getViewOrder()).isEqualTo(DeckPane.Z_DEFAULT));
}
/**
* See {@link #isNodeFresh}.
*/
public boolean isFresh() {
return isNodeFresh(r1) && isNodeFresh(r2) && isNodeFresh(r3);
}
/**
* Checks that all node properties involved into animation
* have been reset to its default state.
*/
public boolean isNodeFresh(Node node) {
return node.getClip() == null && node.getTranslateX() == 0 && node.getTranslateY() == 0;
}
}
}