Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 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 java.util.Arrays;
032
033import javafx.beans.InvalidationListener;
034import javafx.beans.Observable;
035import javafx.beans.property.ObjectProperty;
036import javafx.beans.property.SimpleObjectProperty;
037import javafx.collections.ListChangeListener.Change;
038import javafx.collections.ObservableList;
039import javafx.event.ActionEvent;
040import javafx.event.EventHandler;
041import javafx.scene.Node;
042import javafx.util.Duration;
043
044import com.sun.javafx.collections.TrackableObservableList;
045import com.sun.javafx.collections.VetoableListDecorator;
046import com.sun.scenario.animation.AbstractMasterTimer;
047import java.util.HashSet;
048import java.util.List;
049import java.util.Set;
050import javafx.beans.value.ChangeListener;
051import javafx.beans.value.ObservableValue;
052
053/**
054 * This {@link Transition} plays a list of {@link javafx.animation.Animation
055 * Animations} in sequential order.
056 * <p>
057 * Children of this {@code Transition} inherit {@link #nodeProperty() node}, if their
058 * {@code node} property is not specified.
059 * 
060 * <p>
061 * Code Segment Example:
062 * </p>
063 * 
064 * <pre>
065 * <code>
066 *     Rectangle rect = new Rectangle (100, 40, 100, 100);
067 *     rect.setArcHeight(50);
068 *     rect.setArcWidth(50);
069 *     rect.setFill(Color.VIOLET);
070 * 
071 *     final Duration SEC_2 = Duration.millis(2000);
072 *     final Duration SEC_3 = Duration.millis(3000);
073 * 
074 *     PauseTransition pt = new PauseTransition(Duration.millis(1000));
075 *     FadeTransition ft = new FadeTransition(SEC_3);
076 *     ft.setFromValue(1.0f);
077 *     ft.setToValue(0.3f);
078 *     ft.setCycleCount(2f);
079 *     ft.setAutoReverse(true);
080 *     TranslateTransition tt = new TranslateTransition(SEC_2);
081 *     tt.setFromX(-100f);
082 *     tt.setToX(100f);
083 *     tt.setCycleCount(2f);
084 *     tt.setAutoReverse(true);
085 *     RotateTransition rt = new RotateTransition(SEC_3);
086 *     rt.setByAngle(180f);
087 *     rt.setCycleCount(4f);
088 *     rt.setAutoReverse(true);
089 *     ScaleTransition st = new ScaleTransition(SEC_2);
090 *     st.setByX(1.5f);
091 *     st.setByY(1.5f);
092 *     st.setCycleCount(2f);
093 *     st.setAutoReverse(true);
094 * 
095 *     SequentialTransition seqT = new SequentialTransition (rect, pt, ft, tt, rt, st);
096 *     seqT.play();
097 * </code>
098 * </pre>
099 * 
100 * @see Transition
101 * @see Animation
102 * 
103 */
104public final class SequentialTransition extends Transition {
105
106    private static final Animation[] EMPTY_ANIMATION_ARRAY = new Animation[0];
107    private static final int BEFORE = -1;
108    private static final double EPSILON = 1e-12;
109
110    private Animation[] cachedChildren = EMPTY_ANIMATION_ARRAY;
111    private long[] startTimes;
112    private long[] durations;
113    private long[] delays;
114    private double[] rates;
115    private boolean[] forceChildSync;
116    private int end;
117    private int curIndex = BEFORE;
118    private long oldTicks = 0L;
119    private long offsetTicks;
120    private boolean childrenChanged = true;
121    private boolean toggledRate;
122
123    private final InvalidationListener childrenListener = new InvalidationListener() {
124        @Override
125        public void invalidated(Observable observable) {
126            childrenChanged = true;
127            if (getStatus() == Status.STOPPED) {
128                setCycleDuration(computeCycleDuration());
129            }
130        }
131    };
132
133    private final ChangeListener<Number> rateListener = new ChangeListener<Number>() {
134
135        @Override
136        public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
137            if (oldValue.doubleValue() * newValue.doubleValue() < 0) {
138                for (int i = 0; i < cachedChildren.length; ++i) {
139                    Animation child = cachedChildren[i];
140                    child.clipEnvelope.setRate(rates[i] * Math.signum(getCurrentRate()));
141                }
142                toggledRate = true;
143            }
144        }
145
146    };
147    
148    /**
149     * This {@link javafx.scene.Node} is used in all child {@link Transition
150     * Transitions}, that do not define a target {@code Node} themselves. This
151     * can be used if a number of {@code Transitions} should be applied to a
152     * single {@code Node}.
153     * <p>
154     * It is not possible to change the target {@code node} of a running
155     * {@code Transition}. If the value of {@code node} is changed for a
156     * running {@code Transition}, the animation has to be stopped and started again to
157     * pick up the new value.
158     */
159    private ObjectProperty<Node> node;
160    private static final Node DEFAULT_NODE = null;
161
162    public final void setNode(Node value) {
163        if ((node != null) || (value != null /* DEFAULT_NODE */)) {
164            nodeProperty().set(value);
165        }
166    }
167
168    public final Node getNode() {
169        return (node == null)? DEFAULT_NODE : node.get();
170    }
171
172    public final ObjectProperty<Node> nodeProperty() {
173        if (node == null) {
174            node = new SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE);
175        }
176        return node;
177    }
178
179    private final Set<Animation> childrenSet = new HashSet<Animation>();
180    
181    private final ObservableList<Animation> children = new VetoableListDecorator<Animation>(new TrackableObservableList<Animation>() {
182        @Override
183        protected void onChanged(Change<Animation> c) {
184            while (c.next()) {
185                for (final Animation animation : c.getRemoved()) {
186                    animation.parent = null;
187                    animation.rateProperty().removeListener(childrenListener);
188                    animation.totalDurationProperty().removeListener(childrenListener);
189                    animation.delayProperty().removeListener(childrenListener);
190                }
191                for (final Animation animation : c.getAddedSubList()) {
192                    animation.parent = SequentialTransition.this;
193                    animation.rateProperty().addListener(childrenListener);
194                    animation.totalDurationProperty().addListener(childrenListener);
195                    animation.delayProperty().addListener(childrenListener);
196                }
197            }
198            childrenListener.invalidated(children);
199        }
200    }) {
201
202        @Override
203        protected void onProposedChange(List<Animation> toBeAdded, int... indexes) {
204            IllegalArgumentException exception = null;
205            for (int i = 0; i < indexes.length; i+=2) {
206                for (int idx = indexes[i]; idx < indexes[i+1]; ++idx) {
207                    childrenSet.remove(children.get(idx));
208                }
209            }
210            for (Animation child : toBeAdded) {
211                if (child == null) {
212                    exception = new IllegalArgumentException("Child cannot be null");
213                    break;
214                }
215                if (!childrenSet.add(child)) {
216                    exception = new IllegalArgumentException("Attempting to add a duplicate to the list of children");
217                    break;
218                }
219                if (checkCycle(child, SequentialTransition.this)) {
220                    exception = new IllegalArgumentException("This change would create cycle");
221                    break;
222                }
223            }
224
225            if (exception != null) {
226                childrenSet.clear();
227                childrenSet.addAll(children);
228                throw exception;
229            }
230        }
231
232    };
233
234    private static boolean checkCycle(Animation child, Animation parent) {
235        Animation a = parent;
236        while (a != child) {
237            if (a.parent != null) {
238                a = a.parent;
239            } else {
240                return false;
241            }
242        }
243        return true;
244    }
245
246    /**
247     * A list of {@link javafx.animation.Animation Animations} that will be
248     * played sequentially.
249     * <p>
250     * It is not possible to change the children of a running
251     * {@code SequentialTransition}. If the children are changed for a running
252     * {@code SequentialTransition}, the animation has to be stopped and started
253     * again to pick up the new value.
254     */
255    public final ObservableList<Animation> getChildren() {
256        return children;
257    }
258
259    /**
260     * The constructor of {@code SequentialTransition}.
261     * 
262     * @param node
263     *            The target {@link javafx.scene.Node} to be used in child
264     *            {@link Transition Transitions} that have no {@code Node} specified
265     *            themselves
266     * @param children
267     *            The child {@link javafx.animation.Animation Animations} of
268     *            this {@code SequentialTransition}
269     */
270    public SequentialTransition(Node node, Animation... children) {
271        setInterpolator(Interpolator.LINEAR);
272        setNode(node);
273        getChildren().setAll(children);
274    }
275
276    /**
277     * The constructor of {@code SequentialTransition}.
278     * 
279     * @param children
280     *            The child {@link javafx.animation.Animation Animations} of
281     *            this {@code SequentialTransition}
282     */
283    public SequentialTransition(Animation... children) {
284        this(null, children);
285    }
286
287    /**
288     * The constructor of {@code SequentialTransition}.
289     * 
290     * @param node
291     *            The target {@link javafx.scene.Node} to be used in child
292     *            {@link Transition Transitions} that have no {@code Node} specified
293     *            themselves
294     */
295    public SequentialTransition(Node node) {
296        setInterpolator(Interpolator.LINEAR);
297        setNode(node);
298    }
299
300    /**
301     * The constructor of {@code SequentialTransition}.
302     */
303    public SequentialTransition() {
304        this((Node) null);
305    }
306
307    // For testing purposes
308    SequentialTransition(AbstractMasterTimer timer) {
309        super(timer);
310        setInterpolator(Interpolator.LINEAR);
311    }
312
313    /**
314     * {@inheritDoc}
315     */
316    @Override
317    protected Node getParentTargetNode() {
318        final Node _node = getNode();
319        return (_node != null) ? _node : ((parent != null && parent instanceof Transition) ?
320                ((Transition)parent).getParentTargetNode() : null);
321    }
322
323    private Duration computeCycleDuration() {
324        Duration currentDur = Duration.ZERO;
325
326        for (final Animation animation : getChildren()) {
327            currentDur = currentDur.add(animation.getDelay());
328            final double absRate = Math.abs(animation.getRate());
329            currentDur = currentDur.add((absRate < EPSILON) ? 
330                    animation.getTotalDuration() : animation.getTotalDuration().divide(absRate));
331            if (currentDur.isIndefinite()) {
332                break;
333            }
334        }
335        return currentDur;
336    }
337
338    private double calculateFraction(long currentTicks, long cycleTicks) {
339        final double frac = (double) currentTicks / cycleTicks;
340        return (frac <= 0.0) ? 0 : (frac >= 1.0) ? 1.0 : frac;
341    }
342
343    private int findNewIndex(long ticks) {
344        if ((curIndex != BEFORE) 
345                && (curIndex != end)
346                && (startTimes[curIndex] <= ticks)
347                && (ticks <= startTimes[curIndex + 1])) {
348            return curIndex;
349        }
350
351        final boolean indexUndefined = (curIndex == BEFORE) || (curIndex == end);
352        final int fromIndex = (indexUndefined || (ticks < oldTicks)) ? 0 : curIndex + 1;
353        final int toIndex = (indexUndefined || (oldTicks < ticks)) ? end : curIndex;
354        final int index = Arrays.binarySearch(startTimes, fromIndex, toIndex, ticks);
355        return (index < 0) ? -index - 2 : (index > 0) ? index - 1 : 0;
356    }
357
358    @Override
359    void impl_sync(boolean forceSync) {
360        super.impl_sync(forceSync);
361        
362        if ((forceSync && childrenChanged) || (startTimes == null)) {
363            cachedChildren = getChildren().toArray(EMPTY_ANIMATION_ARRAY);
364            end = cachedChildren.length;
365            startTimes = new long[end + 1];
366            durations = new long[end];
367            delays = new long[end];
368            rates = new double[end];
369            forceChildSync = new boolean[end];
370            long cycleTicks = 0L;
371            int i = 0;
372            for (final Animation animation : cachedChildren) {
373                startTimes[i] = cycleTicks;
374                rates[i] = Math.abs(animation.getRate());
375                if (rates[i] < EPSILON) {
376                    rates[i] = 1;
377                }
378                durations[i] = fromDuration(animation.getTotalDuration(), rates[i]);
379                delays[i] = fromDuration(animation.getDelay());
380                if ((durations[i] == Long.MAX_VALUE) || (delays[i] == Long.MAX_VALUE) || (cycleTicks == Long.MAX_VALUE)) {
381                    cycleTicks = Long.MAX_VALUE;
382                } else {
383                    cycleTicks = add(cycleTicks, add(durations[i], delays[i]));
384                }
385                forceChildSync[i] = true;
386                i++;
387            }
388            startTimes[end] = cycleTicks;
389            childrenChanged = false;
390        } else if (forceSync) {
391            final int n = forceChildSync.length;
392            for (int i=0; i<n; i++) {
393                forceChildSync[i] = true;
394            }
395        }
396    }
397
398    @Override
399    void impl_start(boolean forceSync) {
400        super.impl_start(forceSync);
401        toggledRate = false;
402        rateProperty().addListener(rateListener);
403        offsetTicks = 0L;
404        double curRate = getCurrentRate();
405        final long currentTicks = TickCalculation.fromDuration(getCurrentTime());
406        if (curRate < 0) {
407            jumpToEnd();
408            curIndex = end;
409            if (currentTicks < startTimes[end]) {
410                impl_jumpTo(currentTicks, startTimes[end], false);
411            }
412        } else {
413            jumpToBefore();
414            curIndex = BEFORE;
415            if (currentTicks > 0) {
416                impl_jumpTo(currentTicks, startTimes[end], false);
417            }
418        }
419    }
420
421    @Override
422    void impl_pause() {
423        super.impl_pause();
424        if ((curIndex != BEFORE) && (curIndex != end)) {
425            final Animation current = cachedChildren[curIndex];
426            if (current.getStatus() == Status.RUNNING) {
427                current.impl_pause();
428            }
429        }
430    }
431
432    @Override
433    void impl_resume() {
434        super.impl_resume();
435        if ((curIndex != BEFORE) && (curIndex != end)) {
436            final Animation current = cachedChildren[curIndex];
437            if (current.getStatus() == Status.PAUSED) {
438                current.impl_resume();
439                current.clipEnvelope.setRate(rates[curIndex] * Math.signum(getCurrentRate()));
440            }
441        }
442    }
443
444    @Override
445    void impl_stop() {
446        super.impl_stop();
447        if ((curIndex != BEFORE) && (curIndex != end)) {
448            final Animation current = cachedChildren[curIndex];
449            if (current.getStatus() != Status.STOPPED) {
450                current.impl_stop();
451            }
452        }
453        if (childrenChanged) {
454            setCycleDuration(computeCycleDuration());
455        }
456        rateProperty().removeListener(rateListener);
457    }
458
459    private boolean startChild(Animation child, int index) {
460        final boolean forceSync = forceChildSync[index];
461        if (child.impl_startable(forceSync)) {
462            child.clipEnvelope.setRate(rates[index] * Math.signum(getCurrentRate()));
463            child.impl_start(forceSync);
464            forceChildSync[index] = false;
465            return true;
466        }
467        return false;
468    }
469
470    @Override void impl_playTo(long currentTicks, long cycleTicks) {
471        impl_setCurrentTicks(currentTicks);
472        final double frac = calculateFraction(currentTicks, cycleTicks);
473        final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
474        final int newIndex = findNewIndex(newTicks);
475        final Animation current = ((curIndex == BEFORE) || (curIndex == end)) ? null : cachedChildren[curIndex];
476        if (toggledRate) {
477            if (current != null && current.getStatus() == Status.RUNNING) {
478                offsetTicks -= Math.signum(getCurrentRate()) * (durations[curIndex] - 2 * (oldTicks - delays[curIndex] - startTimes[curIndex]));
479            }
480            toggledRate = false;
481        }
482        if (curIndex == newIndex) {
483            if (getCurrentRate() > 0) {
484                final long currentDelay = add(startTimes[curIndex], delays[curIndex]);
485                if (newTicks >= currentDelay) {
486                    if ((oldTicks <= currentDelay) || (current.getStatus() == Status.STOPPED)) {
487                        final boolean enteringCycle = oldTicks <= currentDelay;
488                        if (enteringCycle) {
489                            current.clipEnvelope.jumpTo(0);
490                        }
491                        if (!startChild(current, curIndex)) {
492                            if (enteringCycle) {
493                                final EventHandler<ActionEvent> handler = current.getOnFinished();
494                                if (handler != null) {
495                                    handler.handle(new ActionEvent(this, null));
496                                }
497                            }
498                            oldTicks = newTicks;
499                            return;
500                        }
501                    }
502                    if (newTicks >= startTimes[curIndex+1]) {
503                        current.impl_timePulse(sub(durations[curIndex], offsetTicks));
504                        if (newTicks == cycleTicks) {
505                            curIndex = end;
506                        }
507                    } else {
508                        final long localTicks = sub(newTicks - currentDelay, offsetTicks);
509                        current.impl_timePulse(localTicks);
510                    }
511                }
512            } else { // getCurrentRate() < 0
513                final long currentDelay = add(startTimes[curIndex], delays[curIndex]);
514                if ((oldTicks >= startTimes[curIndex+1]) || ((oldTicks >= currentDelay) && (current.getStatus() == Status.STOPPED))){
515                    final boolean enteringCycle = oldTicks >= startTimes[curIndex+1];
516                    if (enteringCycle) {
517                        current.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
518                    }
519                    if (!startChild(current, curIndex)) {
520                        if (enteringCycle) {
521                            final EventHandler<ActionEvent> handler = current.getOnFinished();
522                            if (handler != null) {
523                                handler.handle(new ActionEvent(this, null));
524                            }
525                        }
526                        oldTicks = newTicks;
527                        return;
528                    }
529                }
530                if (newTicks <= currentDelay) {
531                    current.impl_timePulse(sub(durations[curIndex], offsetTicks));
532                    if (newTicks == 0) {
533                        curIndex = BEFORE;
534                    }
535                } else {
536                    final long localTicks = sub(startTimes[curIndex + 1] - newTicks, offsetTicks);
537                    current.impl_timePulse(localTicks);
538                }
539            }
540        } else { // curIndex != newIndex
541            if (curIndex < newIndex) {
542                if (current != null) {
543                    final long oldDelay = add(startTimes[curIndex], delays[curIndex]);
544                    if ((oldTicks <= oldDelay) || ((current.getStatus() == Status.STOPPED) && (oldTicks != startTimes[curIndex + 1]))) {
545                        final boolean enteringCycle = oldTicks <= oldDelay;
546                        if (enteringCycle) {
547                            current.clipEnvelope.jumpTo(0);
548                        }
549                        if (!startChild(current, curIndex)) {
550                            if (enteringCycle) {
551                                final EventHandler<ActionEvent> handler = current.getOnFinished();
552                                if (handler != null) {
553                                    handler.handle(new ActionEvent(this, null));
554                                }
555                            }
556                        }
557                    }
558                    if (current.getStatus() == Status.RUNNING) {
559                        current.impl_timePulse(sub(durations[curIndex], offsetTicks));
560                    }
561                    oldTicks = startTimes[curIndex + 1];
562                }
563                offsetTicks = 0;
564                curIndex++;
565                for (; curIndex < newIndex; curIndex++) {
566                    final Animation animation = cachedChildren[curIndex];
567                    animation.clipEnvelope.jumpTo(0);
568                    if (startChild(animation, curIndex)) {
569                        animation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
570                    } else {
571                        final EventHandler<ActionEvent> handler = animation.getOnFinished();
572                        if (handler != null) {
573                            handler.handle(new ActionEvent(this, null));
574                        }
575                    }
576                    oldTicks = startTimes[curIndex + 1];
577                }
578                final Animation newAnimation = cachedChildren[curIndex];
579                newAnimation.clipEnvelope.jumpTo(0);
580                if (startChild(newAnimation, curIndex)) {
581                    if (newTicks >= startTimes[curIndex+1]) {
582                        newAnimation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
583                        if (newTicks == cycleTicks) {
584                            curIndex = end;
585                        }
586                    } else {
587                        final long localTicks = sub(newTicks, add(startTimes[curIndex], delays[curIndex]));
588                        newAnimation.impl_timePulse(localTicks);
589                    }
590                } else {
591                    final EventHandler<ActionEvent> handler = newAnimation.getOnFinished();
592                    if (handler != null) {
593                        handler.handle(new ActionEvent(this, null));
594                    }
595                }
596            } else {
597                if (current != null) {
598                    final long oldDelay = add(startTimes[curIndex], delays[curIndex]);
599                    if ((oldTicks >= startTimes[curIndex+1]) || ((oldTicks > oldDelay) && (current.getStatus() == Status.STOPPED))){
600                        final boolean enteringCycle = oldTicks >= startTimes[curIndex+1];
601                        if (enteringCycle) {
602                            current.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
603                        }
604                        if (!startChild(current, curIndex)) {
605                            if (enteringCycle) {
606                                final EventHandler<ActionEvent> handler = current.getOnFinished();
607                                if (handler != null) {
608                                    handler.handle(new ActionEvent(this, null));
609                                }
610                            }
611                        }
612                    }
613                    if (current.getStatus() == Status.RUNNING) {
614                        current.impl_timePulse(sub(durations[curIndex], offsetTicks));
615                    }
616                    oldTicks = startTimes[curIndex];
617                }
618                offsetTicks = 0;
619                curIndex--;
620                for (; curIndex > newIndex; curIndex--) {
621                    final Animation animation = cachedChildren[curIndex];
622                    animation.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
623                    if (startChild(animation, curIndex)) {
624                        animation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
625                    } else {
626                        final EventHandler<ActionEvent> handler = animation.getOnFinished();
627                        if (handler != null) {
628                            handler.handle(new ActionEvent(this, null));
629                        }
630                    }
631                    oldTicks = startTimes[curIndex];
632                }
633                final Animation newAnimation = cachedChildren[curIndex];
634                newAnimation.clipEnvelope.jumpTo(Math.round(durations[curIndex] * rates[curIndex]));
635                if (startChild(newAnimation, curIndex)) {
636                    if (newTicks <= add(startTimes[curIndex], delays[curIndex])) {
637                        newAnimation.impl_timePulse(durations[curIndex]); // No need to subtract offsetTicks ( == 0)
638                        if (newTicks == 0) {
639                            curIndex = BEFORE;
640                        }
641                    } else {
642                        final long localTicks = sub(startTimes[curIndex + 1], newTicks);
643                        newAnimation.impl_timePulse(localTicks);
644                    }
645                } else {
646                    final EventHandler<ActionEvent> handler = newAnimation.getOnFinished();
647                    if (handler != null) {
648                        handler.handle(new ActionEvent(this, null));
649                    }
650                }
651            }
652        }
653        oldTicks = newTicks;
654    }
655
656    @Override void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump) {
657        impl_setCurrentTicks(currentTicks);
658        final Status status = getStatus();
659
660        if (status == Status.STOPPED && !forceJump) {
661            return;
662        }
663        
664        impl_sync(false);
665        final double frac = calculateFraction(currentTicks, cycleTicks);
666        final long newTicks = Math.max(0, Math.min(getCachedInterpolator().interpolate(0, cycleTicks, frac), cycleTicks));
667        final int oldIndex = curIndex;
668        curIndex = findNewIndex(newTicks);
669        final Animation newAnimation = cachedChildren[curIndex];
670        final double currentRate = getCurrentRate();
671        if (curIndex != oldIndex) {
672            if (status != Status.STOPPED) {
673                if ((oldIndex != BEFORE) && (oldIndex != end)) {
674                    final Animation oldChild = cachedChildren[oldIndex];
675                    if (oldChild.getStatus() != Status.STOPPED) {
676                        cachedChildren[oldIndex].impl_stop();
677                    }
678                }
679                if (curIndex < oldIndex) {
680                    for (int i = oldIndex == end ? end - 1 : oldIndex; i > curIndex; --i) {
681                        cachedChildren[i].impl_jumpTo(0, durations[i], true);
682                    }
683                } else { //curIndex > oldIndex as curIndex != oldIndex
684                    for (int i = oldIndex == BEFORE? 0 : oldIndex; i < curIndex; ++i) {
685                        cachedChildren[i].impl_jumpTo(durations[i], durations[i], true);
686                    }
687                }
688                startChild(newAnimation, curIndex);
689                if (status == Status.PAUSED) {
690                    newAnimation.impl_pause();
691                }
692            }
693        }
694        if (oldIndex == curIndex) {
695            if (currentRate == 0) {
696                offsetTicks += (newTicks - oldTicks) * Math.signum(this.clipEnvelope.getCurrentRate());
697            } else {
698                offsetTicks += currentRate > 0 ? newTicks - oldTicks : oldTicks - newTicks;
699            }
700        } else {
701            if (currentRate == 0) {
702                if (this.clipEnvelope.getCurrentRate() > 0) {
703                    offsetTicks = newTicks - add(startTimes[curIndex], delays[curIndex]);
704                } else {
705                    offsetTicks = startTimes[curIndex] + durations[curIndex] - newTicks;
706                }
707            } else {
708                offsetTicks = currentRate > 0 ? newTicks - add(startTimes[curIndex], delays[curIndex]) : startTimes[curIndex + 1] - newTicks;
709            }
710        }
711        newAnimation.clipEnvelope.jumpTo(Math.round(sub(newTicks, add(startTimes[curIndex], delays[curIndex])) * rates[curIndex]));
712        oldTicks = newTicks;
713    }
714
715    private void jumpToEnd() {
716        for (int i = 0 ; i < end; ++i) {
717            cachedChildren[i].impl_jumpTo(durations[i], durations[i], true);
718        }
719    }
720
721    private void jumpToBefore() {
722        for (int i = end - 1 ; i >= 0; --i) {
723            cachedChildren[i].impl_jumpTo(0, durations[i], true);
724        }
725    }
726
727    /**
728     * {@inheritDoc}
729     */
730    @Override
731    protected void interpolate(double frac) {
732        // no-op
733    }
734
735}