blob: 1556840302580139bed8ac6b2266cca7271ad820 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.scene.animation;
import java.util.Timer;
import java.util.TimerTask;
/**
* Class for scheduling "transitions", which are generally used for animated
* application effects.
*/
public class Transition {
private class ScheduledCallback extends TimerTask {
private Runnable callback = new Runnable() {
@Override
public void run() {
currentTime = System.currentTimeMillis();
long endTime = startTime + duration;
if (currentTime >= endTime) {
if (repeating) {
startTime = endTime;
} else {
currentTime = endTime;
stop();
if (transitionListener != null) {
transitionListener.transitionCompleted(Transition.this);
}
}
}
updateCallback.run();
}
};
private QueuedCallback queuedCallback = null;
@Override
public void run() {
if (queuedCallback != null) {
queuedCallback.cancel();
}
QueuedCallback queuedCallback = new QueuedCallback(callback);
java.awt.EventQueue.invokeLater(queuedCallback);
}
@Override
public boolean cancel() {
if (queuedCallback != null) {
queuedCallback.cancel();
}
return super.cancel();
}
}
private static class QueuedCallback implements Runnable {
private Runnable callback;
private volatile boolean executed = false;
private volatile boolean cancelled = false;
private QueuedCallback(Runnable callback) {
this.callback = callback;
}
@Override
public void run() {
if (!cancelled) {
try {
callback.run();
} catch (Exception exception) {
exception.printStackTrace();
}
executed = true;
}
}
public boolean cancel() {
cancelled = true;
return (!executed);
}
}
private int duration;
private int rate;
private boolean repeating;
private boolean reversed = false;
private TransitionListener transitionListener;
private long startTime = 0;
private long currentTime = 0;
private Runnable updateCallback = null;
private ScheduledCallback scheduledCallback = null;
private static Timer timer = new Timer();
/**
* Creates a new non-repeating transition with the given duration and
* rate.
*
* @param duration
* Transition duration, in milliseconds.
*
* @param rate
* Transition rate, in frames per second.
*/
public Transition(int duration, int rate) {
this(duration, rate, false);
}
/**
* Creates a new transition with the given duration, rate, and repeat.
*
* @param duration
* Transition duration, in milliseconds.
*
* @param rate
* Transition rate, in frames per second.
*
* @param repeating
* <tt>true</tt> if the transition should repeat; <tt>false</tt>, otherwise.
*/
public Transition(int duration, int rate, boolean repeating) {
this(duration, rate, repeating, false);
}
/**
* Creates a new transition with the given duration, rate, and repeat.
*
* @param duration
* Transition duration, in milliseconds.
*
* @param rate
* Transition rate, in frames per second.
*
* @param repeating
* <tt>true</tt> if the transition should repeat; <tt>false</tt>, otherwise.
*
* @param reversed
* <tt>true</tt> if the transition should run in reverse; <tt>false</tt>
* otherwise.
*/
public Transition(int duration, int rate, boolean repeating, boolean reversed) {
if (duration <= 0) {
throw new IllegalArgumentException("duration must be positive.");
}
this.duration = duration;
this.rate = rate;
this.repeating = repeating;
this.reversed = reversed;
}
/**
* Returns the transition duration.
*
* @return
* The duration of the transition, in milliseconds.
*
* @see #setDuration(int)
*/
public int getDuration() {
return duration;
}
/**
* Sets the transition duration, the length of time the transition is
* scheduled to run.
*
* @param duration
* The duration of the transition, in milliseconds.
*/
public void setDuration(int duration) {
if (duration < 0) {
throw new IllegalArgumentException("duration is negative.");
}
if (scheduledCallback != null) {
throw new IllegalStateException("Transition is currently running.");
}
this.duration = duration;
}
/**
* Returns the transition rate.
*
* @return
* The rate of the transition, in frames per second.
*
* @see #setRate(int)
*/
public int getRate() {
return rate;
}
/**
* Sets the transition rate, the number of times the transition will be
* updated within the span of one second.
*
* @param rate
* The transition rate, in frames per second.
*/
public void setRate(int rate) {
if (rate < 0) {
throw new IllegalArgumentException("rate is negative.");
}
if (scheduledCallback != null) {
throw new IllegalStateException("Transition is currently running.");
}
this.rate = rate;
}
/**
* Returns the transition interval, the number of milliseconds between
* updates.
*
* @return
* The transition interval, in milliseconds.
*/
public int getInterval() {
return (int)((1f / rate) * 1000);
}
/**
* Returns the time at which the transition was started.
*
* @return
* The transition's start time.
*/
public long getStartTime() {
return startTime;
}
/**
* Returns the last time the transition was updated.
*
* @return
* The most recent update time.
*/
public long getCurrentTime() {
return currentTime;
}
/**
* Returns the elapsed time since the transition started.
*
* @return
* Returns the amount of time that has passed since the transition
* was started. If the transition is reversed, this value reflects the
* amount of time remaining.
*/
public int getElapsedTime() {
long endTime = startTime + duration;
int elapsedTime;
if (reversed) {
elapsedTime = (int)(endTime - currentTime);
} else {
elapsedTime = (int)(currentTime - startTime);
}
return elapsedTime;
}
/**
* Returns the percentage of the transition that has completed.
*
* @return
* A value between 0 and 1, inclusive, representing the transition's
* percent complete. If the transition is reversed, this value reflects
* the percent remaining.
*/
public float getPercentComplete() {
float percentComplete = (float)(currentTime - startTime) / (float)(duration);
if (reversed) {
percentComplete = 1.0f - percentComplete;
}
return percentComplete;
}
/**
* Tells whether or not the transition is currently running.
*
* @return
* <tt>true</tt> if the transition is currently running; <tt>false</tt> if
* it is not
*/
public boolean isRunning() {
return (scheduledCallback != null);
}
/**
* Starts the transition with no listener.
*
* @param updateCallback
*
* @see #start(Runnable, TransitionListener)
*/
public final void start(Runnable updateCallback) {
start(updateCallback, null);
}
/**
* Starts the transition. Calls {@link #update()} to establish the
* initial state and starts a timer that will repeatedly call
* {@link #update()} at the current rate. The specified
* <tt>TransitionListener</tt> will be notified when the transition
* completes.
*
* @param updateCallback
* A callback that will be invoked to update the transition state.
*
* @param transitionListener
* The listener to get notified when the transition completes, or
* <tt>null</tt> if no notification is necessary
*/
public void start(Runnable updateCallback, TransitionListener transitionListener) {
if (updateCallback == null) {
throw new IllegalArgumentException();
}
if (this.updateCallback != null) {
throw new IllegalStateException("Transition is currently running.");
}
this.updateCallback = updateCallback;
this.transitionListener = transitionListener;
startTime = System.currentTimeMillis();
currentTime = startTime;
scheduledCallback = new ScheduledCallback();
// TODO This is a workaround for a potential OS X bug; revisit
try {
try {
timer.schedule(scheduledCallback, 0, getInterval());
} catch (IllegalStateException exception) {
timer = new Timer();
timer.schedule(scheduledCallback, 0, getInterval());
}
} catch (Throwable throwable) {
System.err.println("Unable to schedule callback: " + throwable);
}
updateCallback.run();
}
/**
* Stops the transition. Does not fire a
* {@link TransitionListener#transitionCompleted(Transition)} event.
*/
public void stop() {
if (scheduledCallback != null) {
scheduledCallback.cancel();
}
scheduledCallback = null;
}
/**
* "Fast-forwards" to the end of the transition and fires a
* {@link TransitionListener#transitionCompleted(Transition)} event.
*/
public void end() {
if (scheduledCallback != null) {
currentTime = startTime + duration;
stop();
updateCallback.run();
transitionListener.transitionCompleted(this);
}
}
public boolean isRepeating() {
return repeating;
}
/**
* Reverses the transition with no listener.
*
* @see #reverse(TransitionListener)
*/
public void reverse() {
reverse(null);
}
/**
* Reverses the transition. Updates the start time so the reverse duration
* is the same as the current elapsed time.
*
* @param transitionListener
* The listener to get notified when the transition completes, or
* <tt>null</tt> if no notification is necessary
*/
public void reverse(TransitionListener transitionListener) {
if (this.updateCallback == null) {
throw new IllegalStateException("Transition is not currently running.");
}
this.transitionListener = transitionListener;
long repeatDuration = currentTime - startTime;
long endTime = currentTime + repeatDuration;
startTime = endTime - duration;
reversed = !reversed;
}
/**
* Tests whether the transition is reversed.
*
* @return
* <tt>true</tt> if the transition is reversed; <tt>false</tt>, otherwise.
*/
public boolean isReversed() {
return reversed;
}
}