1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.animation;
  27 
  28 import com.sun.javafx.animation.TickCalculation;
  29 import static com.sun.javafx.animation.TickCalculation.*;
  30 
  31 import java.util.Arrays;
  32 
  33 import javafx.beans.InvalidationListener;
  34 import javafx.beans.Observable;
  35 import javafx.beans.property.ObjectProperty;
  36 import javafx.beans.property.SimpleObjectProperty;
  37 import javafx.collections.ListChangeListener.Change;
  38 import javafx.collections.ObservableList;
  39 import javafx.event.ActionEvent;
  40 import javafx.event.EventHandler;
  41 import javafx.scene.Node;
  42 import javafx.util.Duration;
  43 
  44 import com.sun.javafx.collections.TrackableObservableList;
  45 import com.sun.javafx.collections.VetoableListDecorator;
  46 import com.sun.scenario.animation.AbstractMasterTimer;
  47 import java.util.HashSet;
  48 import java.util.List;
  49 import java.util.Set;
  50 import javafx.beans.value.ChangeListener;
  51 import javafx.beans.value.ObservableValue;
  52 
  53 /**
  54  * This {@link Transition} plays a list of {@link javafx.animation.Animation
  55  * Animations} in sequential order.
  56  * <p>
  57  * Children of this {@code Transition} inherit {@link #nodeProperty() node}, if their
  58  * {@code node} property is not specified.
  59  *
  60  * <p>
  61  * Code Segment Example:
  62  * </p>
  63  *
  64  * <pre>
  65  * <code>
  66  *     Rectangle rect = new Rectangle (100, 40, 100, 100);
  67  *     rect.setArcHeight(50);
  68  *     rect.setArcWidth(50);
  69  *     rect.setFill(Color.VIOLET);
  70  *
  71  *     final Duration SEC_2 = Duration.millis(2000);
  72  *     final Duration SEC_3 = Duration.millis(3000);
  73  *
  74  *     PauseTransition pt = new PauseTransition(Duration.millis(1000));
  75  *     FadeTransition ft = new FadeTransition(SEC_3);
  76  *     ft.setFromValue(1.0f);
  77  *     ft.setToValue(0.3f);
  78  *     ft.setCycleCount(2f);
  79  *     ft.setAutoReverse(true);
  80  *     TranslateTransition tt = new TranslateTransition(SEC_2);
  81  *     tt.setFromX(-100f);
  82  *     tt.setToX(100f);
  83  *     tt.setCycleCount(2f);
  84  *     tt.setAutoReverse(true);
  85  *     RotateTransition rt = new RotateTransition(SEC_3);
  86  *     rt.setByAngle(180f);
  87  *     rt.setCycleCount(4f);
  88  *     rt.setAutoReverse(true);
  89  *     ScaleTransition st = new ScaleTransition(SEC_2);
  90  *     st.setByX(1.5f);
  91  *     st.setByY(1.5f);
  92  *     st.setCycleCount(2f);
  93  *     st.setAutoReverse(true);
  94  *
  95  *     SequentialTransition seqT = new SequentialTransition (rect, pt, ft, tt, rt, st);
  96  *     seqT.play();
  97  * </code>
  98  * </pre>
  99  *
 100  * @see Transition
 101  * @see Animation
 102  *
 103  * @since JavaFX 2.0
 104  */
 105 public final class SequentialTransition extends Transition {
 106 
 107     private static final Animation[] EMPTY_ANIMATION_ARRAY = new Animation[0];
 108     private static final int BEFORE = -1;
 109     private static final double EPSILON = 1e-12;
 110 
 111     private Animation[] cachedChildren = EMPTY_ANIMATION_ARRAY;
 112     private long[] startTimes;
 113     private long[] durations;
 114     private long[] delays;
 115     private double[] rates;
 116     private boolean[] forceChildSync;
 117     private int end;
 118     private int curIndex = BEFORE;
 119     private long oldTicks = 0L;
 120     private long offsetTicks;
 121     private boolean childrenChanged = true;
 122     private boolean toggledRate;
 123 
 124     private final InvalidationListener childrenListener = observable -> {
 125         childrenChanged = true;
 126         if (getStatus() == Status.STOPPED) {
 127             setCycleDuration(computeCycleDuration());
 128         }
 129     };
 130 
 131     private final ChangeListener<Number> rateListener = new ChangeListener<Number>() {
 132 
 133         @Override
 134         public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
 135             if (oldValue.doubleValue() * newValue.doubleValue() < 0) {
 136                 for (int i = 0; i < cachedChildren.length; ++i) {
 137                     Animation child = cachedChildren[i];
 138                     child.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate()));
 139                 }
 140                 toggledRate = true;
 141             }
 142         }
 143 
 144     };
 145 
 146     /**
 147      * This {@link javafx.scene.Node} is used in all child {@link Transition
 148      * Transitions}, that do not define a target {@code Node} themselves. This
 149      * can be used if a number of {@code Transitions} should be applied to a
 150      * single {@code Node}.
 151      * <p>
 152      * It is not possible to change the target {@code node} of a running
 153      * {@code Transition}. If the value of {@code node} is changed for a
 154      * running {@code Transition}, the animation has to be stopped and started again to
 155      * pick up the new value.
 156      */
 157     private ObjectProperty<Node> node;
 158     private static final Node DEFAULT_NODE = null;
 159 
 160     public final void setNode(Node value) {
 161         if ((node != null) || (value != null /* DEFAULT_NODE */)) {
 162             nodeProperty().set(value);
 163         }
 164     }
 165 
 166     public final Node getNode() {
 167         return (node == null)? DEFAULT_NODE : node.get();
 168     }
 169 
 170     public final ObjectProperty<Node> nodeProperty() {
 171         if (node == null) {
 172             node = new SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE);
 173         }
 174         return node;
 175     }
 176 
 177     private final Set<Animation> childrenSet = new HashSet<Animation>();
 178 
 179     private final ObservableList<Animation> children = new VetoableListDecorator<Animation>(new TrackableObservableList<Animation>() {
 180         @Override
 181         protected void onChanged(Change<Animation> c) {
 182             while (c.next()) {
 183                 for (final Animation animation : c.getRemoved()) {
 184                     animation.parent = null;
 185                     animation.rateProperty().removeListener(childrenListener);
 186                     animation.totalDurationProperty().removeListener(childrenListener);
 187                     animation.delayProperty().removeListener(childrenListener);
 188                 }
 189                 for (final Animation animation : c.getAddedSubList()) {
 190                     animation.parent = SequentialTransition.this;
 191                     animation.rateProperty().addListener(childrenListener);
 192                     animation.totalDurationProperty().addListener(childrenListener);
 193                     animation.delayProperty().addListener(childrenListener);
 194                 }
 195             }
 196             childrenListener.invalidated(children);
 197         }
 198     }) {
 199 
 200         @Override
 201         protected void onProposedChange(List<Animation> toBeAdded, int... indexes) {
 202             IllegalArgumentException exception = null;
 203             for (int i = 0; i < indexes.length; i+=2) {
 204                 for (int idx = indexes[i]; idx < indexes[i+1]; ++idx) {
 205                     childrenSet.remove(children.get(idx));
 206                 }
 207             }
 208             for (Animation child : toBeAdded) {
 209                 if (child == null) {
 210                     exception = new IllegalArgumentException("Child cannot be null");
 211                     break;
 212                 }
 213                 if (!childrenSet.add(child)) {
 214                     exception = new IllegalArgumentException("Attempting to add a duplicate to the list of children");
 215                     break;
 216                 }
 217                 if (checkCycle(child, SequentialTransition.this)) {
 218                     exception = new IllegalArgumentException("This change would create cycle");
 219                     break;
 220                 }
 221             }
 222 
 223             if (exception != null) {
 224                 childrenSet.clear();
 225                 childrenSet.addAll(children);
 226                 throw exception;
 227             }
 228         }
 229 
 230     };
 231 
 232     private static boolean checkCycle(Animation child, Animation parent) {
 233         Animation a = parent;
 234         while (a != child) {
 235             if (a.parent != null) {
 236                 a = a.parent;
 237             } else {
 238                 return false;
 239             }
 240         }
 241         return true;
 242     }
 243 
 244     /**
 245      * A list of {@link javafx.animation.Animation Animations} that will be
 246      * played sequentially.
 247      * <p>
 248      * It is not possible to change the children of a running
 249      * {@code SequentialTransition}. If the children are changed for a running
 250      * {@code SequentialTransition}, the animation has to be stopped and started
 251      * again to pick up the new value.
 252      */
 253     public final ObservableList<Animation> getChildren() {
 254         return children;
 255     }
 256 
 257     /**
 258      * The constructor of {@code SequentialTransition}.
 259      *
 260      * @param node
 261      *            The target {@link javafx.scene.Node} to be used in child
 262      *            {@link Transition Transitions} that have no {@code Node} specified
 263      *            themselves
 264      * @param children
 265      *            The child {@link javafx.animation.Animation Animations} of
 266      *            this {@code SequentialTransition}
 267      */
 268     public SequentialTransition(Node node, Animation... children) {
 269         setInterpolator(Interpolator.LINEAR);
 270         setNode(node);
 271         getChildren().setAll(children);
 272     }
 273 
 274     /**
 275      * The constructor of {@code SequentialTransition}.
 276      *
 277      * @param children
 278      *            The child {@link javafx.animation.Animation Animations} of
 279      *            this {@code SequentialTransition}
 280      */
 281     public SequentialTransition(Animation... children) {
 282         this(null, children);
 283     }
 284 
 285     /**
 286      * The constructor of {@code SequentialTransition}.
 287      *
 288      * @param node
 289      *            The target {@link javafx.scene.Node} to be used in child
 290      *            {@link Transition Transitions} that have no {@code Node} specified
 291      *            themselves
 292      */
 293     public SequentialTransition(Node node) {
 294         setInterpolator(Interpolator.LINEAR);
 295         setNode(node);
 296     }
 297 
 298     /**
 299      * The constructor of {@code SequentialTransition}.
 300      */
 301     public SequentialTransition() {
 302         this((Node) null);
 303     }
 304 
 305     // For testing purposes
 306     SequentialTransition(AbstractMasterTimer timer) {
 307         super(timer);
 308         setInterpolator(Interpolator.LINEAR);
 309     }
 310 
 311     /**
 312      * {@inheritDoc}
 313      */
 314     @Override
 315     protected Node getParentTargetNode() {
 316         final Node _node = getNode();
 317         return (_node != null) ? _node : ((parent != null && parent instanceof Transition) ?
 318                 ((Transition)parent).getParentTargetNode() : null);
 319     }
 320 
 321     private Duration computeCycleDuration() {
 322         Duration currentDur = Duration.ZERO;
 323 
 324         for (final Animation animation : getChildren()) {
 325             currentDur = currentDur.add(animation.getDelay());
 326             final double absRate = Math.abs(animation.getRate());
 327             currentDur = currentDur.add((absRate < EPSILON) ?
 328                     animation.getTotalDuration() : animation.getTotalDuration().divide(absRate));
 329             if (currentDur.isIndefinite()) {
 330                 break;
 331             }
 332         }
 333         return currentDur;
 334     }
 335 
 336     private double calculateFraction(long currentTicks, long cycleTicks) {
 337         final double frac = (double) currentTicks / cycleTicks;
 338         return (frac <= 0.0) ? 0 : (frac >= 1.0) ? 1.0 : frac;
 339     }
 340 
 341     private int findNewIndex(long ticks) {
 342         if ((curIndex != BEFORE)
 343                 && (curIndex != end)
 344                 && (startTimes[curIndex] <= ticks)
 345                 && (ticks <= startTimes[curIndex + 1])) {
 346             return curIndex;
 347         }
 348 
 349         final boolean indexUndefined = (curIndex == BEFORE) || (curIndex == end);
 350         final int fromIndex = (indexUndefined || (ticks < oldTicks)) ? 0 : curIndex + 1;
 351         final int toIndex = (indexUndefined || (oldTicks < ticks)) ? end : curIndex;
 352         final int index = Arrays.binarySearch(startTimes, fromIndex, toIndex, ticks);
 353         return (index < 0) ? -index - 2 : (index > 0) ? index - 1 : 0;
 354     }
 355 
 356     @Override
 357     void impl_sync(boolean forceSync) {
 358         super.impl_sync(forceSync);
 359 
 360         if ((forceSync && childrenChanged) || (startTimes == null)) {
 361             cachedChildren = getChildren().toArray(EMPTY_ANIMATION_ARRAY);
 362             end = cachedChildren.length;
 363             startTimes = new long[end + 1];
 364             durations = new long[end];
 365             delays = new long[end];
 366             rates = new double[end];
 367             forceChildSync = new boolean[end];
 368             long cycleTicks = 0L;
 369             int i = 0;
 370             for (final Animation animation : cachedChildren) {
 371                 startTimes[i] = cycleTicks;
 372                 rates[i] = Math.abs(animation.getRate());
 373                 if (rates[i] < EPSILON) {
 374                     rates[i] = 1;
 375                 }
 376                 durations[i] = fromDuration(animation.getTotalDuration(), rates[i]);
 377                 delays[i] = fromDuration(animation.getDelay());
 378                 if ((durations[i] == Long.MAX_VALUE) || (delays[i] == Long.MAX_VALUE) || (cycleTicks == Long.MAX_VALUE)) {
 379                     cycleTicks = Long.MAX_VALUE;
 380                 } else {
 381                     cycleTicks = add(cycleTicks, add(durations[i], delays[i]));
 382                 }
 383                 forceChildSync[i] = true;
 384                 i++;
 385             }
 386             startTimes[end] = cycleTicks;
 387             childrenChanged = false;
 388         } else if (forceSync) {
 389             final int n = forceChildSync.length;
 390             for (int i=0; i<n; i++) {
 391                 forceChildSync[i] = true;
 392             }
 393         }
 394     }
 395 
 396     @Override
 397     void impl_start(boolean forceSync) {
 398         super.impl_start(forceSync);
 399         toggledRate = false;
 400         rateProperty().addListener(rateListener);
 401         offsetTicks = 0L;
 402         double curRate = getCurrentRate();
 403         final long currentTicks = TickCalculation.fromDuration(getCurrentTime());
 404         if (curRate < 0) {
 405             jumpToEnd();
 406             curIndex = end;
 407             if (currentTicks < startTimes[end]) {
 408                 impl_jumpTo(currentTicks, startTimes[end], false);
 409             }
 410         } else {
 411             jumpToBefore();
 412             curIndex = BEFORE;
 413             if (currentTicks > 0) {
 414                 impl_jumpTo(currentTicks, startTimes[end], false);
 415             }
 416         }
 417     }
 418 
 419     @Override
 420     void impl_pause() {
 421         super.impl_pause();
 422         if ((curIndex != BEFORE) && (curIndex != end)) {
 423             final Animation current = cachedChildren[curIndex];
 424             if (current.getStatus() == Status.RUNNING) {
 425                 current.impl_pause();
 426             }
 427         }
 428     }
 429 
 430     @Override
 431     void impl_resume() {
 432         super.impl_resume();
 433         if ((curIndex != BEFORE) && (curIndex != end)) {
 434             final Animation current = cachedChildren[curIndex];
 435             if (current.getStatus() == Status.PAUSED) {
 436                 current.impl_resume();
 437                 current.clipEnvelope.setRate(rates[curIndex] * Math.signum(getCurrentRate()));
 438             }
 439         }
 440     }
 441 
 442     @Override
 443     void impl_stop() {
 444         super.impl_stop();
 445         if ((curIndex != BEFORE) && (curIndex != end)) {
 446             final Animation current = cachedChildren[curIndex];
 447             if (current.getStatus() != Status.STOPPED) {
 448                 current.impl_stop();
 449             }
 450         }
 451         if (childrenChanged) {
 452             setCycleDuration(computeCycleDuration());
 453         }
 454         rateProperty().removeListener(rateListener);
 455     }
 456 
 457     private boolean startChild(Animation child, int index) {
 458         final boolean forceSync = forceChildSync[index];
 459         if (child.impl_startable(forceSync)) {
 460             child.clipEnvelope.setRate(rates[index] * Math.signum(getCurrentRate()));
 461             child.impl_start(forceSync);
 462             forceChildSync[index] = false;
 463             return true;
 464         }
 465         return false;
 466     }
 467 
 468     @Override void impl_playTo(long currentTicks, long cycleTicks) {
 469         impl_setCurrentTicks(currentTicks);
 470         final double frac = calculateFraction(currentTicks, cycleTicks);
 471         final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
 472         final int newIndex = findNewIndex(newTicks);
 473         final Animation current = ((curIndex == BEFORE) || (curIndex == end)) ? null : cachedChildren[curIndex];
 474         if (toggledRate) {
 475             if (current != null && current.getStatus() == Status.RUNNING) {
 476                 offsetTicks -= Math.signum(getCurrentRate()) * (durations[curIndex] - 2 * (oldTicks - delays[curIndex] - startTimes[curIndex]));
 477             }
 478             toggledRate = false;
 479         }
 480         if (curIndex == newIndex) {
 481             if (getCurrentRate() > 0) {
 482                 final long currentDelay = add(startTimes[curIndex], delays[curIndex]);
 483                 if (newTicks >= currentDelay) {
 484                     if ((oldTicks <= currentDelay) || (current.getStatus() == Status.STOPPED)) {
 485                         final boolean enteringCycle = oldTicks <= currentDelay;
 486                         if (enteringCycle) {
 487                             current.clipEnvelope.jumpTo(0);
 488                         }
 489                         if (!startChild(current, curIndex)) {
 490                             if (enteringCycle) {
 491                                 final EventHandler<ActionEvent> handler = current.getOnFinished();
 492                                 if (handler != null) {
 493                                     handler.handle(new ActionEvent(this, null));
 494                                 }
 495                             }
 496                             oldTicks = newTicks;
 497                             return;
 498                         }
 499                     }
 500                     if (newTicks >= startTimes[curIndex+1]) {
 501                         current.impl_timePulse(sub(durations[curIndex], offsetTicks));
 502                         if (newTicks == cycleTicks) {
 503                             curIndex = end;
 504                         }
 505                     } else {
 506                         final long localTicks = sub(newTicks - currentDelay, offsetTicks);
 507                         current.impl_timePulse(localTicks);
 508                     }
 509                 }
 510             } else { // getCurrentRate() < 0
 511                 final long currentDelay = add(startTimes[curIndex], delays[curIndex]);
 512                 if ((oldTicks >= startTimes[curIndex+1]) || ((oldTicks >= currentDelay) && (current.getStatus() == Status.STOPPED))){
 513                     final boolean enteringCycle = oldTicks >= startTimes[curIndex+1];
 514                     if (enteringCycle) {
 515                         current.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
 516                     }
 517                     if (!startChild(current, curIndex)) {
 518                         if (enteringCycle) {
 519                             final EventHandler<ActionEvent> handler = current.getOnFinished();
 520                             if (handler != null) {
 521                                 handler.handle(new ActionEvent(this, null));
 522                             }
 523                         }
 524                         oldTicks = newTicks;
 525                         return;
 526                     }
 527                 }
 528                 if (newTicks <= currentDelay) {
 529                     current.impl_timePulse(sub(durations[curIndex], offsetTicks));
 530                     if (newTicks == 0) {
 531                         curIndex = BEFORE;
 532                     }
 533                 } else {
 534                     final long localTicks = sub(startTimes[curIndex + 1] - newTicks, offsetTicks);
 535                     current.impl_timePulse(localTicks);
 536                 }
 537             }
 538         } else { // curIndex != newIndex
 539             if (curIndex < newIndex) {
 540                 if (current != null) {
 541                     final long oldDelay = add(startTimes[curIndex], delays[curIndex]);
 542                     if ((oldTicks <= oldDelay) || ((current.getStatus() == Status.STOPPED) && (oldTicks != startTimes[curIndex + 1]))) {
 543                         final boolean enteringCycle = oldTicks <= oldDelay;
 544                         if (enteringCycle) {
 545                             current.clipEnvelope.jumpTo(0);
 546                         }
 547                         if (!startChild(current, curIndex)) {
 548                             if (enteringCycle) {
 549                                 final EventHandler<ActionEvent> handler = current.getOnFinished();
 550                                 if (handler != null) {
 551                                     handler.handle(new ActionEvent(this, null));
 552                                 }
 553                             }
 554                         }
 555                     }
 556                     if (current.getStatus() == Status.RUNNING) {
 557                         current.impl_timePulse(sub(durations[curIndex], offsetTicks));
 558                     }
 559                     oldTicks = startTimes[curIndex + 1];
 560                 }
 561                 offsetTicks = 0;
 562                 curIndex++;
 563                 for (; curIndex < newIndex; curIndex++) {
 564                     final Animation animation = cachedChildren[curIndex];
 565                     animation.clipEnvelope.jumpTo(0);
 566                     if (startChild(animation, curIndex)) {
 567                         animation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
 568                     } else {
 569                         final EventHandler<ActionEvent> handler = animation.getOnFinished();
 570                         if (handler != null) {
 571                             handler.handle(new ActionEvent(this, null));
 572                         }
 573                     }
 574                     oldTicks = startTimes[curIndex + 1];
 575                 }
 576                 final Animation newAnimation = cachedChildren[curIndex];
 577                 newAnimation.clipEnvelope.jumpTo(0);
 578                 if (startChild(newAnimation, curIndex)) {
 579                     if (newTicks >= startTimes[curIndex+1]) {
 580                         newAnimation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
 581                         if (newTicks == cycleTicks) {
 582                             curIndex = end;
 583                         }
 584                     } else {
 585                         final long localTicks = sub(newTicks, add(startTimes[curIndex], delays[curIndex]));
 586                         newAnimation.impl_timePulse(localTicks);
 587                     }
 588                 } else {
 589                     final EventHandler<ActionEvent> handler = newAnimation.getOnFinished();
 590                     if (handler != null) {
 591                         handler.handle(new ActionEvent(this, null));
 592                     }
 593                 }
 594             } else {
 595                 if (current != null) {
 596                     final long oldDelay = add(startTimes[curIndex], delays[curIndex]);
 597                     if ((oldTicks >= startTimes[curIndex+1]) || ((oldTicks > oldDelay) && (current.getStatus() == Status.STOPPED))){
 598                         final boolean enteringCycle = oldTicks >= startTimes[curIndex+1];
 599                         if (enteringCycle) {
 600                             current.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
 601                         }
 602                         if (!startChild(current, curIndex)) {
 603                             if (enteringCycle) {
 604                                 final EventHandler<ActionEvent> handler = current.getOnFinished();
 605                                 if (handler != null) {
 606                                     handler.handle(new ActionEvent(this, null));
 607                                 }
 608                             }
 609                         }
 610                     }
 611                     if (current.getStatus() == Status.RUNNING) {
 612                         current.impl_timePulse(sub(durations[curIndex], offsetTicks));
 613                     }
 614                     oldTicks = startTimes[curIndex];
 615                 }
 616                 offsetTicks = 0;
 617                 curIndex--;
 618                 for (; curIndex > newIndex; curIndex--) {
 619                     final Animation animation = cachedChildren[curIndex];
 620                     animation.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
 621                     if (startChild(animation, curIndex)) {
 622                         animation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
 623                     } else {
 624                         final EventHandler<ActionEvent> handler = animation.getOnFinished();
 625                         if (handler != null) {
 626                             handler.handle(new ActionEvent(this, null));
 627                         }
 628                     }
 629                     oldTicks = startTimes[curIndex];
 630                 }
 631                 final Animation newAnimation = cachedChildren[curIndex];
 632                 newAnimation.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
 633                 if (startChild(newAnimation, curIndex)) {
 634                     if (newTicks <= add(startTimes[curIndex], delays[curIndex])) {
 635                         newAnimation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
 636                         if (newTicks == 0) {
 637                             curIndex = BEFORE;
 638                         }
 639                     } else {
 640                         final long localTicks = sub(startTimes[curIndex + 1], newTicks);
 641                         newAnimation.impl_timePulse(localTicks);
 642                     }
 643                 } else {
 644                     final EventHandler<ActionEvent> handler = newAnimation.getOnFinished();
 645                     if (handler != null) {
 646                         handler.handle(new ActionEvent(this, null));
 647                     }
 648                 }
 649             }
 650         }
 651         oldTicks = newTicks;
 652     }
 653 
 654     @Override void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump) {
 655         impl_setCurrentTicks(currentTicks);
 656         final Status status = getStatus();
 657 
 658         if (status == Status.STOPPED && !forceJump) {
 659             return;
 660         }
 661 
 662         impl_sync(false);
 663         final double frac = calculateFraction(currentTicks, cycleTicks);
 664         final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
 665         final int oldIndex = curIndex;
 666         curIndex = findNewIndex(newTicks);
 667         final Animation newAnimation = cachedChildren[curIndex];
 668         final double currentRate = getCurrentRate();
 669         final long currentDelay = add(startTimes[curIndex], delays[curIndex]);
 670         if (curIndex != oldIndex) {
 671             if (status != Status.STOPPED) {
 672                 if ((oldIndex != BEFORE) && (oldIndex != end)) {
 673                     final Animation oldChild = cachedChildren[oldIndex];
 674                     if (oldChild.getStatus() != Status.STOPPED) {
 675                         cachedChildren[oldIndex].impl_stop();
 676                     }
 677                 }
 678                 if (curIndex < oldIndex) {
 679                     for (int i = oldIndex == end ? end - 1 : oldIndex; i > curIndex; --i) {
 680                         cachedChildren[i].impl_jumpTo(0, durations[i], true);
 681                     }
 682                 } else { //curIndex > oldIndex as curIndex != oldIndex
 683                     for (int i = oldIndex == BEFORE? 0 : oldIndex; i < curIndex; ++i) {
 684                         cachedChildren[i].impl_jumpTo(durations[i], durations[i], true);
 685                     }
 686                 }
 687                 if (newTicks >= currentDelay) {
 688                     startChild(newAnimation, curIndex);
 689                     if (status == Status.PAUSED) {
 690                         newAnimation.impl_pause();
 691                     }
 692                 }
 693             }
 694         }
 695         if (oldIndex == curIndex) {
 696             if (currentRate == 0) {
 697                 offsetTicks += (newTicks - oldTicks) * Math.signum(this.clipEnvelope.getCurrentRate());
 698             } else {
 699                 offsetTicks += currentRate > 0 ? newTicks - oldTicks : oldTicks - newTicks;
 700             }
 701         } else {
 702             if (currentRate == 0) {
 703                 if (this.clipEnvelope.getCurrentRate() > 0) {
 704                     offsetTicks = Math.max(0, newTicks - currentDelay);
 705                 } else {
 706                     offsetTicks = startTimes[curIndex] + durations[curIndex] - newTicks;
 707                 }
 708             } else {
 709                 offsetTicks = currentRate > 0 ? Math.max(0, newTicks - currentDelay) : startTimes[curIndex + 1] - newTicks;
 710             }
 711         }
 712         newAnimation.clipEnvelope.jumpTo(Math.round(sub(newTicks, currentDelay) * rates[curIndex]));
 713         oldTicks = newTicks;
 714     }
 715 
 716     private void jumpToEnd() {
 717         for (int i = 0 ; i < end; ++i) {
 718             if (forceChildSync[i]) {
 719                 cachedChildren[i].impl_sync(true);
 720                 //NOTE: do not clean up forceChildSync[i] here. Another sync will be needed during the play
 721                 // The reason is we have 2 different use-cases for jumping (1)play from start, (2)play next cycle.
 722                 // and 2 different types of sub-transitions (A)"by" transitions that need to synchronize on
 723                 // the current state and move property by certain value and (B)"from-to" transitions that
 724                 // move from one point to another on each play/cycle. We can't query if transition is A or B.
 725                 //
 726                 // Now for combination 1A we need to synchronize here, as the subsequent jump would move
 727                 // the property to the previous value. 1B doesn't need to sync here, but it's not unsafe to
 728                 // do it. As forceChildSync is set only in case (1) and not in case (2), the cycles are always equal.
 729                 //
 730                 // Now the reason why we cannot clean forceChildSync[i] here is that while we need to sync here,
 731                 // there might be children of (A)-"by" type that operate on the same property, but fail to synchronize
 732                 // them when they start would mean they all would have the same value at the beginning.
 733             }
 734             cachedChildren[i].impl_jumpTo(durations[i], durations[i], true);
 735 
 736         }
 737     }
 738 
 739     private void jumpToBefore() {
 740         for (int i = end - 1 ; i >= 0; --i) {
 741             if (forceChildSync[i]) {
 742                 cachedChildren[i].impl_sync(true);
 743                 //NOTE: do not clean up forceChildSync[i] here. Another sync will be needed during the play
 744                 // See explanation in jumpToEnd
 745             }
 746             cachedChildren[i].impl_jumpTo(0, durations[i], true);
 747         }
 748     }
 749 
 750     /**
 751      * {@inheritDoc}
 752      */
 753     @Override
 754     protected void interpolate(double frac) {
 755         // no-op
 756     }
 757 
 758 }