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 }