1 /* 2 * Copyright (c) 2010, 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 javafx.beans.InvalidationListener; 32 import javafx.beans.Observable; 33 import javafx.beans.property.ObjectProperty; 34 import javafx.collections.ListChangeListener.Change; 35 import javafx.collections.ObservableList; 36 import javafx.event.ActionEvent; 37 import javafx.event.EventHandler; 38 import javafx.scene.Node; 39 import javafx.util.Duration; 40 41 import com.sun.javafx.collections.TrackableObservableList; 42 import com.sun.javafx.collections.VetoableListDecorator; 43 import com.sun.scenario.animation.AbstractMasterTimer; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Set; 47 import javafx.beans.value.ChangeListener; 48 import javafx.beans.value.ObservableValue; 49 50 /** 51 * This {@link Transition} plays a list of {@link javafx.animation.Animation 52 * Animations} in parallel. 53 * <p> 54 * Children of this {@code Transition} inherit {@link #nodeProperty() node}, if their 55 * {@code node} property is not specified. 56 * 57 * <p> 58 * Code Segment Example: 59 * </p> 60 * 61 * <pre> 62 * <code> 63 * Rectangle rect = new Rectangle (100, 40, 100, 100); 64 * rect.setArcHeight(50); 65 * rect.setArcWidth(50); 66 * rect.setFill(Color.VIOLET); 67 * 68 * final Duration SEC_2 = Duration.millis(2000); 69 * final Duration SEC_3 = Duration.millis(3000); 70 * 71 * FadeTransition ft = new FadeTransition(SEC_3); 72 * ft.setFromValue(1.0f); 73 * ft.setToValue(0.3f); 74 * ft.setCycleCount(2f); 75 * ft.setAutoReverse(true); 76 * TranslateTransition tt = new TranslateTransition(SEC_2); 77 * tt.setFromX(-100f); 78 * tt.setToX(100f); 79 * tt.setCycleCount(2f); 80 * tt.setAutoReverse(true); 81 * RotateTransition rt = new RotateTransition(SEC_3); 82 * rt.setByAngle(180f); 83 * rt.setCycleCount(4f); 84 * rt.setAutoReverse(true); 85 * ScaleTransition st = new ScaleTransition(SEC_2); 86 * st.setByX(1.5f); 87 * st.setByY(1.5f); 88 * st.setCycleCount(2f); 89 * st.setAutoReverse(true); 90 * 91 * ParallelTransition pt = new ParallelTransition(rect, ft, tt, rt, st); 92 * pt.play(); 93 * </code> 94 * </pre> 95 * 96 * @see Transition 97 * @see Animation 98 * 99 * @since JavaFX 2.0 100 */ 101 public final class ParallelTransition extends Transition { 102 103 private static final Animation[] EMPTY_ANIMATION_ARRAY = new Animation[0]; 104 private static final double EPSILON = 1e-12; 105 106 private Animation[] cachedChildren = EMPTY_ANIMATION_ARRAY; 107 private long[] durations; 108 private long[] delays; 109 private double[] rates; 110 private long[] offsetTicks; 111 private boolean[] forceChildSync; 112 private long oldTicks; 113 private long cycleTime; 114 private boolean childrenChanged = true; 115 private boolean toggledRate; 116 117 private final InvalidationListener childrenListener = observable -> { 118 childrenChanged = true; 119 if (getStatus() == Status.STOPPED) { 120 setCycleDuration(computeCycleDuration()); 121 } 122 }; 123 124 private final ChangeListener<Number> rateListener = new ChangeListener<Number>() { 125 126 @Override 127 public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { 128 if (oldValue.doubleValue() * newValue.doubleValue() < 0) { 129 for (int i = 0; i < cachedChildren.length; ++i) { 130 Animation child = cachedChildren[i]; 131 child.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate())); 132 } 133 toggledRate = true; 134 } 135 } 136 137 }; 138 /** 139 * This {@link javafx.scene.Node} is used in all child {@link Transition 140 * Transitions}, that do not define a target {@code Node} themselves. This 141 * can be used if a number of {@code Transitions} should be applied to a 142 * single {@code Node}. 143 * <p> 144 * It is not possible to change the target {@code node} of a running 145 * {@code Transition}. If the value of {@code node} is changed for a running 146 * {@code Transition}, the animation has to be stopped and started again to 147 * pick up the new value. 148 */ 149 private ObjectProperty<Node> node; 150 private static final Node DEFAULT_NODE = null; 151 152 public final void setNode(Node value) { 153 if ((node != null) || (value != null /* DEFAULT_NODE */)) { 154 nodeProperty().set(value); 155 } 156 } 157 158 public final Node getNode() { 159 return (node == null)? DEFAULT_NODE : node.get(); 160 } 161 162 public final ObjectProperty<Node> nodeProperty() { 163 if (node == null) { 164 node = new javafx.beans.property.SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE); 165 } 166 return node; 167 } 168 169 private final Set<Animation> childrenSet = new HashSet<Animation>(); 170 171 private final ObservableList<Animation> children = new VetoableListDecorator<Animation>(new TrackableObservableList<Animation>() { 172 @Override 173 protected void onChanged(Change<Animation> c) { 174 while (c.next()) { 175 for (final Animation animation : c.getRemoved()) { 176 animation.parent = null; 177 animation.rateProperty().removeListener(childrenListener); 178 animation.totalDurationProperty().removeListener(childrenListener); 179 animation.delayProperty().removeListener(childrenListener); 180 } 181 for (final Animation animation : c.getAddedSubList()) { 182 animation.parent = ParallelTransition.this; 183 animation.rateProperty().addListener(childrenListener); 184 animation.totalDurationProperty().addListener(childrenListener); 185 animation.delayProperty().addListener(childrenListener); 186 } 187 } 188 childrenListener.invalidated(children); 189 } 190 }) { 191 192 @Override 193 protected void onProposedChange(List<Animation> toBeAdded, int... indexes) { 194 IllegalArgumentException exception = null; 195 for (int i = 0; i < indexes.length; i+=2) { 196 for (int idx = indexes[i]; idx < indexes[i+1]; ++idx) { 197 childrenSet.remove(children.get(idx)); 198 } 199 } 200 for (Animation child : toBeAdded) { 201 if (child == null) { 202 exception = new IllegalArgumentException("Child cannot be null"); 203 break; 204 } 205 if (!childrenSet.add(child)) { 206 exception = new IllegalArgumentException("Attempting to add a duplicate to the list of children"); 207 break; 208 } 209 if (checkCycle(child, ParallelTransition.this)) { 210 exception = new IllegalArgumentException("This change would create cycle"); 211 break; 212 } 213 } 214 215 if (exception != null) { 216 childrenSet.clear(); 217 childrenSet.addAll(children); 218 throw exception; 219 } 220 } 221 222 }; 223 224 private static boolean checkCycle(Animation child, Animation parent) { 225 Animation a = parent; 226 while (a != child) { 227 if (a.parent != null) { 228 a = a.parent; 229 } else { 230 return false; 231 } 232 } 233 return true; 234 } 235 236 /** 237 * A list of {@link javafx.animation.Animation Animations} that will be 238 * played sequentially. 239 * <p> 240 * It is not possible to change the children of a running 241 * {@code ParallelTransition}. If the children are changed for a running 242 * {@code ParallelTransition}, the animation has to be stopped and started 243 * again to pick up the new value. 244 * 245 * @return the list of {@link javafx.animation.Animation Animations} 246 */ 247 public final ObservableList<Animation> getChildren() { 248 return children; 249 } 250 251 /** 252 * The constructor of {@code ParallelTransition}. 253 * 254 * @param node 255 * The target {@link javafx.scene.Node} to be used in child 256 * {@link Transition Transitions} that have no {@code Node} specified 257 * themselves 258 * @param children 259 * The child {@link javafx.animation.Animation Animations} of 260 * this {@code ParallelTransition} 261 */ 262 public ParallelTransition(Node node, Animation... children) { 263 setInterpolator(Interpolator.LINEAR); 264 setNode(node); 265 getChildren().setAll(children); 266 } 267 268 /** 269 * The constructor of {@code ParallelTransition}. 270 * 271 * @param children 272 * The child {@link javafx.animation.Animation Animations} of 273 * this {@code ParallelTransition} 274 */ 275 public ParallelTransition(Animation... children) { 276 this(null, children); 277 } 278 279 /** 280 * The constructor of {@code ParallelTransition}. 281 * 282 * @param node 283 * The target {@link javafx.scene.Node} to be used in child 284 * {@link Transition Transitions} that have no {@code Node} specified 285 * themselves 286 */ 287 public ParallelTransition(Node node) { 288 setInterpolator(Interpolator.LINEAR); 289 setNode(node); 290 } 291 292 /** 293 * The constructor of {@code ParallelTransition}. 294 */ 295 public ParallelTransition() { 296 this((Node) null); 297 } 298 299 // For testing purposes 300 ParallelTransition(AbstractMasterTimer timer) { 301 super(timer); 302 setInterpolator(Interpolator.LINEAR); 303 } 304 305 /** 306 * {@inheritDoc} 307 */ 308 @Override 309 protected Node getParentTargetNode() { 310 final Node node = getNode(); 311 return (node != null) ? node : (parent != null && parent instanceof Transition) ? 312 ((Transition)parent).getParentTargetNode() : null; 313 } 314 315 private Duration computeCycleDuration() { 316 Duration maxTime = Duration.ZERO; 317 for (final Animation animation : getChildren()) { 318 final double absRate = Math.abs(animation.getRate()); 319 final Duration totalDuration = (absRate < EPSILON) ? 320 animation.getTotalDuration() : animation.getTotalDuration().divide(absRate); 321 final Duration childDuration = totalDuration.add(animation.getDelay()); 322 if (childDuration.isIndefinite()) { 323 return Duration.INDEFINITE; 324 } else { 325 if (childDuration.greaterThan(maxTime)) { 326 maxTime = childDuration; 327 } 328 } 329 } 330 return maxTime; 331 } 332 333 private double calculateFraction(long currentTicks, long cycleTicks) { 334 final double frac = (double) currentTicks / cycleTicks; 335 return (frac <= 0.0) ? 0 : (frac >= 1.0) ? 1.0 : frac; 336 } 337 338 private boolean startChild(Animation child, int index) { 339 final boolean forceSync = forceChildSync[index]; 340 if (child.impl_startable(forceSync)) { 341 child.clipEnvelope.setRate(rates[index] * Math.signum(getCurrentRate())); 342 child.impl_start(forceSync); 343 forceChildSync[index] = false; 344 return true; 345 } 346 return false; 347 } 348 349 @Override 350 void impl_sync(boolean forceSync) { 351 super.impl_sync(forceSync); 352 if ((forceSync && childrenChanged) || (durations == null)) { 353 cachedChildren = getChildren().toArray(EMPTY_ANIMATION_ARRAY); 354 final int n = cachedChildren.length; 355 durations = new long[n]; 356 delays = new long[n]; 357 rates = new double[n]; 358 offsetTicks = new long[n]; 359 forceChildSync = new boolean[n]; 360 cycleTime = 0; 361 int i = 0; 362 for (final Animation animation : cachedChildren) { 363 rates[i] = Math.abs(animation.getRate()); 364 if (rates[i] < EPSILON) { 365 rates[i] = 1; 366 } 367 durations[i] = fromDuration(animation.getTotalDuration(), rates[i]); 368 delays[i] = fromDuration(animation.getDelay()); 369 cycleTime = Math.max(cycleTime, add(durations[i], delays[i])); 370 forceChildSync[i] = true; 371 i++; 372 } 373 childrenChanged = false; 374 } else if (forceSync) { 375 final int n = forceChildSync.length; 376 for (int i=0; i<n; i++) { 377 forceChildSync[i] = true; 378 } 379 } 380 } 381 382 @Override 383 void impl_pause() { 384 super.impl_pause(); 385 for (final Animation animation : cachedChildren) { 386 if (animation.getStatus() == Status.RUNNING) { 387 animation.impl_pause(); 388 } 389 } 390 } 391 392 @Override 393 void impl_resume() { 394 super.impl_resume(); 395 int i = 0; 396 for (final Animation animation : cachedChildren) { 397 if (animation.getStatus() == Status.PAUSED) { 398 animation.impl_resume(); 399 animation.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate())); 400 } 401 i++; 402 } 403 } 404 405 @Override 406 void impl_start(boolean forceSync) { 407 super.impl_start(forceSync); 408 toggledRate = false; 409 rateProperty().addListener(rateListener); 410 double curRate = getCurrentRate(); 411 final long currentTicks = TickCalculation.fromDuration(getCurrentTime()); 412 if (curRate < 0) { 413 jumpToEnd(); 414 if (currentTicks < cycleTime) { 415 impl_jumpTo(currentTicks, cycleTime, false); 416 } 417 } else { 418 jumpToStart(); 419 if (currentTicks > 0) { 420 impl_jumpTo(currentTicks, cycleTime, false); 421 } 422 } 423 } 424 425 @Override 426 void impl_stop() { 427 super.impl_stop(); 428 for (final Animation animation : cachedChildren) { 429 if (animation.getStatus() != Status.STOPPED) { 430 animation.impl_stop(); 431 } 432 } 433 if (childrenChanged) { 434 setCycleDuration(computeCycleDuration()); 435 } 436 rateProperty().removeListener(rateListener); 437 } 438 439 440 /** 441 * @treatAsPrivate implementation detail 442 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 443 */ 444 @Deprecated 445 @Override public void impl_playTo(long currentTicks, long cycleTicks) { 446 impl_setCurrentTicks(currentTicks); 447 final double frac = calculateFraction(currentTicks, cycleTicks); 448 final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks)); 449 if (toggledRate) { 450 for (int i = 0; i < cachedChildren.length; ++i) { 451 if (cachedChildren[i].getStatus() == Status.RUNNING) { 452 offsetTicks[i] -= Math.signum(getCurrentRate()) * (durations[i] - 2 * (oldTicks - delays[i])); 453 } 454 } 455 toggledRate = false; 456 } 457 if (getCurrentRate() > 0) { 458 int i = 0; 459 for (final Animation animation : cachedChildren) { 460 if ((newTicks >= delays[i]) && ((oldTicks <= delays[i]) || 461 ((newTicks < add(delays[i], durations[i])) && (animation.getStatus() == Status.STOPPED)))) { 462 final boolean enteringCycle = oldTicks <= delays[i]; 463 if (startChild(animation, i)) { 464 animation.clipEnvelope.jumpTo(0); 465 } else { 466 if (enteringCycle) { 467 final EventHandler<ActionEvent> handler = animation.getOnFinished(); 468 if (handler != null) { 469 handler.handle(new ActionEvent(this, null)); 470 } 471 } 472 continue; 473 } 474 } 475 if (newTicks >= add(durations[i], delays[i])) { 476 if (animation.getStatus() == Status.RUNNING) { 477 animation.impl_timePulse(sub(durations[i], offsetTicks[i])); 478 offsetTicks[i] = 0; 479 } 480 } else if (newTicks > delays[i]) { 481 animation.impl_timePulse(sub(newTicks - delays[i], offsetTicks[i])); 482 } 483 i++; 484 } 485 } else { 486 int i = 0; 487 for (final Animation animation : cachedChildren) { 488 if (newTicks < add(durations[i], delays[i])) { 489 if ((oldTicks >= add(durations[i], delays[i])) || ((newTicks >= delays[i]) && (animation.getStatus() == Status.STOPPED))){ 490 final boolean enteringCycle = oldTicks >= add(durations[i], delays[i]); 491 if (startChild(animation, i)) { 492 animation.clipEnvelope.jumpTo(Math.round(durations[i] * rates[i])); 493 } else { 494 if (enteringCycle) { 495 final EventHandler<ActionEvent> handler = animation.getOnFinished(); 496 if (handler != null) { 497 handler.handle(new ActionEvent(this, null)); 498 } 499 } 500 continue; 501 } 502 } 503 if (newTicks <= delays[i]) { 504 if (animation.getStatus() == Status.RUNNING) { 505 animation.impl_timePulse(sub(durations[i], offsetTicks[i])); 506 offsetTicks[i] = 0; 507 } 508 } else { 509 animation.impl_timePulse(sub( add(durations[i], delays[i]) - newTicks, offsetTicks[i])); 510 } 511 } 512 i++; 513 } 514 } 515 oldTicks = newTicks; 516 } 517 518 /** 519 * @treatAsPrivate implementation detail 520 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 521 */ 522 @Deprecated 523 @Override public void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump) { 524 impl_setCurrentTicks(currentTicks); 525 if (getStatus() == Status.STOPPED && !forceJump) { 526 return; 527 } 528 impl_sync(false); 529 final double frac = calculateFraction(currentTicks, cycleTicks); 530 final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks)); 531 int i = 0; 532 for (final Animation animation : cachedChildren) { 533 final Status status = animation.getStatus(); 534 if (newTicks <= delays[i]) { 535 offsetTicks[i] = 0; 536 if (status != Status.STOPPED) { 537 animation.clipEnvelope.jumpTo(0); 538 animation.impl_stop(); 539 } else if(TickCalculation.fromDuration(animation.getCurrentTime()) != 0) { 540 animation.impl_jumpTo(0, durations[i], true); 541 } 542 } else if (newTicks >= add(durations[i], delays[i])) { 543 offsetTicks[i] = 0; 544 if (status != Status.STOPPED) { 545 animation.clipEnvelope.jumpTo(Math.round(durations[i] * rates[i])); 546 animation.impl_stop(); 547 } else if (TickCalculation.fromDuration(animation.getCurrentTime()) != durations[i]) { 548 animation.impl_jumpTo(durations[i], durations[i], true); 549 } 550 } else { 551 if (status == Status.STOPPED) { 552 startChild(animation, i); 553 if (getStatus() == Status.PAUSED) { 554 animation.impl_pause(); 555 } 556 557 offsetTicks[i] = (getCurrentRate() > 0)? newTicks - delays[i] : add(durations[i], delays[i]) - newTicks; 558 } else if (status == Status.PAUSED) { 559 offsetTicks[i] += (newTicks - oldTicks) * Math.signum(this.clipEnvelope.getCurrentRate()); 560 } else { 561 offsetTicks[i] += (getCurrentRate() > 0) ? newTicks - oldTicks : oldTicks - newTicks; 562 } 563 animation.clipEnvelope.jumpTo(Math.round(sub(newTicks, delays[i]) * rates[i])); 564 } 565 i++; 566 } 567 oldTicks = newTicks; 568 } 569 570 /** 571 * {@inheritDoc} 572 */ 573 @Override 574 protected void interpolate(double frac) { 575 // no-op 576 } 577 578 private void jumpToEnd() { 579 for (int i = 0 ; i < cachedChildren.length; ++i) { 580 if (forceChildSync[i]) { 581 // See explanation in SequentialTransition#jumpToEnd 582 cachedChildren[i].impl_sync(true); 583 } 584 cachedChildren[i].impl_jumpTo(durations[i], durations[i], true); 585 } 586 } 587 588 private void jumpToStart() { 589 for (int i = cachedChildren.length - 1 ; i >= 0; --i) { 590 if (forceChildSync[i]) { 591 cachedChildren[i].impl_sync(true); 592 } 593 cachedChildren[i].impl_jumpTo(0, durations[i], true); 594 } 595 } 596 597 }