/** * 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()); } } }