/*
 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.scenario.animation.shared;

import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.util.Duration;
import com.sun.javafx.animation.TickCalculation;

An instance of ClipCore handles the core part of a clip. The functionality to react on a pulse-signal from the timer is implemented in two classes: ClipEnvelope and ClipCore. ClipEnvelope is responsible for the "loop-part" (keeping track of the number of cycles, handling the direction of the clip etc.). ClipCore takes care of the inner part (interpolating the values, triggering the action-functions, ...) Both classes have an abstract public definition and can only be created using the factory method create(). The intend is to provide a general implementation plus eventually some fast-track implementations for common use cases.
/** * An instance of ClipCore handles the core part of a clip. * * The functionality to react on a pulse-signal from the timer is implemented in * two classes: ClipEnvelope and ClipCore. ClipEnvelope is responsible for the * "loop-part" (keeping track of the number of cycles, handling the direction of * the clip etc.). ClipCore takes care of the inner part (interpolating the * values, triggering the action-functions, ...) * * Both classes have an abstract public definition and can only be created using * the factory method create(). The intend is to provide a general * implementation plus eventually some fast-track implementations for common use * cases. */
// @@OPT // - Use known information (kf) in visitKeyFrame to set values? public class TimelineClipCore { private static final int UNDEFINED_KEYFRAME = -1;
Note: this comparator imposes orderings that are inconsistent with equals.
/** * Note: this comparator imposes orderings that are inconsistent with * equals. */
private static final Comparator<KeyFrame> KEY_FRAME_COMPARATOR = (kf1, kf2) -> kf1.getTime().compareTo(kf2.getTime()); // The owner of this ClipCore Timeline timeline; // The sorted list of keyframes private KeyFrame[] keyFrames = new KeyFrame[0]; private long[] keyFrameTicks = new long[0]; // If there are no KeyFrames with onFinished handler then we can skip frames // This works because KeyFrame.onFinished is final private boolean canSkipFrames = true; private ClipInterpolator clipInterpolator; public TimelineClipCore(Timeline timeline) { this.timeline = timeline; this.clipInterpolator = ClipInterpolator.create(keyFrames, keyFrameTicks); }
Changes the keyframes.
/** * Changes the keyframes. */
public Duration setKeyFrames(Collection<KeyFrame> keyFrames) { final int n = keyFrames.size(); final KeyFrame[] sortedKeyFrames = new KeyFrame[n]; keyFrames.toArray(sortedKeyFrames); Arrays.sort(sortedKeyFrames, KEY_FRAME_COMPARATOR); canSkipFrames = true; this.keyFrames = sortedKeyFrames; keyFrameTicks = new long[n]; for (int i = 0; i < n; ++i) { keyFrameTicks[i] = TickCalculation.fromDuration(this.keyFrames[i].getTime()); if (canSkipFrames && this.keyFrames[i].getOnFinished() != null) { canSkipFrames = false; } } clipInterpolator = clipInterpolator.setKeyFrames(sortedKeyFrames, keyFrameTicks); return (n == 0) ? Duration.ZERO : sortedKeyFrames[n-1].getTime(); } public void notifyCurrentRateChanged() { // special case: if clip is toggled while stopped, we want to revisit // all key frames if (timeline.getStatus() != Status.RUNNING) { clearLastKeyFrame(); } }
This method is called if while visiting a keyframe of a timeline the time or rate are changed, or if the timeline is stopped. In these cases visiting the keyframes must be aborted.
/** * This method is called if while visiting a keyframe of a timeline the time * or rate are changed, or if the timeline is stopped. In these cases * visiting the keyframes must be aborted. */
public void abort() { aborted = true; } private boolean aborted = false; // The index of the keyframe that was visited last private int lastKF = UNDEFINED_KEYFRAME; // The position where clip currently stands private long curTicks = 0; private void clearLastKeyFrame() { lastKF = UNDEFINED_KEYFRAME; } public void jumpTo(long ticks, boolean forceJump) { lastKF = UNDEFINED_KEYFRAME; curTicks = ticks; if (timeline.getStatus() != Status.STOPPED || forceJump) { if (forceJump) { clipInterpolator.validate(false); } clipInterpolator.interpolate(ticks); } } public void start(boolean forceSync) { clearLastKeyFrame(); clipInterpolator.validate(forceSync); if (curTicks > 0) { clipInterpolator.interpolate(curTicks); } }
Called to visit all keyframes within a specified time-interval.
/** * Called to visit all keyframes within a specified time-interval. */
public void playTo(long ticks) { if (canSkipFrames) { clearLastKeyFrame(); setTime(ticks); clipInterpolator.interpolate(ticks); return; } aborted = false; final boolean forward = curTicks <= ticks; if (forward) { final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? 0 : (keyFrameTicks[lastKF] <= curTicks) ? lastKF + 1 : lastKF; final int toKF = keyFrames.length; for (int fi = fromKF; fi < toKF; fi++) { final long kfTicks = keyFrameTicks[fi]; if (kfTicks > ticks) { lastKF = fi - 1; break; } if (kfTicks >= curTicks) { visitKeyFrame(fi, kfTicks); if (aborted) { break; } } } } else { final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? keyFrames.length - 1 : (keyFrameTicks[lastKF] >= curTicks) ? lastKF - 1 : lastKF; for (int fi = fromKF; fi >= 0; fi--) { final long kfTicks = keyFrameTicks[fi]; if (kfTicks < ticks) { lastKF = fi + 1; break; } if (kfTicks <= curTicks) { visitKeyFrame(fi, kfTicks); if (aborted) { break; } } } } if (!aborted && ((lastKF == UNDEFINED_KEYFRAME) || keyFrameTicks[lastKF] != ticks || (keyFrames[lastKF] .getOnFinished() == null))) { setTime(ticks); clipInterpolator.interpolate(ticks); } } private void setTime(long ticks) { curTicks = ticks; AnimationAccessor.getDefault().setCurrentTicks(timeline, ticks); }
Visit a single keyframe.
Params:
  • kfIndex – the index of the keyframe in the keyframe-array
  • kfTicks – the time of that keyframe
/** * Visit a single keyframe. * * @param kfIndex * the index of the keyframe in the keyframe-array * @param kfTicks * the time of that keyframe */
private void visitKeyFrame(int kfIndex, long kfTicks) { if (kfIndex != lastKF) { // suppress double visiting on toggle for // autoReverse lastKF = kfIndex; final KeyFrame kf = keyFrames[kfIndex]; final EventHandler<ActionEvent> onFinished = kf.getOnFinished(); if (onFinished != null) { // visit the action of this keyframe setTime(kfTicks); clipInterpolator.interpolate(kfTicks); try { onFinished.handle(new ActionEvent(kf, null)); } catch (Throwable ex) { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), ex); } } } } }