blob: cf3530ef17edc13598ef57621569e1efdf437e74 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed 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 com.google.gwt.animation.client;
import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
import com.google.gwt.core.client.Duration;
import com.google.gwt.dom.client.Element;
/**
* An {@link Animation} is a continuous event that updates progressively over
* time at a non-fixed frame rate.
*/
public abstract class Animation {
private final AnimationCallback callback = new AnimationCallback() {
@Override
public void execute(double timestamp) {
if (update(timestamp)) {
// Schedule the next animation frame.
requestHandle = scheduler.requestAnimationFrame(callback, element);
} else {
requestHandle = null;
}
}
};
/**
* The duration of the {@link Animation} in milliseconds.
*/
private int duration = -1;
/**
* The element being animated.
*/
private Element element;
/**
* Is the animation running, even if it hasn't started yet.
*/
private boolean isRunning = false;
/**
* Has the {@link Animation} actually started.
*/
private boolean isStarted = false;
/**
* The ID of the pending animation request.
*/
private AnimationHandle requestHandle;
/**
* The unique ID of the current run. Used to handle cases where an animation
* is restarted within an execution block.
*/
private int runId = -1;
private final AnimationScheduler scheduler;
/**
* The start time of the {@link Animation}.
*/
private double startTime = -1;
/**
* Did the animation start before {@link #cancel()} was called.
*/
private boolean wasStarted = false;
/**
* Construct a new {@link Animation}.
*/
public Animation() {
this(AnimationScheduler.get());
}
/**
* Construct a new {@link AnimationScheduler} using the specified scheduler to
* sheduler request frames.
*
* @param scheduler an {@link AnimationScheduler} instance
*/
protected Animation(AnimationScheduler scheduler) {
this.scheduler = scheduler;
}
/**
* Immediately cancel this animation. If the animation is running or is
* scheduled to run, {@link #onCancel()} will be called.
*/
public void cancel() {
// Ignore if the animation is not currently running.
if (!isRunning) {
return;
}
// Reset the state.
wasStarted = isStarted; // Used by onCancel.
element = null;
isRunning = false;
isStarted = false;
// Cancel the animation request.
if (requestHandle != null) {
requestHandle.cancel();
requestHandle = null;
}
onCancel();
}
/**
* Immediately run this animation. If the animation is already running, it
* will be canceled first.
* <p>
* This is equivalent to <code>run(duration, null)</code>.
*
* @param duration the duration of the animation in milliseconds
* @see #run(int, Element)
*/
public void run(int duration) {
run(duration, null);
}
/**
* Immediately run this animation. If the animation is already running, it
* will be canceled first.
* <p>
* If the element is not <code>null</code>, the {@link #onUpdate(double)}
* method might be called only if the element may be visible (generally left
* at the appreciation of the browser). Otherwise, it will be called
* unconditionally.
*
* @param duration the duration of the animation in milliseconds
* @param element the element that visually bounds the entire animation
*/
public void run(int duration, Element element) {
run(duration, Duration.currentTimeMillis(), element);
}
/**
* Run this animation at the given startTime. If the startTime has already
* passed, the animation will run synchronously as if it started at the
* specified start time. If the animation is already running, it will be
* canceled first.
* <p>
* This is equivalent to <code>run(duration, startTime, null)</code>.
*
* @param duration the duration of the animation in milliseconds
* @param startTime the synchronized start time in milliseconds
* @see #run(int, double, Element)
*/
public void run(int duration, double startTime) {
run(duration, startTime, null);
}
/**
* Run this animation at the given startTime. If the startTime has already
* passed, the animation will run synchronously as if it started at the
* specified start time. If the animation is already running, it will be
* canceled first.
* <p>
* If the element is not <code>null</code>, the {@link #onUpdate(double)}
* method might be called only if the element may be visible (generally left
* at the appreciation of the browser). Otherwise, it will be called
* unconditionally.
*
* @param duration the duration of the animation in milliseconds
* @param startTime the synchronized start time in milliseconds
* @param element the element that visually bounds the entire animation
*/
public void run(int duration, double startTime, Element element) {
// Cancel the animation if it is running
cancel();
// Save the duration and startTime
isRunning = true;
isStarted = false;
this.duration = duration;
this.startTime = startTime;
this.element = element;
++runId;
// Execute the first callback.
callback.execute(Duration.currentTimeMillis());
}
/**
* Interpolate the linear progress into a more natural easing function.
*
* Depending on the {@link Animation}, the return value of this method can be
* less than 0.0 or greater than 1.0.
*
* @param progress the linear progress, between 0.0 and 1.0
* @return the interpolated progress
*/
protected double interpolate(double progress) {
return (1 + Math.cos(Math.PI + progress * Math.PI)) / 2;
}
/**
* Called immediately after the animation is canceled. The default
* implementation of this method calls {@link #onComplete()} only if the
* animation has actually started running.
*/
protected void onCancel() {
if (wasStarted) {
onComplete();
}
}
/**
* Called immediately after the animation completes.
*/
protected void onComplete() {
onUpdate(interpolate(1.0));
}
/**
* Called immediately before the animation starts.
*/
protected void onStart() {
onUpdate(interpolate(0.0));
}
/**
* Called when the animation should be updated.
*
* The value of progress is between 0.0 and 1.0 (inclusive) (unless you
* override the {@link #interpolate(double)} method to provide a wider range
* of values). You can override {@link #onStart()} and {@link #onComplete()}
* to perform setup and tear down procedures.
*
* @param progress a double, normally between 0.0 and 1.0 (inclusive)
*/
protected abstract void onUpdate(double progress);
/**
* Check if the specified run ID is still being run.
*
* @param curRunId the current run ID to check
* @return true if running, false if canceled or restarted
*/
private boolean isRunning(int curRunId) {
return isRunning && (runId == curRunId);
}
/**
* Update the {@link Animation}.
*
* @param curTime the current time
* @return true if the animation should run again, false if it is complete
*/
private boolean update(double curTime) {
/*
* Save the run id. If the runId is incremented during this execution block,
* we know that this run has been canceled.
*/
final int curRunId = runId;
boolean finished = curTime >= startTime + duration;
if (isStarted && !finished) {
// Animation is in progress.
double progress = (curTime - startTime) / duration;
onUpdate(interpolate(progress));
return isRunning(curRunId); // Check if this run was canceled.
}
if (!isStarted && curTime >= startTime) {
/*
* Start the animation. We do not call onUpdate() because onStart() calls
* onUpdate() by default.
*/
isStarted = true;
onStart();
if (!isRunning(curRunId)) {
// This run was canceled.
return false;
}
// Intentional fall through to possibly end the animation.
}
if (finished) {
// Animation is complete.
isRunning = false;
isStarted = false;
onComplete();
return false;
}
return true;
}
}