1 /* 2 * Copyright (c) 2010, 2017, 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 java.util.HashMap; 29 30 import com.sun.javafx.tk.Toolkit; 31 import javafx.beans.property.BooleanProperty; 32 import javafx.beans.property.DoubleProperty; 33 import javafx.beans.property.DoublePropertyBase; 34 import javafx.beans.property.IntegerProperty; 35 import javafx.beans.property.IntegerPropertyBase; 36 import javafx.beans.property.ObjectProperty; 37 import javafx.beans.property.ObjectPropertyBase; 38 import javafx.beans.property.ReadOnlyDoubleProperty; 39 import javafx.beans.property.ReadOnlyDoublePropertyBase; 40 import javafx.beans.property.ReadOnlyObjectProperty; 41 import javafx.beans.property.ReadOnlyObjectPropertyBase; 42 import javafx.beans.property.SimpleBooleanProperty; 43 import javafx.beans.property.SimpleObjectProperty; 44 import javafx.collections.FXCollections; 45 import javafx.collections.ObservableMap; 46 import javafx.event.ActionEvent; 47 import javafx.event.EventHandler; 48 import javafx.util.Duration; 49 import com.sun.javafx.animation.TickCalculation; 50 import com.sun.scenario.animation.AbstractMasterTimer; 51 import com.sun.scenario.animation.shared.ClipEnvelope; 52 import com.sun.scenario.animation.shared.PulseReceiver; 53 54 import static com.sun.javafx.animation.TickCalculation.*; 55 import java.security.AccessControlContext; 56 import java.security.AccessController; 57 import java.security.PrivilegedAction; 58 59 /** 60 * The class {@code Animation} provides the core functionality of all animations 61 * used in the JavaFX runtime. 62 * <p> 63 * An animation can run in a loop by setting {@link #cycleCount}. To make an 64 * animation run back and forth while looping, set the {@link #autoReverse} 65 * -flag. 66 * <p> 67 * Call {@link #play()} or {@link #playFromStart()} to play an {@code Animation} 68 * . The {@code Animation} progresses in the direction and speed specified by 69 * {@link #rate}, and stops when its duration is elapsed. An {@code Animation} 70 * with indefinite duration (a {@link #cycleCount} of {@link #INDEFINITE}) runs 71 * repeatedly until the {@link #stop()} method is explicitly called, which will 72 * stop the running {@code Animation} and reset its play head to the initial 73 * position. 74 * <p> 75 * An {@code Animation} can be paused by calling {@link #pause()}, and the next 76 * {@link #play()} call will resume the {@code Animation} from where it was 77 * paused. 78 * <p> 79 * An {@code Animation}'s play head can be randomly positioned, whether it is 80 * running or not. If the {@code Animation} is running, the play head jumps to 81 * the specified position immediately and continues playing from new position. 82 * If the {@code Animation} is not running, the next {@link #play()} will start 83 * the {@code Animation} from the specified position. 84 * <p> 85 * Inverting the value of {@link #rate} toggles the play direction. 86 * 87 * @see Timeline 88 * @see Transition 89 * 90 * @since JavaFX 2.0 91 */ 92 public abstract class Animation { 93 94 static { 95 AnimationAccessorImpl.DEFAULT = new AnimationAccessorImpl(); 96 } 97 98 /** 99 * Used to specify an animation that repeats indefinitely, until the 100 * {@code stop()} method is called. 101 */ 102 public static final int INDEFINITE = -1; 103 104 /** 105 * The possible states for {@link Animation#statusProperty status}. 106 * @since JavaFX 2.0 107 */ 108 public static enum Status { 109 /** 110 * The paused state. 111 */ 112 PAUSED, 113 /** 114 * The running state. 115 */ 116 RUNNING, 117 /** 118 * The stopped state. 119 */ 120 STOPPED 121 } 122 123 private static final double EPSILON = 1e-12; 124 125 /* 126 These four fields and associated methods were moved here from AnimationPulseReceiver 127 when that class was removed. They could probably be integrated much cleaner into Animation, 128 but to make sure the change was made without introducing regressions, this code was 129 moved pretty much verbatim. 130 */ 131 private long startTime; 132 private long pauseTime; 133 private boolean paused = false; 134 private final AbstractMasterTimer timer; 135 136 // Access control context, captured whenever we add this pulse reciever to 137 // the master timer (which is called when an animation is played or resumed) 138 private AccessControlContext accessCtrlCtx = null; 139 140 private long now() { 141 return TickCalculation.fromNano(timer.nanos()); 142 } 143 144 private void addPulseReceiver() { 145 // Capture the Access Control Context to be used during the animation pulse 146 accessCtrlCtx = AccessController.getContext(); 147 148 timer.addPulseReceiver(pulseReceiver); 149 } 150 151 void startReceiver(long delay) { 152 paused = false; 153 startTime = now() + delay; 154 addPulseReceiver(); 155 } 156 157 void pauseReceiver() { 158 if (!paused) { 159 pauseTime = now(); 160 paused = true; 161 timer.removePulseReceiver(pulseReceiver); 162 } 163 } 164 165 void resumeReceiver() { 166 if (paused) { 167 final long deltaTime = now() - pauseTime; 168 startTime += deltaTime; 169 paused = false; 170 addPulseReceiver(); 171 } 172 } 173 174 // package private only for the sake of testing 175 final PulseReceiver pulseReceiver = new PulseReceiver() { 176 @Override public void timePulse(long now) { 177 final long elapsedTime = now - startTime; 178 if (elapsedTime < 0) { 179 return; 180 } 181 if (accessCtrlCtx == null) { 182 throw new IllegalStateException("Error: AccessControlContext not captured"); 183 } 184 185 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 186 doTimePulse(elapsedTime); 187 return null; 188 }, accessCtrlCtx); 189 } 190 }; 191 192 private class CurrentRateProperty extends ReadOnlyDoublePropertyBase { 193 private double value; 194 195 @Override 196 public Object getBean() { 197 return Animation.this; 198 } 199 200 @Override 201 public String getName() { 202 return "currentRate"; 203 } 204 205 @Override 206 public double get() { 207 return value; 208 } 209 210 private void set(double value) { 211 this.value = value; 212 fireValueChangedEvent(); 213 } 214 } 215 216 private class AnimationReadOnlyProperty<T> extends ReadOnlyObjectPropertyBase<T> { 217 218 private final String name; 219 private T value; 220 221 private AnimationReadOnlyProperty(String name, T value) { 222 this.name = name; 223 this.value = value; 224 } 225 226 @Override 227 public Object getBean() { 228 return Animation.this; 229 } 230 231 @Override 232 public String getName() { 233 return name; 234 } 235 236 @Override 237 public T get() { 238 return value; 239 } 240 241 private void set(T value) { 242 this.value = value; 243 fireValueChangedEvent(); 244 } 245 } 246 247 /** 248 * The parent of this {@code Animation}. If this animation has not been 249 * added to another animation, such as {@link ParallelTransition} and 250 * {@link SequentialTransition}, then parent will be null. 251 * 252 * @defaultValue null 253 */ 254 Animation parent = null; 255 256 /* Package-private for testing purposes */ 257 ClipEnvelope clipEnvelope; 258 259 private boolean lastPlayedFinished = false; 260 261 private boolean lastPlayedForward = true; 262 /** 263 * Defines the direction/speed at which the {@code Animation} is expected to 264 * be played. 265 * <p> 266 * The absolute value of {@code rate} indicates the speed which the 267 * {@code Animation} is to be played, while the sign of {@code rate} 268 * indicates the direction. A positive value of {@code rate} indicates 269 * forward play, a negative value indicates backward play and {@code 0.0} to 270 * stop a running {@code Animation}. 271 * <p> 272 * Rate {@code 1.0} is normal play, {@code 2.0} is 2 time normal, 273 * {@code -1.0} is backwards, etc... 274 * 275 * <p> 276 * Inverting the rate of a running {@code Animation} will cause the 277 * {@code Animation} to reverse direction in place and play back over the 278 * portion of the {@code Animation} that has already elapsed. 279 * 280 * @defaultValue 1.0 281 */ 282 private DoubleProperty rate; 283 private static final double DEFAULT_RATE = 1.0; 284 285 public final void setRate(double value) { 286 if ((rate != null) || (Math.abs(value - DEFAULT_RATE) > EPSILON)) { 287 rateProperty().set(value); 288 } 289 } 290 291 public final double getRate() { 292 return (rate == null)? DEFAULT_RATE : rate.get(); 293 } 294 295 public final DoubleProperty rateProperty() { 296 if (rate == null) { 297 rate = new DoublePropertyBase(DEFAULT_RATE) { 298 299 @Override 300 public void invalidated() { 301 final double newRate = getRate(); 302 if (isRunningEmbedded()) { 303 if (isBound()) { 304 unbind(); 305 } 306 set(oldRate); 307 throw new IllegalArgumentException("Cannot set rate of embedded animation while running."); 308 } else { 309 if (Math.abs(newRate) < EPSILON) { 310 if (getStatus() == Status.RUNNING) { 311 lastPlayedForward = (Math.abs(getCurrentRate() 312 - oldRate) < EPSILON); 313 } 314 doSetCurrentRate(0.0); 315 pauseReceiver(); 316 } else { 317 if (getStatus() == Status.RUNNING) { 318 final double currentRate = getCurrentRate(); 319 if (Math.abs(currentRate) < EPSILON) { 320 doSetCurrentRate(lastPlayedForward ? newRate : -newRate); 321 resumeReceiver(); 322 } else { 323 final boolean playingForward = Math.abs(currentRate - oldRate) < EPSILON; 324 doSetCurrentRate(playingForward ? newRate : -newRate); 325 } 326 } 327 oldRate = newRate; 328 } 329 clipEnvelope.setRate(newRate); 330 } 331 } 332 333 @Override 334 public Object getBean() { 335 return Animation.this; 336 } 337 338 @Override 339 public String getName() { 340 return "rate"; 341 } 342 }; 343 } 344 return rate; 345 } 346 347 private boolean isRunningEmbedded() { 348 if (parent == null) { 349 return false; 350 } 351 return parent.getStatus() != Status.STOPPED || parent.isRunningEmbedded(); 352 } 353 354 private double oldRate = 1.0; 355 /** 356 * Read-only variable to indicate current direction/speed at which the 357 * {@code Animation} is being played. 358 * <p> 359 * {@code currentRate} is not necessary equal to {@code rate}. 360 * {@code currentRate} is set to {@code 0.0} when animation is paused or 361 * stopped. {@code currentRate} may also point to different direction during 362 * reverse cycles when {@code autoReverse} is {@code true} 363 * 364 * @defaultValue 0.0 365 */ 366 private ReadOnlyDoubleProperty currentRate; 367 private static final double DEFAULT_CURRENT_RATE = 0.0; 368 369 private void doSetCurrentRate(double value) { 370 if ((currentRate != null) || (Math.abs(value - DEFAULT_CURRENT_RATE) > EPSILON)) { 371 ((CurrentRateProperty)currentRateProperty()).set(value); 372 } 373 } 374 375 public final double getCurrentRate() { 376 return (currentRate == null)? DEFAULT_CURRENT_RATE : currentRate.get(); 377 } 378 379 public final ReadOnlyDoubleProperty currentRateProperty() { 380 if (currentRate == null) { 381 currentRate = new CurrentRateProperty(); 382 } 383 return currentRate; 384 } 385 386 /** 387 * Read-only variable to indicate the duration of one cycle of this 388 * {@code Animation}: the time it takes to play from time 0 to the 389 * end of the Animation (at the default {@code rate} of 390 * 1.0). 391 * 392 * @defaultValue 0ms 393 */ 394 private ReadOnlyObjectProperty<Duration> cycleDuration; 395 private static final Duration DEFAULT_CYCLE_DURATION = Duration.ZERO; 396 397 protected final void setCycleDuration(Duration value) { 398 if ((cycleDuration != null) || (!DEFAULT_CYCLE_DURATION.equals(value))) { 399 if (value.lessThan(Duration.ZERO)) { 400 throw new IllegalArgumentException("Cycle duration cannot be negative"); 401 } 402 ((AnimationReadOnlyProperty<Duration>)cycleDurationProperty()).set(value); 403 updateTotalDuration(); 404 } 405 } 406 407 public final Duration getCycleDuration() { 408 return (cycleDuration == null)? DEFAULT_CYCLE_DURATION : cycleDuration.get(); 409 } 410 411 public final ReadOnlyObjectProperty<Duration> cycleDurationProperty() { 412 if (cycleDuration == null) { 413 cycleDuration = new AnimationReadOnlyProperty<Duration>("cycleDuration", DEFAULT_CYCLE_DURATION); 414 } 415 return cycleDuration; 416 } 417 418 /** 419 * Read-only variable to indicate the total duration of this 420 * {@code Animation}, including repeats. A {@code Animation} with a {@code cycleCount} 421 * of {@code Animation.INDEFINITE} will have a {@code totalDuration} of 422 * {@code Duration.INDEFINITE}. 423 * 424 * <p> 425 * This is set to cycleDuration * cycleCount. 426 * 427 * @defaultValue 0ms 428 */ 429 private ReadOnlyObjectProperty<Duration> totalDuration; 430 private static final Duration DEFAULT_TOTAL_DURATION = Duration.ZERO; 431 432 public final Duration getTotalDuration() { 433 return (totalDuration == null)? DEFAULT_TOTAL_DURATION : totalDuration.get(); 434 } 435 436 public final ReadOnlyObjectProperty<Duration> totalDurationProperty() { 437 if (totalDuration == null) { 438 totalDuration = new AnimationReadOnlyProperty<Duration>("totalDuration", DEFAULT_TOTAL_DURATION); 439 } 440 return totalDuration; 441 } 442 443 private void updateTotalDuration() { 444 // Implementing the bind eagerly, because cycleCount and 445 // cycleDuration should not change that often 446 final int cycleCount = getCycleCount(); 447 final Duration cycleDuration = getCycleDuration(); 448 final Duration newTotalDuration = Duration.ZERO.equals(cycleDuration) ? Duration.ZERO 449 : (cycleCount == Animation.INDEFINITE) ? Duration.INDEFINITE 450 : (cycleCount <= 1) ? cycleDuration : cycleDuration 451 .multiply(cycleCount); 452 if ((totalDuration != null) || (!DEFAULT_TOTAL_DURATION.equals(newTotalDuration))) { 453 ((AnimationReadOnlyProperty<Duration>)totalDurationProperty()).set(newTotalDuration); 454 } 455 if (getStatus() == Status.STOPPED) { 456 syncClipEnvelope(); 457 if (newTotalDuration.lessThan(getCurrentTime())) { 458 clipEnvelope.jumpTo(fromDuration(newTotalDuration)); 459 } 460 } 461 } 462 463 /** 464 * Defines the {@code Animation}'s play head position. 465 * 466 * @defaultValue 0ms 467 */ 468 private CurrentTimeProperty currentTime; 469 private long currentTicks; 470 private class CurrentTimeProperty extends ReadOnlyObjectPropertyBase<Duration> { 471 472 @Override 473 public Object getBean() { 474 return Animation.this; 475 } 476 477 @Override 478 public String getName() { 479 return "currentTime"; 480 } 481 482 @Override 483 public Duration get() { 484 return getCurrentTime(); 485 } 486 487 @Override 488 public void fireValueChangedEvent() { 489 super.fireValueChangedEvent(); 490 } 491 492 } 493 494 public final Duration getCurrentTime() { 495 return TickCalculation.toDuration(currentTicks); 496 } 497 498 public final ReadOnlyObjectProperty<Duration> currentTimeProperty() { 499 if (currentTime == null) { 500 currentTime = new CurrentTimeProperty(); 501 } 502 return currentTime; 503 } 504 505 /** 506 * Delays the start of an animation. 507 * 508 * Cannot be negative. Setting to a negative number will result in {@link IllegalArgumentException}. 509 * 510 * @defaultValue 0ms 511 */ 512 private ObjectProperty<Duration> delay; 513 private static final Duration DEFAULT_DELAY = Duration.ZERO; 514 515 public final void setDelay(Duration value) { 516 if ((delay != null) || (!DEFAULT_DELAY.equals(value))) { 517 delayProperty().set(value); 518 } 519 } 520 521 public final Duration getDelay() { 522 return (delay == null)? DEFAULT_DELAY : delay.get(); 523 } 524 525 public final ObjectProperty<Duration> delayProperty() { 526 if (delay == null) { 527 delay = new ObjectPropertyBase<Duration>(DEFAULT_DELAY) { 528 529 @Override 530 public Object getBean() { 531 return Animation.this; 532 } 533 534 @Override 535 public String getName() { 536 return "delay"; 537 } 538 539 @Override 540 protected void invalidated() { 541 final Duration newDuration = get(); 542 if (newDuration.lessThan(Duration.ZERO)) { 543 if (isBound()) { 544 unbind(); 545 } 546 set(Duration.ZERO); 547 throw new IllegalArgumentException("Cannot set delay to negative value. Setting to Duration.ZERO"); 548 } 549 } 550 551 }; 552 } 553 return delay; 554 } 555 556 /** 557 * Defines the number of cycles in this animation. The {@code cycleCount} 558 * may be {@code INDEFINITE} for animations that repeat indefinitely, but 559 * must otherwise be {@literal >} 0. 560 * <p> 561 * It is not possible to change the {@code cycleCount} of a running 562 * {@code Animation}. If the value of {@code cycleCount} is changed for a 563 * running {@code Animation}, the animation has to be stopped and started again to pick 564 * up the new value. 565 * 566 * @defaultValue 1.0 567 * 568 */ 569 private IntegerProperty cycleCount; 570 private static final int DEFAULT_CYCLE_COUNT = 1; 571 572 public final void setCycleCount(int value) { 573 if ((cycleCount != null) || (value != DEFAULT_CYCLE_COUNT)) { 574 cycleCountProperty().set(value); 575 } 576 } 577 578 public final int getCycleCount() { 579 return (cycleCount == null)? DEFAULT_CYCLE_COUNT : cycleCount.get(); 580 } 581 582 public final IntegerProperty cycleCountProperty() { 583 if (cycleCount == null) { 584 cycleCount = new IntegerPropertyBase(DEFAULT_CYCLE_COUNT) { 585 586 @Override 587 public void invalidated() { 588 updateTotalDuration(); 589 } 590 591 @Override 592 public Object getBean() { 593 return Animation.this; 594 } 595 596 @Override 597 public String getName() { 598 return "cycleCount"; 599 } 600 }; 601 } 602 return cycleCount; 603 } 604 605 /** 606 * Defines whether this 607 * {@code Animation} reverses direction on alternating cycles. If 608 * {@code true}, the 609 * {@code Animation} will proceed forward on the first cycle, 610 * then reverses on the second cycle, and so on. Otherwise, animation will 611 * loop such that each cycle proceeds forward from the start. 612 * 613 * It is not possible to change the {@code autoReverse} flag of a running 614 * {@code Animation}. If the value of {@code autoReverse} is changed for a 615 * running {@code Animation}, the animation has to be stopped and started again to pick 616 * up the new value. 617 * 618 * @defaultValue false 619 */ 620 private BooleanProperty autoReverse; 621 private static final boolean DEFAULT_AUTO_REVERSE = false; 622 623 public final void setAutoReverse(boolean value) { 624 if ((autoReverse != null) || (value != DEFAULT_AUTO_REVERSE)) { 625 autoReverseProperty().set(value); 626 } 627 } 628 629 public final boolean isAutoReverse() { 630 return (autoReverse == null)? DEFAULT_AUTO_REVERSE : autoReverse.get(); 631 } 632 633 public final BooleanProperty autoReverseProperty() { 634 if (autoReverse == null) { 635 autoReverse = new SimpleBooleanProperty(this, "autoReverse", DEFAULT_AUTO_REVERSE); 636 } 637 return autoReverse; 638 } 639 640 /** 641 * The status of the {@code Animation}. 642 * 643 * In {@code Animation} can be in one of three states: 644 * {@link Status#STOPPED}, {@link Status#PAUSED} or {@link Status#RUNNING}. 645 */ 646 private ReadOnlyObjectProperty<Status> status; 647 private static final Status DEFAULT_STATUS = Status.STOPPED; 648 649 protected final void setStatus(Status value) { 650 if ((status != null) || (!DEFAULT_STATUS.equals(value))) { 651 ((AnimationReadOnlyProperty<Status>)statusProperty()).set(value); 652 } 653 } 654 655 public final Status getStatus() { 656 return (status == null)? DEFAULT_STATUS : status.get(); 657 } 658 659 public final ReadOnlyObjectProperty<Status> statusProperty() { 660 if (status == null) { 661 status = new AnimationReadOnlyProperty<Status>("status", Status.STOPPED); 662 } 663 return status; 664 } 665 666 private final double targetFramerate; 667 private final int resolution; 668 private long lastPulse; 669 670 /** 671 * The target framerate is the maximum framerate at which this {@code Animation} 672 * will run, in frames per second. This can be used, for example, to keep 673 * particularly complex {@code Animations} from over-consuming system resources. 674 * By default, an {@code Animation}'s framerate is not explicitly limited, meaning 675 * the {@code Animation} will run at an optimal framerate for the underlying platform. 676 * 677 * @return the target framerate 678 */ 679 public final double getTargetFramerate() { 680 return targetFramerate; 681 } 682 683 /** 684 * The action to be executed at the conclusion of this {@code Animation}. 685 * 686 * @defaultValue null 687 */ 688 private ObjectProperty<EventHandler<ActionEvent>> onFinished; 689 private static final EventHandler<ActionEvent> DEFAULT_ON_FINISHED = null; 690 691 public final void setOnFinished(EventHandler<ActionEvent> value) { 692 if ((onFinished != null) || (value != null /* DEFAULT_ON_FINISHED */)) { 693 onFinishedProperty().set(value); 694 } 695 } 696 697 public final EventHandler<ActionEvent> getOnFinished() { 698 return (onFinished == null)? DEFAULT_ON_FINISHED : onFinished.get(); 699 } 700 701 public final ObjectProperty<EventHandler<ActionEvent>> onFinishedProperty() { 702 if (onFinished == null) { 703 onFinished = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onFinished", DEFAULT_ON_FINISHED); 704 } 705 return onFinished; 706 } 707 708 private final ObservableMap<String, Duration> cuePoints = FXCollections 709 .observableMap(new HashMap<String, Duration>(0)); 710 711 /** 712 * The cue points can be 713 * used to mark important positions of the {@code Animation}. Once a cue 714 * point was defined, it can be used as an argument of 715 * {@link #jumpTo(String) jumpTo()} and {@link #playFrom(String) playFrom()} 716 * to move to the associated position quickly. 717 * <p> 718 * Every {@code Animation} has two predefined cue points {@code "start"} and 719 * {@code "end"}, which are set at the start respectively the end of the 720 * {@code Animation}. The predefined cuepoints do not appear in the map, 721 * attempts to override them have no effect. 722 * <p> 723 * Another option to define a cue point in a {@code Animation} is to set the 724 * {@link KeyFrame#name} property of a {@link KeyFrame}. 725 * 726 * @return {@link javafx.collections.ObservableMap} of cue points 727 */ 728 public final ObservableMap<String, Duration> getCuePoints() { 729 return cuePoints; 730 } 731 732 /** 733 * Jumps to a given position in this {@code Animation}. 734 * 735 * If the given time is less than {@link Duration#ZERO}, this method will 736 * jump to the start of the animation. If the given time is larger than the 737 * duration of this {@code Animation}, this method will jump to the end. 738 * 739 * @param time 740 * the new position 741 * @throws NullPointerException 742 * if {@code time} is {@code null} 743 * @throws IllegalArgumentException 744 * if {@code time} is {@link Duration#UNKNOWN} 745 * @throws IllegalStateException 746 * if embedded in another animation, 747 * such as {@link SequentialTransition} or {@link ParallelTransition} 748 */ 749 public void jumpTo(Duration time) { 750 if (time == null) { 751 throw new NullPointerException("Time needs to be specified."); 752 } 753 if (time.isUnknown()) { 754 throw new IllegalArgumentException("The time is invalid"); 755 } 756 if (parent != null) { 757 throw new IllegalStateException("Cannot jump when embedded in another animation"); 758 } 759 760 lastPlayedFinished = false; 761 762 final Duration totalDuration = getTotalDuration(); 763 time = time.lessThan(Duration.ZERO) ? Duration.ZERO : time 764 .greaterThan(totalDuration) ? totalDuration : time; 765 final long ticks = fromDuration(time); 766 767 if (getStatus() == Status.STOPPED) { 768 syncClipEnvelope(); 769 } 770 clipEnvelope.jumpTo(ticks); 771 } 772 773 /** 774 * Jumps to a predefined position in this {@code Animation}. This method 775 * looks for an entry in cue points and jumps to the associated 776 * position, if it finds one. 777 * <p> 778 * If the cue point is behind the end of this {@code Animation}, calling 779 * {@code jumpTo} will result in a jump to the end. If the cue point has a 780 * negative {@link javafx.util.Duration} it will result in a jump to the 781 * beginning. If the cue point has a value of 782 * {@link javafx.util.Duration#UNKNOWN} calling {@code jumpTo} will have no 783 * effect for this cue point. 784 * <p> 785 * There are two predefined cue points {@code "start"} and {@code "end"} 786 * which are defined to be at the start respectively the end of this 787 * {@code Animation}. 788 * 789 * @param cuePoint 790 * the name of the cue point 791 * @throws NullPointerException 792 * if {@code cuePoint} is {@code null} 793 * @throws IllegalStateException 794 * if embedded in another animation, 795 * such as {@link SequentialTransition} or {@link ParallelTransition} 796 * @see #getCuePoints() 797 */ 798 public void jumpTo(String cuePoint) { 799 if (cuePoint == null) { 800 throw new NullPointerException("CuePoint needs to be specified"); 801 } 802 if ("start".equalsIgnoreCase(cuePoint)) { 803 jumpTo(Duration.ZERO); 804 } else if ("end".equalsIgnoreCase(cuePoint)) { 805 jumpTo(getTotalDuration()); 806 } else { 807 final Duration target = getCuePoints().get(cuePoint); 808 if (target != null) { 809 jumpTo(target); 810 } 811 } 812 } 813 814 /** 815 * A convenience method to play this {@code Animation} from a predefined 816 * position. The position has to be predefined in cue points. 817 * Calling this method is equivalent to 818 * 819 * <pre> 820 * <code> 821 * animation.jumpTo(cuePoint); 822 * animation.play(); 823 * </code> 824 * </pre> 825 * 826 * Note that unlike {@link #playFromStart()} calling this method will not 827 * change the playing direction of this {@code Animation}. 828 * 829 * @param cuePoint 830 * name of the cue point 831 * @throws NullPointerException 832 * if {@code cuePoint} is {@code null} 833 * @throws IllegalStateException 834 * if embedded in another animation, 835 * such as {@link SequentialTransition} or {@link ParallelTransition} 836 * @see #getCuePoints() 837 */ 838 public void playFrom(String cuePoint) { 839 jumpTo(cuePoint); 840 play(); 841 } 842 843 /** 844 * A convenience method to play this {@code Animation} from a specific 845 * position. Calling this method is equivalent to 846 * 847 * <pre> 848 * <code> 849 * animation.jumpTo(time); 850 * animation.play(); 851 * </code> 852 * </pre> 853 * 854 * Note that unlike {@link #playFromStart()} calling this method will not 855 * change the playing direction of this {@code Animation}. 856 * 857 * @param time 858 * position where to play from 859 * @throws NullPointerException 860 * if {@code time} is {@code null} 861 * @throws IllegalArgumentException 862 * if {@code time} is {@link Duration#UNKNOWN} 863 * @throws IllegalStateException 864 * if embedded in another animation, 865 * such as {@link SequentialTransition} or {@link ParallelTransition} 866 */ 867 public void playFrom(Duration time) { 868 jumpTo(time); 869 play(); 870 } 871 872 /** 873 * Plays {@code Animation} from current position in the direction indicated 874 * by {@code rate}. If the {@code Animation} is running, it has no effect. 875 * <p> 876 * When {@code rate} {@literal >} 0 (forward play), if an {@code Animation} is already 877 * positioned at the end, the first cycle will not be played, it is 878 * considered to have already finished. This also applies to a backward ( 879 * {@code rate} {@literal <} 0) cycle if an {@code Animation} is positioned at the beginning. 880 * However, if the {@code Animation} has {@code cycleCount} {@literal >} 1, following 881 * cycle(s) will be played as usual. 882 * <p> 883 * When the {@code Animation} reaches the end, the {@code Animation} is stopped and 884 * the play head remains at the end. 885 * <p> 886 * To play an {@code Animation} backwards from the end:<br> 887 * <code> 888 * animation.setRate(negative rate);<br> 889 * animation.jumpTo(overall duration of animation);<br> 890 * animation.play();<br> 891 * </code> 892 * <p> 893 * Note: <ul> 894 * <li>{@code play()} is an asynchronous call, the {@code Animation} may not 895 * start immediately. </ul> 896 * 897 * @throws IllegalStateException 898 * if embedded in another animation, 899 * such as {@link SequentialTransition} or {@link ParallelTransition} 900 */ 901 public void play() { 902 if (parent != null) { 903 throw new IllegalStateException("Cannot start when embedded in another animation"); 904 } 905 switch (getStatus()) { 906 case STOPPED: 907 if (startable(true)) { 908 final double rate = getRate(); 909 if (lastPlayedFinished) { 910 jumpTo((rate < 0)? getTotalDuration() : Duration.ZERO); 911 } 912 doStart(true); 913 startReceiver(TickCalculation.fromDuration(getDelay())); 914 if (Math.abs(rate) < EPSILON) { 915 pauseReceiver(); 916 } else { 917 918 } 919 } else { 920 final EventHandler<ActionEvent> handler = getOnFinished(); 921 if (handler != null) { 922 handler.handle(new ActionEvent(this, null)); 923 } 924 } 925 break; 926 case PAUSED: 927 doResume(); 928 if (Math.abs(getRate()) >= EPSILON) { 929 resumeReceiver(); 930 } 931 break; 932 } 933 } 934 935 /** 936 * Plays an {@code Animation} from initial position in forward direction. 937 * <p> 938 * It is equivalent to 939 * <p> 940 * <code> 941 * animation.stop();<br> 942 * animation.setRate = setRate(Math.abs(animation.getRate())); <br> 943 * animation.jumpTo(Duration.ZERO);<br> 944 * animation.play();<br> 945 * </code> 946 * 947 * <p> 948 * Note: <ul> 949 * <li>{@code playFromStart()} is an asynchronous call, {@code Animation} may 950 * not start immediately. </ul> 951 * <p> 952 * 953 * @throws IllegalStateException 954 * if embedded in another animation, 955 * such as {@link SequentialTransition} or {@link ParallelTransition} 956 */ 957 public void playFromStart() { 958 stop(); 959 setRate(Math.abs(getRate())); 960 jumpTo(Duration.ZERO); 961 play(); 962 } 963 964 /** 965 * Stops the animation and resets the play head to its initial position. If 966 * the animation is not currently running, this method has no effect. 967 * <p> 968 * Note: <ul> 969 * <li>{@code stop()} is an asynchronous call, the {@code Animation} may not stop 970 * immediately. </ul> 971 * @throws IllegalStateException 972 * if embedded in another animation, 973 * such as {@link SequentialTransition} or {@link ParallelTransition} 974 */ 975 public void stop() { 976 if (parent != null) { 977 throw new IllegalStateException("Cannot stop when embedded in another animation"); 978 } 979 if (getStatus() != Status.STOPPED) { 980 clipEnvelope.abortCurrentPulse(); 981 doStop(); 982 jumpTo(Duration.ZERO); 983 } 984 } 985 986 /** 987 * Pauses the animation. If the animation is not currently running, this 988 * method has no effect. 989 * <p> 990 * Note: <ul> 991 * <li>{@code pause()} is an asynchronous call, the {@code Animation} may not pause 992 * immediately. </ul> 993 * @throws IllegalStateException 994 * if embedded in another animation, 995 * such as {@link SequentialTransition} or {@link ParallelTransition} 996 */ 997 public void pause() { 998 if (parent != null) { 999 throw new IllegalStateException("Cannot pause when embedded in another animation"); 1000 } 1001 if (getStatus() == Status.RUNNING) { 1002 clipEnvelope.abortCurrentPulse(); 1003 pauseReceiver(); 1004 doPause(); 1005 } 1006 } 1007 1008 /** 1009 * The constructor of {@code Animation}. 1010 * 1011 * This constructor allows to define a target framerate. 1012 * 1013 * @param targetFramerate 1014 * The custom target frame rate for this {@code Animation} 1015 * @see #getTargetFramerate() 1016 */ 1017 protected Animation(double targetFramerate) { 1018 this.targetFramerate = targetFramerate; 1019 this.resolution = (int) Math.max(1, Math.round(TickCalculation.TICKS_PER_SECOND / targetFramerate)); 1020 this.clipEnvelope = ClipEnvelope.create(this); 1021 this.timer = Toolkit.getToolkit().getMasterTimer(); 1022 } 1023 1024 /** 1025 * The constructor of {@code Animation}. 1026 */ 1027 protected Animation() { 1028 this.resolution = 1; 1029 this.targetFramerate = TickCalculation.TICKS_PER_SECOND / Toolkit.getToolkit().getMasterTimer().getDefaultResolution(); 1030 this.clipEnvelope = ClipEnvelope.create(this); 1031 this.timer = Toolkit.getToolkit().getMasterTimer(); 1032 } 1033 1034 // These constructors are only for testing purposes 1035 Animation(AbstractMasterTimer timer) { 1036 this.resolution = 1; 1037 this.targetFramerate = TickCalculation.TICKS_PER_SECOND / timer.getDefaultResolution(); 1038 this.clipEnvelope = ClipEnvelope.create(this); 1039 this.timer = timer; 1040 } 1041 1042 // These constructors are only for testing purposes 1043 Animation(AbstractMasterTimer timer, ClipEnvelope clipEnvelope, int resolution) { 1044 this.resolution = resolution; 1045 this.targetFramerate = TickCalculation.TICKS_PER_SECOND / resolution; 1046 this.clipEnvelope = clipEnvelope; 1047 this.timer = timer; 1048 } 1049 1050 boolean startable(boolean forceSync) { 1051 return (fromDuration(getCycleDuration()) > 0L) 1052 || (!forceSync && clipEnvelope.wasSynched()); 1053 } 1054 1055 void sync(boolean forceSync) { 1056 if (forceSync || !clipEnvelope.wasSynched()) { 1057 syncClipEnvelope(); 1058 } 1059 } 1060 1061 private void syncClipEnvelope() { 1062 final int publicCycleCount = getCycleCount(); 1063 final int internalCycleCount = (publicCycleCount <= 0) 1064 && (publicCycleCount != INDEFINITE) ? 1 : publicCycleCount; 1065 clipEnvelope = clipEnvelope.setCycleCount(internalCycleCount); 1066 clipEnvelope.setCycleDuration(getCycleDuration()); 1067 clipEnvelope.setAutoReverse(isAutoReverse()); 1068 } 1069 1070 void doStart(boolean forceSync) { 1071 sync(forceSync); 1072 setStatus(Status.RUNNING); 1073 clipEnvelope.start(); 1074 doSetCurrentRate(clipEnvelope.getCurrentRate()); 1075 lastPulse = 0; 1076 } 1077 1078 void doPause() { 1079 final double currentRate = getCurrentRate(); 1080 if (Math.abs(currentRate) >= EPSILON) { 1081 lastPlayedForward = Math.abs(getCurrentRate() - getRate()) < EPSILON; 1082 } 1083 doSetCurrentRate(0.0); 1084 setStatus(Status.PAUSED); 1085 } 1086 1087 void doResume() { 1088 setStatus(Status.RUNNING); 1089 doSetCurrentRate(lastPlayedForward ? getRate() : -getRate()); 1090 } 1091 1092 void doStop() { 1093 if (!paused) { 1094 timer.removePulseReceiver(pulseReceiver); 1095 } 1096 setStatus(Status.STOPPED); 1097 doSetCurrentRate(0.0); 1098 } 1099 1100 void doTimePulse(long elapsedTime) { 1101 if (resolution == 1) { // fullspeed 1102 clipEnvelope.timePulse(elapsedTime); 1103 } else if (elapsedTime - lastPulse >= resolution) { 1104 lastPulse = (elapsedTime / resolution) * resolution; 1105 clipEnvelope.timePulse(elapsedTime); 1106 } 1107 } 1108 1109 abstract void doPlayTo(long currentTicks, long cycleTicks); 1110 1111 abstract void doJumpTo(long currentTicks, long cycleTicks, boolean forceJump); 1112 1113 void setCurrentTicks(long ticks) { 1114 currentTicks = ticks; 1115 if (currentTime != null) { 1116 currentTime.fireValueChangedEvent(); 1117 } 1118 } 1119 1120 void setCurrentRate(double currentRate) { 1121 // if (getStatus() == Status.RUNNING) { 1122 doSetCurrentRate(currentRate); 1123 // } 1124 } 1125 1126 final void finished() { 1127 lastPlayedFinished = true; 1128 doStop(); 1129 final EventHandler<ActionEvent> handler = getOnFinished(); 1130 if (handler != null) { 1131 try { 1132 handler.handle(new ActionEvent(this, null)); 1133 } catch (Exception ex) { 1134 Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), ex); 1135 } 1136 } 1137 } 1138 }