Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.animation;
027
028import com.sun.javafx.animation.TickCalculation;
029import static com.sun.javafx.animation.TickCalculation.*;
030
031import javafx.beans.InvalidationListener;
032import javafx.beans.Observable;
033import javafx.beans.property.ObjectProperty;
034import javafx.collections.ListChangeListener.Change;
035import javafx.collections.ObservableList;
036import javafx.event.ActionEvent;
037import javafx.event.EventHandler;
038import javafx.scene.Node;
039import javafx.util.Duration;
040
041import com.sun.javafx.collections.TrackableObservableList;
042import com.sun.javafx.collections.VetoableListDecorator;
043import com.sun.scenario.animation.AbstractMasterTimer;
044import java.util.HashSet;
045import java.util.List;
046import java.util.Set;
047import javafx.beans.value.ChangeListener;
048import javafx.beans.value.ObservableValue;
049
050/**
051 * This {@link Transition} plays a list of {@link javafx.animation.Animation
052 * Animations} in parallel.
053 * <p>
054 * Children of this {@code Transition} inherit {@link #nodeProperty() node}, if their
055 * {@code node} property is not specified.
056 *
057 * <p>
058 * Code Segment Example:
059 * </p>
060 *
061 * <pre>
062 * <code>
063 *     Rectangle rect = new Rectangle (100, 40, 100, 100);
064 *     rect.setArcHeight(50);
065 *     rect.setArcWidth(50);
066 *     rect.setFill(Color.VIOLET);
067 *
068 *     final Duration SEC_2 = Duration.millis(2000);
069 *     final Duration SEC_3 = Duration.millis(3000);
070 *
071 *     FadeTransition ft = new FadeTransition(SEC_3);
072 *     ft.setFromValue(1.0f);
073 *     ft.setToValue(0.3f);
074 *     ft.setCycleCount(2f);
075 *     ft.setAutoReverse(true);
076 *     TranslateTransition tt = new TranslateTransition(SEC_2);
077 *     tt.setFromX(-100f);
078 *     tt.setToX(100f);
079 *     tt.setCycleCount(2f);
080 *     tt.setAutoReverse(true);
081 *     RotateTransition rt = new RotateTransition(SEC_3);
082 *     rt.setByAngle(180f);
083 *     rt.setCycleCount(4f);
084 *     rt.setAutoReverse(true);
085 *     ScaleTransition st = new ScaleTransition(SEC_2);
086 *     st.setByX(1.5f);
087 *     st.setByY(1.5f);
088 *     st.setCycleCount(2f);
089 *     st.setAutoReverse(true);
090 *
091 *     ParallelTransition pt = new ParallelTransition(rect, ft, tt, rt, st);
092 *     pt.play();
093 * </code>
094 * </pre>
095 *
096 * @see Transition
097 * @see Animation
098 *
099 */
100public final class ParallelTransition extends Transition {
101
102    private static final Animation[] EMPTY_ANIMATION_ARRAY = new Animation[0];
103    private static final double EPSILON = 1e-12;
104
105    private Animation[] cachedChildren = EMPTY_ANIMATION_ARRAY;
106    private long[] durations;
107    private long[] delays;
108    private double[] rates;
109    private long[] offsetTicks;
110    private boolean[] forceChildSync;
111    private long oldTicks;
112    private long cycleTime;
113    private boolean childrenChanged = true;
114    private boolean toggledRate;
115
116    private final InvalidationListener childrenListener = new InvalidationListener() {
117        @Override
118        public void invalidated(Observable observable) {
119            childrenChanged = true;
120            if (getStatus() == Status.STOPPED) {
121                setCycleDuration(computeCycleDuration());
122            }
123        }
124    };
125
126    private final ChangeListener<Number> rateListener = new ChangeListener<Number>() {
127
128        @Override
129        public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
130            if (oldValue.doubleValue() * newValue.doubleValue() < 0) {
131                for (int i = 0; i < cachedChildren.length; ++i) {
132                    Animation child = cachedChildren[i];
133                    child.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate()));
134                }
135                toggledRate = true;
136            }
137        }
138
139    };
140    /**
141     * This {@link javafx.scene.Node} is used in all child {@link Transition
142     * Transitions}, that do not define a target {@code Node} themselves. This
143     * can be used if a number of {@code Transitions} should be applied to a
144     * single {@code Node}.
145     * <p>
146     * It is not possible to change the target {@code node} of a running
147     * {@code Transition}. If the value of {@code node} is changed for a running
148     * {@code Transition}, the animation has to be stopped and started again to
149     * pick up the new value.
150     */
151    private ObjectProperty<Node> node;
152    private static final Node DEFAULT_NODE = null;
153
154    public final void setNode(Node value) {
155        if ((node != null) || (value != null /* DEFAULT_NODE */)) {
156            nodeProperty().set(value);
157        }
158    }
159
160    public final Node getNode() {
161        return (node == null)? DEFAULT_NODE : node.get();
162    }
163
164    public final ObjectProperty<Node> nodeProperty() {
165        if (node == null) {
166            node = new javafx.beans.property.SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE);
167        }
168        return node;
169    }
170
171    private final Set<Animation> childrenSet = new HashSet<Animation>();
172
173    private final ObservableList<Animation> children = new VetoableListDecorator<Animation>(new TrackableObservableList<Animation>() {
174        @Override
175        protected void onChanged(Change<Animation> c) {
176            while (c.next()) {
177                for (final Animation animation : c.getRemoved()) {
178                    animation.parent = null;
179                    animation.rateProperty().removeListener(childrenListener);
180                    animation.totalDurationProperty().removeListener(childrenListener);
181                    animation.delayProperty().removeListener(childrenListener);
182                }
183                for (final Animation animation : c.getAddedSubList()) {
184                    animation.parent = ParallelTransition.this;
185                    animation.rateProperty().addListener(childrenListener);
186                    animation.totalDurationProperty().addListener(childrenListener);
187                    animation.delayProperty().addListener(childrenListener);
188                }
189            }
190            childrenListener.invalidated(children);
191        }
192    }) {
193
194        @Override
195        protected void onProposedChange(List<Animation> toBeAdded, int... indexes) {
196            IllegalArgumentException exception = null;
197            for (int i = 0; i < indexes.length; i+=2) {
198                for (int idx = indexes[i]; idx < indexes[i+1]; ++idx) {
199                    childrenSet.remove(children.get(idx));
200                }
201            }
202            for (Animation child : toBeAdded) {
203                if (child == null) {
204                    exception = new IllegalArgumentException("Child cannot be null");
205                    break;
206                }
207                if (!childrenSet.add(child)) {
208                    exception = new IllegalArgumentException("Attempting to add a duplicate to the list of children");
209                    break;
210                }
211                if (checkCycle(child, ParallelTransition.this)) {
212                    exception = new IllegalArgumentException("This change would create cycle");
213                    break;
214                }
215            }
216
217            if (exception != null) {
218                childrenSet.clear();
219                childrenSet.addAll(children);
220                throw exception;
221            }
222        }
223
224    };
225
226    private static boolean checkCycle(Animation child, Animation parent) {
227        Animation a = parent;
228        while (a != child) {
229            if (a.parent != null) {
230                a = a.parent;
231            } else {
232                return false;
233            }
234        }
235        return true;
236    }
237
238    /**
239     * A list of {@link javafx.animation.Animation Animations} that will be
240     * played sequentially.
241     * <p>
242     * It is not possible to change the children of a running
243     * {@code ParallelTransition}. If the children are changed for a running
244     * {@code ParallelTransition}, the animation has to be stopped and started
245     * again to pick up the new value.
246     *
247     * @return the list of {@link javafx.animation.Animation Animations}
248     */
249    public final ObservableList<Animation> getChildren() {
250        return children;
251    }
252
253    /**
254     * The constructor of {@code ParallelTransition}.
255     *
256     * @param node
257     *            The target {@link javafx.scene.Node} to be used in child
258     *            {@link Transition Transitions} that have no {@code Node} specified
259     *            themselves
260     * @param children
261     *            The child {@link javafx.animation.Animation Animations} of
262     *            this {@code ParallelTransition}
263     */
264    public ParallelTransition(Node node, Animation... children) {
265        setInterpolator(Interpolator.LINEAR);
266        setNode(node);
267        getChildren().setAll(children);
268    }
269
270    /**
271     * The constructor of {@code ParallelTransition}.
272     *
273     * @param children
274     *            The child {@link javafx.animation.Animation Animations} of
275     *            this {@code ParallelTransition}
276     */
277    public ParallelTransition(Animation... children) {
278        this(null, children);
279    }
280
281    /**
282     * The constructor of {@code ParallelTransition}.
283     *
284     * @param node
285     *            The target {@link javafx.scene.Node} to be used in child
286     *            {@link Transition Transitions} that have no {@code Node} specified
287     *            themselves
288     */
289    public ParallelTransition(Node node) {
290        setInterpolator(Interpolator.LINEAR);
291        setNode(node);
292    }
293
294    /**
295     * The constructor of {@code ParallelTransition}.
296     */
297    public ParallelTransition() {
298        this((Node) null);
299    }
300
301    // For testing purposes
302    ParallelTransition(AbstractMasterTimer timer) {
303        super(timer);
304        setInterpolator(Interpolator.LINEAR);
305    }
306
307    /**
308     * {@inheritDoc}
309     */
310    @Override
311    protected Node getParentTargetNode() {
312        final Node node = getNode();
313        return (node != null) ? node : (parent != null && parent instanceof Transition) ?
314                ((Transition)parent).getParentTargetNode() : null;
315    }
316
317    private Duration computeCycleDuration() {
318        Duration maxTime = Duration.ZERO;
319        for (final Animation animation : getChildren()) {
320            final double absRate = Math.abs(animation.getRate());
321            final Duration totalDuration = (absRate < EPSILON) ?
322                    animation.getTotalDuration() : animation.getTotalDuration().divide(absRate);
323            final Duration childDuration = totalDuration.add(animation.getDelay());
324            if (childDuration.isIndefinite()) {
325                return Duration.INDEFINITE;
326            } else {
327                if (childDuration.greaterThan(maxTime)) {
328                    maxTime = childDuration;
329                }
330            }
331        }
332        return maxTime;
333    }
334
335    private double calculateFraction(long currentTicks, long cycleTicks) {
336        final double frac = (double) currentTicks / cycleTicks;
337        return (frac <= 0.0) ? 0 : (frac >= 1.0) ? 1.0 : frac;
338    }
339
340    private boolean startChild(Animation child, int index) {
341        final boolean forceSync = forceChildSync[index];
342        if (child.impl_startable(forceSync)) {
343            child.clipEnvelope.setRate(rates[index] * Math.signum(getCurrentRate()));
344            child.impl_start(forceSync);
345            forceChildSync[index] = false;
346            return true;
347        }
348        return false;
349    }
350
351    @Override
352    void impl_sync(boolean forceSync) {
353        super.impl_sync(forceSync);
354        if ((forceSync && childrenChanged) || (durations == null)) {
355            cachedChildren = getChildren().toArray(EMPTY_ANIMATION_ARRAY);
356            final int n = cachedChildren.length;
357            durations = new long[n];
358            delays = new long[n];
359            rates = new double[n];
360            offsetTicks = new long[n];
361            forceChildSync = new boolean[n];
362            cycleTime = 0;
363            int i = 0;
364            for (final Animation animation : cachedChildren) {
365                rates[i] = Math.abs(animation.getRate());
366                if (rates[i] < EPSILON) {
367                    rates[i] = 1;
368                }
369                durations[i] = fromDuration(animation.getTotalDuration(), rates[i]);
370                delays[i] = fromDuration(animation.getDelay());
371                cycleTime = Math.max(cycleTime, add(durations[i], delays[i]));
372                forceChildSync[i] = true;
373                i++;
374            }
375            childrenChanged = false;
376        } else if (forceSync) {
377            final int n = forceChildSync.length;
378            for (int i=0; i<n; i++) {
379                forceChildSync[i] = true;
380            }
381        }
382    }
383
384    @Override
385    void impl_pause() {
386        super.impl_pause();
387        for (final Animation animation : cachedChildren) {
388            if (animation.getStatus() == Status.RUNNING) {
389                animation.impl_pause();
390            }
391        }
392    }
393
394    @Override
395    void impl_resume() {
396        super.impl_resume();
397        int i = 0;
398        for (final Animation animation : cachedChildren) {
399            if (animation.getStatus() == Status.PAUSED) {
400                animation.impl_resume();
401                animation.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate()));
402            }
403            i++;
404        }
405    }
406
407    @Override
408    void impl_start(boolean forceSync) {
409        super.impl_start(forceSync);
410        toggledRate = false;
411        rateProperty().addListener(rateListener);
412        double curRate = getCurrentRate();
413        final long currentTicks = TickCalculation.fromDuration(getCurrentTime());
414        if (curRate < 0) {
415            jumpToEnd();
416            if (currentTicks < cycleTime) {
417                impl_jumpTo(currentTicks, cycleTime, false);
418            }
419        } else {
420            jumpToStart();
421            if (currentTicks > 0) {
422                impl_jumpTo(currentTicks, cycleTime, false);
423            }
424        }
425    }
426
427    @Override
428    void impl_stop() {
429        super.impl_stop();
430        for (final Animation animation : cachedChildren) {
431            if (animation.getStatus() != Status.STOPPED) {
432                animation.impl_stop();
433            }
434        }
435        if (childrenChanged) {
436            setCycleDuration(computeCycleDuration());
437        }
438        rateProperty().removeListener(rateListener);
439    }
440
441
442    /**
443     * @treatAsPrivate implementation detail
444     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
445     */
446    @Deprecated
447    @Override public void impl_playTo(long currentTicks, long cycleTicks) {
448        impl_setCurrentTicks(currentTicks);
449        final double frac = calculateFraction(currentTicks, cycleTicks);
450        final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
451        if (toggledRate) {
452            for (int i = 0; i < cachedChildren.length; ++i) {
453                if (cachedChildren[i].getStatus() == Status.RUNNING) {
454                    offsetTicks[i] -= Math.signum(getCurrentRate()) * (durations[i] - 2 * (oldTicks - delays[i]));
455                }
456            }
457            toggledRate = false;
458        }
459        if (getCurrentRate() > 0) {
460            int i = 0;
461            for (final Animation animation : cachedChildren) {
462                if ((newTicks >= delays[i]) && ((oldTicks <= delays[i]) ||
463                        ((newTicks < add(delays[i], durations[i])) && (animation.getStatus() == Status.STOPPED)))) {
464                    final boolean enteringCycle = oldTicks <= delays[i];
465                    if (startChild(animation, i)) {
466                        animation.clipEnvelope.jumpTo(0);
467                    } else {
468                        if (enteringCycle) {
469                            final EventHandler<ActionEvent> handler = animation.getOnFinished();
470                            if (handler != null) {
471                                handler.handle(new ActionEvent(this, null));
472                            }
473                        }
474                        continue;
475                    }
476                }
477                if (newTicks >= add(durations[i], delays[i])) {
478                    if (animation.getStatus() == Status.RUNNING) {
479                        animation.impl_timePulse(sub(durations[i], offsetTicks[i]));
480                        offsetTicks[i] = 0;
481                    }
482                } else if (newTicks > delays[i]) {
483                    animation.impl_timePulse(sub(newTicks - delays[i], offsetTicks[i]));
484                }
485                i++;
486            }
487        } else {
488            int i = 0;
489            for (final Animation animation : cachedChildren) {
490                if (newTicks < add(durations[i], delays[i])) {
491                    if ((oldTicks >= add(durations[i], delays[i])) || ((newTicks >= delays[i]) && (animation.getStatus() == Status.STOPPED))){
492                        final boolean enteringCycle = oldTicks >= add(durations[i], delays[i]);
493                        if (startChild(animation, i)) {
494                            animation.clipEnvelope.jumpTo(Math.round(durations[i] * rates[i]));
495                        } else {
496                            if (enteringCycle) {
497                                final EventHandler<ActionEvent> handler = animation.getOnFinished();
498                                if (handler != null) {
499                                    handler.handle(new ActionEvent(this, null));
500                                }
501                            }
502                            continue;
503                        }
504                    }
505                    if (newTicks <= delays[i]) {
506                        if (animation.getStatus() == Status.RUNNING) {
507                            animation.impl_timePulse(sub(durations[i], offsetTicks[i]));
508                            offsetTicks[i] = 0;
509                        }
510                    } else {
511                        animation.impl_timePulse(sub( add(durations[i], delays[i]) - newTicks, offsetTicks[i]));
512                    }
513                }
514                i++;
515            }
516        }
517        oldTicks = newTicks;
518    }
519
520    /**
521     * @treatAsPrivate implementation detail
522     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
523     */
524    @Deprecated
525    @Override public void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump) {
526        impl_setCurrentTicks(currentTicks);
527        if (getStatus() == Status.STOPPED && !forceJump) {
528            return;
529        }
530        impl_sync(false);
531        final double frac = calculateFraction(currentTicks, cycleTicks);
532        final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
533        int i = 0;
534        for (final Animation animation : cachedChildren) {
535            final Status status = animation.getStatus();
536            if (newTicks <= delays[i]) {
537                offsetTicks[i] = 0;
538                if (status != Status.STOPPED) {
539                    animation.clipEnvelope.jumpTo(0);
540                    animation.impl_stop();
541                } else if(TickCalculation.fromDuration(animation.getCurrentTime()) != 0) {
542                    animation.impl_jumpTo(0, durations[i], true);
543                }
544            } else if (newTicks >= add(durations[i], delays[i])) {
545                offsetTicks[i] = 0;
546                if (status != Status.STOPPED) {
547                    animation.clipEnvelope.jumpTo(Math.round(durations[i] * rates[i]));
548                    animation.impl_stop();
549                } else if (TickCalculation.fromDuration(animation.getCurrentTime()) != durations[i]) {
550                    animation.impl_jumpTo(durations[i], durations[i], true);
551                }
552            } else {
553                if (status == Status.STOPPED) {
554                    startChild(animation, i);
555                    if (getStatus() == Status.PAUSED) {
556                        animation.impl_pause();
557                    }
558
559                    offsetTicks[i] = (getCurrentRate() > 0)? newTicks - delays[i] : add(durations[i], delays[i]) - newTicks;
560                } else if (status == Status.PAUSED) {
561                    offsetTicks[i] += (newTicks - oldTicks) * Math.signum(this.clipEnvelope.getCurrentRate());
562                } else {
563                    offsetTicks[i] += (getCurrentRate() > 0) ? newTicks - oldTicks : oldTicks - newTicks;
564                }
565                animation.clipEnvelope.jumpTo(Math.round(sub(newTicks, delays[i]) * rates[i]));
566            }
567            i++;
568        }
569        oldTicks = newTicks;
570    }
571
572    /**
573     * {@inheritDoc}
574     */
575    @Override
576    protected void interpolate(double frac) {
577        // no-op
578    }
579
580    private void jumpToEnd() {
581        for (int i = 0 ; i < cachedChildren.length; ++i) {
582            cachedChildren[i].impl_jumpTo(durations[i], durations[i], true);
583        }
584    }
585
586    private void jumpToStart() {
587        for (int i = cachedChildren.length - 1 ; i >= 0; --i) {
588            cachedChildren[i].impl_jumpTo(0, durations[i], true);
589        }
590    }
591
592}