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 }