/**
* Copyright (c) 2005-2006, Sun Microsystems, Inc
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of the TimingFramework project nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jdesktop.animation.timing;
import javax.swing.Timer;
import java.awt.event.*;
import java.util.ArrayList;
import org.jdesktop.animation.timing.interpolation.Interpolator;
import org.jdesktop.animation.timing.interpolation.LinearInterpolator;
/**
* This class controls animations. Its constructors and various
* set methods control the parameters under which animations are run,
* and the other methods support starting and stopping the animation.
* The parameters of this class use the concepts of a "cycle" (the base
* animation) and an "envelope" that controls how the cycle is started,
* ended, and repeated.
* <p>
* Most of the methods here are simle getters/setters for the properties
* used by Animator. Typical animations will simply use one of the
* two constructors (depending on whether you are constructing a repeating
* animation), optionally call any of the <code>set*</code> methods to alter
* any of the other parameters, and then call start() to run the animation.
* For example, this animation will run for 1 second, calling your
* {@link TimingTarget} with timing events when the animation is started,
* running, and stopped:
* <pre>
* Animator animator = new Animator(1000, myTarget);
* animator.start();
* </pre>
* The following variation will run a half-second animation 4 times,
* reversing direction each time:
* <pre>
* Animator animator = new Animator(500, 4, RepeatBehavior.REVERSE, myTarget);
* animator.start();
* </pre>
* More complex animations can be created through using the properties
* in Animator, such as {@link Animator#setAcceleration acceleration} and {@link
* Animator#setDeceleration}. More automated animations can be created and run
* using the {@link org.jdesktop.animation.timing.triggers triggers}
* package to control animations through events and {@link
* org.jdesktop.animation.timing.interpolation.PropertySetter} to
* handle animating object properties.
*/
public final class Animator {
private TimingSource timer; // Currently uses Swing timer. This could change
// in the future to use a more general mechanism
// (and one of better timing resolution). An
// important advantage to the Swing timer is that
// it ensures that we receive and send our timing
// events on the Event Dispatch Thread, which makes
// it easier to use the framework for GUI
// animations.
private TimingSource swingTimer;
private TimingSourceTarget timingSourceTarget;
private ArrayList<TimingTarget> targets = new ArrayList<TimingTarget>(); // Animators may have
// multiple targets
private long startTime; // Tracks original Animator start time
private long currentStartTime; // Tracks start time of current cycle
private int currentCycle = 0; // Tracks number of cycles so far
private boolean intRepeatCount = true; // for typical cases
// of repeated cycles
private boolean timeToStop = false; // This gets triggered during
// fraction calculation
private boolean hasBegun = false;
private long pauseBeginTime = 0; // Used for pause/resume
private boolean running = false; // Used for isRunning()
// Private variables to hold the internal "envelope" values that control
// how the cycle is started, ended, and repeated.
private double repeatCount = 1.0;
private int startDelay;
private RepeatBehavior repeatBehavior = RepeatBehavior.REVERSE;
private EndBehavior endBehavior = EndBehavior.HOLD;
// Private variables to hold the internal values of the base
// animation (the cycle)
private int duration;
private int resolution = 20;
private float acceleration = 0;
private float deceleration = 0.0f;
private float startFraction = 0.0f;
private Direction direction = Direction.FORWARD; // Direction of each cycle
private Interpolator interpolator = LinearInterpolator.getInstance();
/**
* EndBehavior determines what happens at the end of the animation.
* @see #setEndBehavior
*/
public static enum EndBehavior {
/** Timing sequence will maintain its final value at the end */
HOLD,
/** Timing sequence should reset to the initial value at the end */
RESET,
};
/**
* Direction is used to set the initial direction in which the
* animation starts.
*
* @see #setStartDirection
*/
public static enum Direction {
/**
* cycle proceeds forward
*/
FORWARD,
/** cycle proceeds backward */
BACKWARD,
};
/**
* RepeatBehavior determines how each successive cycle will flow.
* @see #setRepeatBehavior
*/
public static enum RepeatBehavior {
/**
* Each repeated cycle proceeds in the same direction as the
* previous one
*/
LOOP,
/**
* Each cycle proceeds in the opposite direction as the
* previous one
*/
REVERSE
};
/**
* Used to specify unending duration or repeatCount
* @see #setDuration
* @see #setRepeatCount
* */
public static final int INFINITE = -1;
private void validateRepeatCount(double repeatCount) {
if (repeatCount < 1 && repeatCount != INFINITE) {
throw new IllegalArgumentException("repeatCount (" + repeatCount +
") cannot be <= 0");
}
}
/**
* Constructor: this is a utility constructor
* for a simple timing sequence that will run for
* <code>duration</code> length of time. This variant takes no
* TimingTarget, and is equivalent to calling {@link #Animator(int,
* TimingTarget)} with a TimingTarget of <code>null</code>.
*
* @param duration The length of time that this will run, in milliseconds.
*/
public Animator(int duration) {
this(duration, null);
}
/**
* Constructor: this is a utility constructor
* for a simple timing sequence that will run for
* <code>duration</code> length of time.
*
* @param duration The length of time that this will run, in milliseconds.
* @param target TimingTarget object that will be called with
* all timing events. Null is acceptable, but no timingEvents will be
* sent to any targets without future calls to {@link #addTarget}.
*/
public Animator(int duration, TimingTarget target) {
this.duration = duration;
addTarget(target);
/**
* hack workaround for starting the Toolkit thread before any Timer stuff
* javax.swing.Timer uses the Event Dispatch Thread, which is not
* created until the Toolkit thread starts up. Using the Swing
* Timer before starting this stuff starts up may get unexpected
* results (such as taking a long time before the first timer
* event).
*/
java.awt.Toolkit tk = java.awt.Toolkit.getDefaultToolkit();
// Create internal Timer object
swingTimer = new SwingTimingSource();
timer = swingTimer;
}
/**
* Constructor that sets the most common properties of a
* repeating animation.
* @param duration the length of each animation cycle, in milliseconds.
* This value can also be {@link #INFINITE} for animations that have no
* end. Note that fractions sent out with such unending animations will
* be undefined since there is no fraction of an infinitely long cycle.
* @param repeatCount the number of times the animation cycle will repeat.
* This is a positive value, which allows a non-integral number
* of repetitions (allowing an animation to stop mid-cycle, for example).
* This value can also be {@link #INFINITE}, indicating that the animation
* will continue repeating forever, or until manually stopped.
* @param repeatBehavior {@link RepeatBehavior} of each successive
* cycle. A value of null is equivalent to RepeatBehavior.REVERSE.
* @param target TimingTarget object that will be called with
* all timing events. Null is acceptable, but no timingEvents will be
* sent to any targets without future calls to {@link #addTarget}.
* @throws IllegalArgumentException if any parameters have invalid
* values
* @see Animator#INFINITE
* @see Direction
* @see EndBehavior
*/
public Animator(int duration, double repeatCount,
RepeatBehavior repeatBehavior, TimingTarget target) {
this(duration, target);
// First, check for bad parameters
validateRepeatCount(repeatCount);
this.repeatCount = repeatCount;
this.repeatBehavior = (repeatBehavior != null) ?
repeatBehavior : RepeatBehavior.REVERSE;
// Set convenience variable: do we have an integer number of cycles?
intRepeatCount = (Math.rint(repeatCount) == repeatCount);
}
/**
* Returns the initial direction for the animation.
* @return direction that the initial animation cycle will be moving
*/
public Direction getStartDirection() {
return direction;
}
/**
* Sets the startDirection for the initial animation cycle. The default
* startDirection is {@link Direction#FORWARD FORWARD}.
*
* @param startDirection initial animation cycle direction
* @see #isRunning()
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
*/
public void setStartDirection(Direction startDirection) {
throwExceptionIfRunning();
this.direction = startDirection;
}
/**
* Returns the interpolator for the animation.
* @return interpolator that the initial animation cycle uses
*/
public Interpolator getInterpolator() {
return interpolator;
}
/**
* Sets the interpolator for the animation cycle. The default
* interpolator is {@link LinearInterpolator}.
* @param interpolator the interpolation to use each animation cycle
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
*/
public void setInterpolator(Interpolator interpolator) {
throwExceptionIfRunning();
this.interpolator = interpolator;
}
/**
* Sets the fraction of the timing cycle that will be spent accelerating
* at the beginning. The default acceleration value is 0 (no acceleration).
* @param acceleration value from 0 to 1
* @throws IllegalArgumentException acceleration value must be between 0 and
* 1, inclusive.
* @throws IllegalArgumentException acceleration cannot be greater than
* (1 - deceleration)
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
* @see #setDeceleration(float)
*/
public void setAcceleration(float acceleration) {
throwExceptionIfRunning();
if (acceleration < 0 || acceleration > 1.0f) {
throw new IllegalArgumentException("Acceleration value cannot lie" +
" outside [0,1] range");
}
if (acceleration > (1.0f - deceleration)) {
throw new IllegalArgumentException("Acceleration value cannot be" +
" greater than (1 - deceleration)");
}
this.acceleration = acceleration;
}
/**
* Sets the fraction of the timing cycle that will be spent decelerating
* at the end. The default deceleration value is 0 (no deceleration).
* @param deceleration value from 0 to 1
* @throws IllegalArgumentException deceleration value must be between 0 and
* 1, inclusive.
* @throws IllegalArgumentException deceleration cannot be greater than
* (1 - acceleration)
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
* @see #setAcceleration(float)
*/
public void setDeceleration(float deceleration) {
throwExceptionIfRunning();
if (deceleration < 0 || deceleration > 1.0f) {
throw new IllegalArgumentException("Deceleration value cannot lie" +
" outside [0,1] range");
}
if (deceleration > (1.0f - acceleration)) {
throw new IllegalArgumentException("Deceleration value cannot be" +
" greater than (1 - acceleration)");
}
this.deceleration = deceleration;
}
/**
* Returns the current value of acceleration property
* @return acceleration value
*/
public float getAcceleration() {
return acceleration;
}
/**
* Returns the current value of deceleration property
* @return deceleration value
*/
public float getDeceleration() {
return deceleration;
}
/**
* Adds a TimingTarget to the list of targets that get notified of each
* timingEvent. This can be done at any time before, during, or after the
* animation has started or completed; the new target will begin
* having its TimingTarget methods called as soon as it is added.
* If <code>target</code> is already on the list of targets in this Animator, it
* is not added again (there will be only one instance of any given
* target in any Animator's list of targets).
* @param target TimingTarget to be added to the list of targets that
* get notified by this Animator of all timing events. Target cannot
* be null.
*/
public void addTarget(TimingTarget target) {
if (target != null) {
synchronized (targets) {
if (!targets.contains(target)) {
targets.add(target);
}
}
}
}
/**
* Removes the specified TimingTarget from the list of targets that get
* notified of each timingEvent. This can be done at any time before,
* during, or after the animation has started or completed; the
* target will cease having its TimingTarget methods called as soon
* as it is removed.
* @param target TimingTarget to be removed from the list of targets that
* get notified by this Animator of all timing events.
*/
public void removeTarget(TimingTarget target) {
synchronized (targets) {
targets.remove(target);
}
}
/**
* Private utility to throw an exception if the animation is running. This
* is used by all of the property-setting methods to ensure that the
* properties are not being changed mid-stream.
*/
private void throwExceptionIfRunning() {
if (isRunning()) {
throw new IllegalStateException("Cannot perform this operation " +
"while Animator is running");
}
}
/**
* Returns the current resolution of the animation. This helps
* determine the maximum frame rate at which the animation will run.
* @return the resolution, in milliseconds, of the timer
*/
public int getResolution() {
return resolution;
}
/**
* Sets the resolution of the animation
* @param resolution the amount of time between timing events of the
* animation, in milliseconds. Note that the actual resolution may vary,
* according to the resolution of the timer used by the framework as well
* as system load and configuration; this value should be seen more as a
* minimum resolution than a guaranteed resolution.
* @throws IllegalArgumentException resolution must be >= 0
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
*/
public void setResolution(int resolution) {
if (resolution < 0) {
throw new IllegalArgumentException("resolution must be >= 0");
}
throwExceptionIfRunning();
this.resolution = resolution;
timer.setResolution(resolution);
}
/**
* Returns the duration of the animation.
* @return the length of the animation, in milliseconds. A
* return value of -1 indicates an {@link #INFINITE} duration.
*/
public int getDuration() {
return duration;
}
/**
* Sets the duration for the animation
* @param duration the length of the animation, in milliseconds. This
* value can also be {@link #INFINITE}, meaning the animation will run
* until manually stopped.
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
* @see #stop()
*/
public void setDuration(int duration) {
throwExceptionIfRunning();
this.duration = duration;
}
/**
* Returns the number of times the animation cycle will repeat.
* @return the number of times the animation cycle will repeat.
*/
public double getRepeatCount() {
return repeatCount;
}
/**
* Sets the number of times the animation cycle will repeat. The default
* value is 1.
* @param repeatCount Number of times the animation cycle will repeat.
* This value may be >= 1 or {@link #INFINITE} for animations that repeat
* indefinitely. The value may be fractional if the animation should
* stop at some fractional point.
* @throws IllegalArgumentException if repeatCount is not >=1 or
* INFINITE.
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
*/
public void setRepeatCount(double repeatCount) {
validateRepeatCount(repeatCount);
throwExceptionIfRunning();
this.repeatCount = repeatCount;
}
/**
* Returns the amount of delay prior to starting the first animation
* cycle after the call to {@link #start}.
* @return the duration, in milliseconds, between the call
* to start the animation and the first animation cycle actually
* starting.
* @see #start
*/
public int getStartDelay() {
return startDelay;
}
/**
* Sets the duration of the initial delay between calling {@link #start}
* and the start of the first animation cycle. The default value is 0 (no
* delay).
* @param startDelay the duration, in milliseconds, between the call
* to start the animation and the first animation cycle actually
* starting. This value must be >= 0.
* @throws IllegalArgumentException if startDelay is < 0
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
*/
public void setStartDelay(int startDelay) {
if (startDelay < 0) {
throw new IllegalArgumentException("startDelay (" + startDelay +
") cannot be < 0");
}
throwExceptionIfRunning();
this.startDelay = startDelay;
timer.setStartDelay(startDelay);
}
/**
* Returns the {@link RepeatBehavior} of the animation. The default
* behavior is REVERSE, meaning that the animation will reverse direction
* at the end of each cycle.
* @return whether the animation will repeat in the same
* direction or will reverse direction each time.
*/
public RepeatBehavior getRepeatBehavior() {
return repeatBehavior;
}
/**
* Sets the {@link RepeatBehavior} of the animation.
* @param repeatBehavior the behavior for each successive cycle in the
* animation. A null behavior is equivalent to specifying the default:
* REVERSE. The default behaviors is HOLD.
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning()
*/
public void setRepeatBehavior(RepeatBehavior repeatBehavior) {
throwExceptionIfRunning();
this.repeatBehavior = (repeatBehavior != null) ?
repeatBehavior : RepeatBehavior.REVERSE;
}
/**
* Returns the {@link EndBehavior} of the animation, either HOLD to
* retain the final value or RESET to take on the initial value. The
* default behavior is HOLD.
* @return the behavior at the end of the animation
*/
public EndBehavior getEndBehavior() {
return endBehavior;
}
/**
* Sets the behavior at the end of the animation.
* @param endBehavior the behavior at the end of the animation, either
* HOLD or RESET. A null value is equivalent to the default value of
* HOLD.
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
* @see #isRunning
*/
public void setEndBehavior(EndBehavior endBehavior) {
throwExceptionIfRunning();
this.endBehavior = endBehavior;
}
/**
* Returns the fraction that the first cycle will start at.
* @return fraction between 0 and 1 at which the first cycle will start.
*/
public float getStartFraction() {
return startFraction;
}
/**
* Sets the initial fraction at which the first animation cycle will
* begin. The default value is 0.
* @param startFraction
* @see #isRunning()
* @throws IllegalArgumentException if startFraction is less than 0
* or greater than 1
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended
*/
public void setStartFraction(float startFraction) {
if (startFraction < 0 || startFraction > 1.0f) {
throw new IllegalArgumentException("initialFraction must be " +
"between 0 and 1");
}
throwExceptionIfRunning();
this.startFraction = startFraction;
}
/**
* Starts the animation
* @throws IllegalStateException if animation is already running; this
* command may only be run prior to starting the animation or
* after the animation has ended
*/
public void start() {
throwExceptionIfRunning();
hasBegun = false;
running = true;
// Initialize start time variables to current time
startTime = (System.nanoTime() / 1000000) + getStartDelay();
if (duration != INFINITE &&
((direction == Direction.FORWARD && startFraction > 0.0f) ||
(direction == Direction.BACKWARD && startFraction < 1.0f))) {
float offsetFraction = (direction == Direction.FORWARD) ?
startFraction : (1.0f - startFraction);
long startDelta = (long)(duration * offsetFraction);
startTime -= startDelta;
}
currentStartTime = startTime;
timer.start();
}
/**
* Returns whether this Animator object is currently running
*/
public boolean isRunning() {
return running;
}
/**
* This method is optional; animations will always stop on their own
* if Animator is provided with appropriate values for
* duration and repeatCount in the constructor. But if the application
* wants to stop the timer mid-stream, this is the method to call.
* This call will result in calls to the <code>end()</code> method
* of all TimingTargets of this Animator.
* @see #cancel()
*/
public void stop() {
timer.stop();
end();
timeToStop = false;
running = false;
pauseBeginTime = 0;
}
/**
* This method is like the {@link #stop} method, only this one will
* not result in a calls to the <code>end()</code> method in all
* TimingTargets of this Animation; it simply cancels the Animator
* immediately.
* @see #stop()
*/
public void cancel() {
timer.stop();
timeToStop = false;
running = false;
pauseBeginTime = 0;
}
/**
* This method pauses a running animation. No further events are sent to
* TimingTargets. A paused animation may be d again by calling the
* {@link #resume} method. Pausing a non-running animation has no effect.
*
* @see #resume()
* @see #isRunning()
*/
public void pause() {
if (isRunning()) {
pauseBeginTime = System.nanoTime();
running = false;
timer.stop();
}
}
/**
* This method resumes a paused animation. Resuming an animation that
* is not paused has no effect.
*
* @see #pause()
*/
public void resume() {
if (pauseBeginTime > 0) {
long pauseDelta = (System.nanoTime() - pauseBeginTime) / 1000000;
startTime += pauseDelta;
currentStartTime += pauseDelta;
timer.start();
pauseBeginTime = 0;
running = true;
}
}
//
// TimingTarget implementations
// Note that Animator does not actually implement TimingTarget directly;
// it does not want to make public methods of these events. But it uses
// the same methods internally to propagate the events to all of the
// Animator's targets.
//
/**
* Internal timingEvent method that sends out the event to all targets
*/
private void timingEvent(float fraction) {
synchronized (targets) {
for (int i = 0; i < targets.size(); ++i) {
TimingTarget target = targets.get(i);
target.timingEvent(fraction);
}
}
if (timeToStop) {
stop();
}
}
/**
* Internal begin event that sends out the event to all targets
*/
private void begin() {
synchronized (targets) {
for (int i = 0; i < targets.size(); ++i) {
TimingTarget target = targets.get(i);
target.begin();
}
}
}
/**
* Internal end event that sends out the event to all targets
*/
private void end() {
synchronized (targets) {
for (int i = 0; i < targets.size(); ++i) {
TimingTarget target = targets.get(i);
target.end();
}
}
}
/**
* Internal repeat event that sends out the event to all targets
*/
private void repeat() {
synchronized (targets) {
for (int i = 0; i < targets.size(); ++i) {
TimingTarget target = targets.get(i);
target.repeat();
}
}
}
/**
* This method calculates a new fraction value based on the
* acceleration and deceleration settings of Animator. It then
* passes this value through the interpolator (by default,
* a LinearInterpolator) before returning it to the caller (who
* will then call the timingEvent() methods in the TimingTargets
* with this fraction).
*/
private float timingEventPreprocessor(float fraction) {
// First, take care of acceleration/deceleration factors
if (acceleration != 0 || deceleration != 0.0f) {
// See the SMIL 2.0 specification for details on this
// calculation
float oldFraction = fraction;
float runRate = 1.0f / (1.0f - acceleration/2.0f -
deceleration/2.0f);
if (fraction < acceleration) {
float averageRunRate = runRate * (fraction / acceleration) / 2;
fraction *= averageRunRate;
} else if (fraction > (1.0f - deceleration)) {
// time spent in deceleration portion
float tdec = fraction - (1.0f - deceleration);
// proportion of tdec to total deceleration time
float pdec = tdec / deceleration;
fraction = runRate * (1.0f - ( acceleration / 2) -
deceleration + tdec * (2 - pdec) / 2);
} else {
fraction = runRate * (fraction - (acceleration / 2));
}
// clamp fraction to [0,1] since above calculations may
// cause rounding errors
if (fraction < 0) {
fraction = 0;
} else if (fraction > 1.0f) {
fraction = 1.0f;
}
}
// run the result through the current interpolator
return interpolator.interpolate(fraction);
}
/**
* Returns the total elapsed time for the current animation.
* @param currentTime value of current time to use in calculating
* elapsed time.
* @return the total time elapsed between the time
* the Animator started and the supplied currentTime.
*/
public long getTotalElapsedTime(long currentTime) {
return (currentTime - startTime);
}
/**
* Returns the total elapsed time for the current animation. Calculates
* current time.
* @return the total time elapsed between the time
* the Animator started and the current time.
*/
public long getTotalElapsedTime() {
long currentTime = System.nanoTime() / 1000000;
return getTotalElapsedTime(currentTime);
}
/**
* Returns the elapsed time for the current animation cycle.
* @param currentTime value of current time to use in calculating
* elapsed time.
* @return the time elapsed between the time
* this cycle started and the supplied currentTime.
*/
public long getCycleElapsedTime(long currentTime) {
return (currentTime - currentStartTime);
}
/**
* Returns the elapsed time for the current animation cycle. Calculates
* current time.
* @return the time elapsed between the time
* this cycle started and the current time.
*/
public long getCycleElapsedTime() {
long currentTime = System.nanoTime() / 1000000;
return getCycleElapsedTime(currentTime);
}
/**
* This method calculates and returns the fraction elapsed of the current
* cycle based on the current time
* @return fraction elapsed of the current animation cycle
*/
public float getTimingFraction() {
long currentTime = System.nanoTime() / 1000000;
long cycleElapsedTime = getCycleElapsedTime(currentTime);
long totalElapsedTime = getTotalElapsedTime(currentTime);
double currentCycle = (double)totalElapsedTime / duration;
float fraction;
if (!hasBegun) {
// Call begin() first time after calling start()
begin();
hasBegun = true;
}
if ((duration != INFINITE) && (repeatCount != INFINITE) &&
(currentCycle >= repeatCount)) {
// Envelope done: stop based on end behavior
switch (endBehavior) {
case HOLD:
// Make sure we send a final end value
if (intRepeatCount) {
// If supposed to run integer number of cycles, hold
// on integer boundary
if (direction == Direction.BACKWARD) {
// If we were traveling backward, hold on 0
fraction = 0.0f;
} else {
fraction = 1.0f;
}
} else {
// hold on final value instead
fraction = Math.min(1.0f,
((float)cycleElapsedTime / duration));
}
break;
case RESET:
// RESET requires setting the final value to the start value
fraction = 0.0f;
break;
default:
fraction = 0.0f;
// should not reach here
break;
}
timeToStop = true;
} else if ((duration != INFINITE) && (cycleElapsedTime > duration)) {
// Cycle end: Time to stop or change the behavior of the timer
long actualCycleTime = cycleElapsedTime % duration;
fraction = (float)actualCycleTime / duration;
// Set new start time for this cycle
currentStartTime = currentTime - actualCycleTime;
if (repeatBehavior == RepeatBehavior.REVERSE) {
boolean oddCycles =
((int)(cycleElapsedTime / duration) % 2)
> 0;
if (oddCycles) {
// reverse the direction
direction = (direction == Direction.FORWARD) ?
Direction.BACKWARD :
Direction.FORWARD;
}
if (direction == Direction.BACKWARD) {
fraction = 1.0f - fraction;
}
}
repeat();
} else {
// mid-stream: calculate fraction of animation between
// start and end times and send fraction to target
fraction = 0.0f;
if (duration != INFINITE) {
// Only limited duration animations need a fraction
fraction = (float)cycleElapsedTime / duration;
if (direction == Direction.BACKWARD) {
// If this is a reversing cycle, want to know inverse
// fraction; how much from start to finish, not
// finish to start
fraction = (1.0f - fraction);
}
// Clamp fraction in case timing mechanism caused out of
// bounds value
fraction = Math.min(fraction, 1.0f);
fraction = Math.max(fraction, 0.0f);
}
}
return timingEventPreprocessor(fraction);
}
/**
* Sets a new TimingSource that will supply the timing
* events to this Animator. Animator uses an internal
* TimingSource by default and most developers will probably not
* need to change this default behavior. But for those wishing to
* supply their own timer, this method can be called to
* tell Animator to use a different TimingSource instead. Setting a
* new TimingSource implicitly removes this Animator as a listener
* to any previously-set TimingSource object.
*
* @param timer the object that will provide the
* timing events to Animator. A value of <code>null</code> is
* equivalent to telling Animator to use its default internal
* TimingSource object.
* @throws IllegalStateException if animation is already running; this
* parameter may only be changed prior to starting the animation or
* after the animation has ended.
*/
public synchronized void setTimer(TimingSource timer) {
throwExceptionIfRunning();
if (this.timer != swingTimer) {
// Remove this Animator from any previously-set external timer
this.timer.removeEventListener(timingSourceTarget);
}
if (timer == null) {
this.timer = swingTimer;
} else {
this.timer = timer;
if (timingSourceTarget == null) {
timingSourceTarget = new TimingSourceTarget();
}
timer.addEventListener(timingSourceTarget);
}
// sync this new timer with existing timer properties
this.timer.setResolution(resolution);
this.timer.setStartDelay(startDelay);
}
/**
* This package-private class will be called by TimingSource.timingEvent()
* when a timer sends in timing events to this Animator.
*/
class TimingSourceTarget implements TimingEventListener {
public void timingSourceEvent(TimingSource timingSource) {
// Make sure that we are being called by the current timer
// and that the animation is actually running
if ((timer == timingSource) && running) {
timingEvent(getTimingFraction());
}
}
}
/**
* Implementation of internal timer, which uses the Swing Timer class.
* Note that we do not bother going through the TimingSource.timingEvent()
* class with our timing events; they go through the TimerTarget
* ActionListener implementation and then directly to timingEvent(fraction).
*/
private class SwingTimingSource extends TimingSource {
Timer timer; // Swing timer
public SwingTimingSource() {
timer = new Timer(resolution, new TimerTarget());
timer.setInitialDelay(0);
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
public void setResolution(int resolution) {
timer.setDelay(resolution);
}
public void setStartDelay(int delay) {
timer.setInitialDelay(delay);
}
}
/**
* Internal implementation detail: we happen to use javax.swing.Timer
* currently, which sends its timing events to an ActionListener.
* This internal private class is our ActionListener that traps
* these calls and forwards them to the Animator.timingEvent(fraction)
* method.
*/
private class TimerTarget implements ActionListener {
public void actionPerformed(ActionEvent e) {
timingEvent(getTimingFraction());
}
}
}