Resubmitting r9970 again (again) - updating Animation to use the native requestAnimationFrame, which generally results in optimal performance versus using timer based animations. I did away with the soft permutation in this version. Basing the impl on user agent leads to a smaller code footprint versus a soft perm because a soft perm includes all implementations, whereas we now only include the native and timer backup implementations in any permutation.
After seeing some of the demos at Google IO, I'm excited about the awesomeness and simplicity of requestAnimationFrame, even beyond the Animation class. Animation carries the implicit restriction that it must execute over a fixed amount of time, so it can't be used for something like a game or unbounded animation. So, I've modified tbroyer's patch and added static methods Animation.requestAnimationFrame(Callback,Element)/cancelAnimationFrame(). These are alternatives to using a Timer or a Scheduler, and the Animation class makes use these static methods.
AnimationTest wasn't included in a test suite, so we aren't running it in our build, which is good, because its flaky as heck. I created a StubAnimationSchedulerImpl that lets you manually trigger the AnimationCallback, specifying a mock timestamp. This allowed me convert the asynchronous tests in AnimationTest into synchronous tests, add more tests, and remove the flakiness. I fixed some edge conditions revealed by the new tests. Specifically, cancelling (or restarting) an animation within onStart/onComplete/onUpdate would usually result in a bad state. Now, we increment a runId so we can detect that the current run has been canceled and gracefully move on. The tests are part of AnimationSuite, which will run in our build.
Author: tbroyer,jlabanca
Issue: 5639
Review at http://gwt-code-reviews.appspot.com/1446812
Review by: fabbott@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10257 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/animation/Animation.gwt.xml b/user/src/com/google/gwt/animation/Animation.gwt.xml
index a34f63d..bc2a47c 100644
--- a/user/src/com/google/gwt/animation/Animation.gwt.xml
+++ b/user/src/com/google/gwt/animation/Animation.gwt.xml
@@ -18,7 +18,25 @@
<!-- -->
<module>
<inherits name="com.google.gwt.core.Core"/>
+ <inherits name="com.google.gwt.user.UserAgent"/>
<!-- Include User module to inherit Timer -->
<inherits name="com.google.gwt.user.User"/>
+
+ <!-- Fallback implementation, based on a timer -->
+ <replace-with class="com.google.gwt.animation.client.AnimationSchedulerImplTimer">
+ <when-type-is class="com.google.gwt.animation.client.AnimationScheduler"/>
+ </replace-with>
+
+ <!-- Implementation based on mozRequestAnimationFrame -->
+ <replace-with class="com.google.gwt.animation.client.AnimationSchedulerImplMozilla">
+ <when-type-is class="com.google.gwt.animation.client.AnimationScheduler"/>
+ <when-property-is name="user.agent" value="gecko1_8"/>
+ </replace-with>
+
+ <!-- Implementation based on webkitRequestAnimationFrame -->
+ <replace-with class="com.google.gwt.animation.client.AnimationSchedulerImplWebkit">
+ <when-type-is class="com.google.gwt.animation.client.AnimationScheduler"/>
+ <when-property-is name="user.agent" value="safari"/>
+ </replace-with>
</module>
diff --git a/user/src/com/google/gwt/animation/client/Animation.java b/user/src/com/google/gwt/animation/client/Animation.java
index f4225b2..cf3530e 100644
--- a/user/src/com/google/gwt/animation/client/Animation.java
+++ b/user/src/com/google/gwt/animation/client/Animation.java
@@ -15,56 +15,28 @@
*/
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.user.client.Timer;
-
-import java.util.ArrayList;
-import java.util.List;
+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 {
- /**
- * The default time in milliseconds between frames.
- */
- private static final int DEFAULT_FRAME_DELAY = 25;
- /**
- * The {@link Animation Animations} that are currently in progress.
- */
- private static List<Animation> animations = null;
-
- /**
- * The {@link Timer} that applies the animations.
- */
- private static Timer animationTimer = null;
-
- /**
- * Update all {@link Animation Animations}.
- */
- private static void updateAnimations() {
- // Duplicate the animations list in case it changes as we iterate over it
- Animation[] curAnimations = new Animation[animations.size()];
- curAnimations = animations.toArray(curAnimations);
-
- // Iterator through the animations
- double curTime = Duration.currentTimeMillis();
- for (Animation animation : curAnimations) {
- if (animation.running && animation.update(curTime)) {
- // We can't just remove the animation at the index, because calling
- // animation.update may have the side effect of canceling this
- // animation, running new animations, or canceling other animations.
- animations.remove(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;
}
}
-
- // Reschedule the timer
- if (animations.size() > 0) {
- animationTimer.schedule(DEFAULT_FRAME_DELAY);
- }
- }
+ };
/**
* The duration of the {@link Animation} in milliseconds.
@@ -72,15 +44,32 @@
private int duration = -1;
/**
- * Is the {@link Animation} running, even if {@link #onStart()} has not yet
- * been called.
+ * The element being animated.
*/
- private boolean running = false;
+ 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 started = false;
+ 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}.
@@ -88,73 +77,126 @@
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 (!running) {
+ // Ignore if the animation is not currently running.
+ if (!isRunning) {
return;
}
- animations.remove(this);
+ // 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();
- started = false;
- running = false;
}
/**
* 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, Duration.currentTimeMillis());
+ 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 be synchronize as if it started at the specified
- * start time. If the animation is already running, it will be canceled first.
+ * 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
- this.running = true;
+ isRunning = true;
+ isStarted = false;
this.duration = duration;
this.startTime = startTime;
+ this.element = element;
+ ++runId;
- // Start synchronously if start time has passed
- if (update(Duration.currentTimeMillis())) {
- return;
- }
-
- // Add to the list of animations
-
- // We use a static list of animations and a single timer, and create them
- // only if we are the only active animation. This is safe since JS is
- // single-threaded.
- if (animations == null) {
- animations = new ArrayList<Animation>();
- animationTimer = new Timer() {
- @Override
- public void run() {
- updateAnimations();
- }
- };
- }
- animations.add(this);
-
- // Restart the timer if there is the only animation
- if (animations.size() == 1) {
- animationTimer.schedule(DEFAULT_FRAME_DELAY);
- }
+ // Execute the first callback.
+ callback.execute(Duration.currentTimeMillis());
}
/**
@@ -176,7 +218,7 @@
* animation has actually started running.
*/
protected void onCancel() {
- if (started) {
+ if (wasStarted) {
onComplete();
}
}
@@ -208,32 +250,55 @@
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 is complete, false if still running
+ * @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 (started && !finished) {
+ if (isStarted && !finished) {
// Animation is in progress.
double progress = (curTime - startTime) / duration;
onUpdate(interpolate(progress));
- return false;
+ return isRunning(curRunId); // Check if this run was canceled.
}
- if (!started && curTime >= startTime) {
- // Start the animation.
- started = true;
+ 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();
- started = false;
- running = false;
- return true;
+ return false;
}
- return false;
+ return true;
}
}
diff --git a/user/src/com/google/gwt/animation/client/AnimationScheduler.java b/user/src/com/google/gwt/animation/client/AnimationScheduler.java
new file mode 100644
index 0000000..1432ce8
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/AnimationScheduler.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 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.dom.client.Element;
+
+/**
+ * This class provides task scheduling for animations. Any exceptions thrown by
+ * the command objects executed by the scheduler will be passed to the
+ * {@link com.google.gwt.core.client.GWT.UncaughtExceptionHandler} if one is
+ * installed.
+ */
+public abstract class AnimationScheduler {
+
+ /**
+ * The callback used when an animation frame becomes available.
+ */
+ public interface AnimationCallback {
+ /**
+ * Invokes the command.
+ *
+ * @param timestamp the current timestamp
+ */
+ void execute(double timestamp);
+ }
+
+ /**
+ * A handle to the requested animation frame created by
+ * {@link #requestAnimationFrame(AnimationCallback, Element)}.
+ */
+ public abstract static class AnimationHandle {
+ /**
+ * Cancel the requested animation frame. If the animation frame is already
+ * canceled, do nothing.
+ */
+ public abstract void cancel();
+ }
+
+ /**
+ * Returns the default implementation of the AnimationScheduler API.
+ */
+ public static AnimationScheduler get() {
+ return AnimationSchedulerImpl.INSTANCE;
+ }
+
+ /**
+ * Schedule an animation, letting the browser decide when to trigger the next
+ * step in the animation.
+ *
+ * <p>
+ * NOTE: If you are animating an element, use
+ * {@link #requestAnimationFrame(AnimationCallback, Element)} instead so the
+ * browser can optimize for the specified element.
+ * </p>
+ *
+ * <p>
+ * Using this method instead of a timeout is preferred because the browser is
+ * in the best position to decide how frequently to trigger the callback for
+ * an animation of the specified element. The browser can balance multiple
+ * animations and trigger callbacks at the optimal rate for smooth
+ * performance.
+ * </p>
+ *
+ * @param callback the callback to fire
+ * @return a handle to the requested animation frame
+ * @see #requestAnimationFrame(AnimationCallback, Element)
+ */
+ public AnimationHandle requestAnimationFrame(AnimationCallback callback) {
+ return requestAnimationFrame(callback, null);
+ }
+
+ /**
+ * Schedule an animation, letting the browser decide when to trigger the next
+ * step in the animation.
+ *
+ * <p>
+ * Using this method instead of a timeout is preferred because the browser is
+ * in the best position to decide how frequently to trigger the callback for
+ * an animation of the specified element. The browser can balance multiple
+ * animations and trigger callbacks at the optimal rate for smooth
+ * performance.
+ * </p>
+ *
+ * @param callback the callback to fire
+ * @return a handle to the requested animation frame
+ * @param element the element being animated
+ */
+ public abstract AnimationHandle requestAnimationFrame(AnimationCallback callback, Element element);
+}
diff --git a/user/src/com/google/gwt/animation/client/AnimationSchedulerImpl.java b/user/src/com/google/gwt/animation/client/AnimationSchedulerImpl.java
new file mode 100644
index 0000000..fc36fc9
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/AnimationSchedulerImpl.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 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.core.client.GWT;
+
+/**
+ * Base class for animation implementations.
+ */
+abstract class AnimationSchedulerImpl extends AnimationScheduler {
+
+ /**
+ * The singleton instance of animation scheduler.
+ */
+ static final AnimationScheduler INSTANCE;
+
+ static {
+ AnimationScheduler impl = GWT.create(AnimationScheduler.class);
+
+ /*
+ * If the implementation isn't natively supported, revert back to the timer
+ * based implementation.
+ *
+ * If impl==null (such as with GWTMockUitlities.disarm()), use null. We
+ * don't want to create a new AnimationSchedulerImplTimer in this case.
+ */
+ if (impl instanceof AnimationSchedulerImpl) {
+ if (!((AnimationSchedulerImpl) impl).isNativelySupported()) {
+ impl = new AnimationSchedulerImplTimer();
+ }
+ }
+
+ INSTANCE = impl;
+ }
+
+ /**
+ * Check if the implementation is natively supported.
+ *
+ * @return true if natively supported, false if not
+ */
+ protected abstract boolean isNativelySupported();
+}
diff --git a/user/src/com/google/gwt/animation/client/AnimationSchedulerImplMozilla.java b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplMozilla.java
new file mode 100644
index 0000000..68d4247
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplMozilla.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2011 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.dom.client.Element;
+
+/**
+ * Implementation using <code>mozRequestAnimationFrame</code>.
+ *
+ * @see <a
+ * href="https://developer.mozilla.org/en/DOM/window.mozRequestAnimationFrame">
+ * Documentation on the MDN</a>
+ */
+class AnimationSchedulerImplMozilla extends AnimationSchedulerImpl {
+
+ /**
+ * Mozilla implementation of {@link AnimationScheduler.AnimationHandle}.
+ * Mozilla does not provide a request ID, so we mark a boolean in the handle
+ * and check it in the callback wrapper.
+ */
+ private class AnimationHandleImpl extends AnimationHandle {
+ @SuppressWarnings("unused")
+ private boolean canceled;
+
+ @Override
+ public void cancel() {
+ canceled = true;
+ }
+ }
+
+ @Override
+ public AnimationHandle requestAnimationFrame(AnimationCallback callback, Element element) {
+ AnimationHandleImpl handle = new AnimationHandleImpl();
+ requestAnimationFrameImpl(callback, handle);
+ return handle;
+ }
+
+ @Override
+ protected native boolean isNativelySupported() /*-{
+ return !!$wnd.mozRequestAnimationFrame;
+ }-*/;
+
+ /**
+ * Request an animation frame. Firefox does not return a request ID, so we
+ * create a JavaScriptObject and add an expando named "canceled" to inidicate
+ * if the request was canceled. The callback wrapper checks the expando before
+ * executing the user callback.
+ *
+ * @param callback the user callback to execute
+ * @param handle the handle object
+ */
+ private native void requestAnimationFrameImpl(AnimationCallback callback,
+ AnimationHandleImpl handle) /*-{
+ var wrapper = $entry(function(time) {
+ if (!handle.@com.google.gwt.animation.client.AnimationSchedulerImplMozilla.AnimationHandleImpl::canceled) {
+ callback.@com.google.gwt.animation.client.AnimationScheduler.AnimationCallback::execute(D)(time);
+ }
+ });
+ $wnd.mozRequestAnimationFrame(wrapper);
+ }-*/;
+}
diff --git a/user/src/com/google/gwt/animation/client/AnimationSchedulerImplTimer.java b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplTimer.java
new file mode 100644
index 0000000..ef2ef05
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplTimer.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2011 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.core.client.Duration;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation using a timer for browsers that do not support animation
+ * frames.
+ */
+class AnimationSchedulerImplTimer extends AnimationSchedulerImpl {
+
+ /**
+ * Timer based implementation of {@link AnimationScheduler.AnimationHandle}.
+ */
+ private class AnimationHandleImpl extends AnimationHandle {
+ private final AnimationCallback callback;
+
+ public AnimationHandleImpl(AnimationCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void cancel() {
+ cancelAnimationFrame(this);
+ }
+
+ public AnimationCallback getCallback() {
+ return callback;
+ }
+ }
+
+ /**
+ * The default time in milliseconds between frames. 60 fps == 16.67 ms.
+ */
+ private static final int DEFAULT_FRAME_DELAY = 16;
+
+ /**
+ * The minimum delay in milliseconds between frames. The minimum delay is
+ * imposed to prevent freezing the UI.
+ */
+ private static final int MIN_FRAME_DELAY = 5;
+
+ /**
+ * The list of animations that are currently running.
+ */
+ private final List<AnimationHandleImpl> animationRequests = new ArrayList<AnimationHandleImpl>();
+
+ /**
+ * The singleton timer that updates all animations.
+ */
+ private final Timer timer = new Timer() {
+ @Override
+ public void run() {
+ updateAnimations();
+ }
+ };
+
+ @Override
+ public AnimationHandle requestAnimationFrame(final AnimationCallback callback, Element element) {
+ // Save the animation frame request.
+ AnimationHandleImpl requestId = new AnimationHandleImpl(callback);
+ animationRequests.add(requestId);
+
+ // Start the timer if it isn't started.
+ if (animationRequests.size() == 1) {
+ timer.schedule(DEFAULT_FRAME_DELAY);
+ }
+
+ // Return the request id.
+ return requestId;
+ }
+
+ @Override
+ protected boolean isNativelySupported() {
+ return true;
+ }
+
+ private void cancelAnimationFrame(AnimationHandle requestId) {
+ // Remove the request from the list.
+ animationRequests.remove(requestId);
+
+ // Stop the timer if there are no more requests.
+ if (animationRequests.size() == 0) {
+ timer.cancel();
+ }
+ }
+
+ /**
+ * Iterate over all animations and update them.
+ */
+ private void updateAnimations() {
+ // Copy the animation requests to avoid concurrent modifications.
+ AnimationHandleImpl[] curAnimations = new AnimationHandleImpl[animationRequests.size()];
+ curAnimations = animationRequests.toArray(curAnimations);
+
+ // Iterate over the animation requests.
+ Duration duration = new Duration();
+ for (AnimationHandleImpl requestId : curAnimations) {
+ // Remove the current request.
+ animationRequests.remove(requestId);
+
+ // Execute the callback.
+ requestId.getCallback().execute(duration.getStartMillis());
+ }
+
+ // Reschedule the timer if there are more animation requests.
+ if (animationRequests.size() > 0) {
+ /*
+ * In order to achieve as close to 60fps as possible, we calculate the new
+ * delay based on the execution time of this method. The delay will be
+ * less than 16ms, assuming this method takes more than 1ms to complete.
+ */
+ timer.schedule(Math.max(MIN_FRAME_DELAY, DEFAULT_FRAME_DELAY - duration.elapsedMillis()));
+ }
+ }
+}
diff --git a/user/src/com/google/gwt/animation/client/AnimationSchedulerImplWebkit.java b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplWebkit.java
new file mode 100644
index 0000000..82e8b36
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/AnimationSchedulerImplWebkit.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 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.dom.client.Element;
+
+/**
+ * Implementation using <code>webkitRequestAnimationFrame</code> and
+ * <code>webkitCancelRequestAnimationFrame</code>.
+ *
+ * @see <a
+ * href="http://www.chromium.org/developers/web-platform-status#TOC-requestAnimationFrame">
+ * Chromium Web Platform Status</a>
+ * @see <a href="http://webstuff.nfshost.com/anim-timing/Overview.html"> webkit
+ * draft spec</a>
+ */
+class AnimationSchedulerImplWebkit extends AnimationSchedulerImpl {
+
+ /**
+ * Webkit implementation of {@link AnimationScheduler.AnimationHandle}. Webkit
+ * provides the request ID as a double.
+ */
+ private class AnimationHandleImpl extends AnimationHandle {
+ private final double requestId;
+
+ public AnimationHandleImpl(double requestId) {
+ this.requestId = requestId;
+ }
+
+ @Override
+ public void cancel() {
+ cancelAnimationFrameImpl(requestId);
+ }
+ }
+
+ @Override
+ public AnimationHandle requestAnimationFrame(AnimationCallback callback, Element element) {
+ double requestId = requestAnimationFrameImpl(callback, element);
+ return new AnimationHandleImpl(requestId);
+ }
+
+ @Override
+ protected native boolean isNativelySupported() /*-{
+ return !!($wnd.webkitRequestAnimationFrame && $wnd.webkitCancelRequestAnimationFrame);
+ }-*/;
+
+ private native void cancelAnimationFrameImpl(double requestId) /*-{
+ $wnd.webkitCancelRequestAnimationFrame(requestId);
+ }-*/;
+
+ private native double requestAnimationFrameImpl(AnimationCallback callback, Element element) /*-{
+ var _callback = callback;
+ var wrapper = $entry(function(time) {
+ // Chrome 10 does not pass the 'time' argument, so we fake it.
+ time = time || @com.google.gwt.core.client.Duration::currentTimeMillis()();
+ _callback.@com.google.gwt.animation.client.AnimationScheduler.AnimationCallback::execute(D)(time);
+ });
+ return $wnd.webkitRequestAnimationFrame(wrapper, element);
+ }-*/;
+}
diff --git a/user/src/com/google/gwt/animation/client/testing/StubAnimationScheduler.java b/user/src/com/google/gwt/animation/client/testing/StubAnimationScheduler.java
new file mode 100644
index 0000000..148207e
--- /dev/null
+++ b/user/src/com/google/gwt/animation/client/testing/StubAnimationScheduler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2011 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.testing;
+
+import com.google.gwt.animation.client.AnimationScheduler;
+import com.google.gwt.dom.client.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A stub implementation of {@link AnimationScheduler} that does not execute the
+ * callbacks. Use {@link StubAnimationScheduler#getAnimationCallbacks()} to
+ * retrieve and execute callbacks manually.
+ */
+public class StubAnimationScheduler extends AnimationScheduler {
+
+ /**
+ * A handle to the requested animation frame created by
+ * {@link #requestAnimationFrame(AnimationCallback, Element)}.
+ */
+ public class StubAnimationHandle extends AnimationHandle {
+
+ private final AnimationCallback callback;
+
+ public StubAnimationHandle(AnimationCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void cancel() {
+ callbacks.remove(callback);
+ }
+ }
+
+ private final List<AnimationCallback> callbacks = new ArrayList<AnimationCallback>();
+
+ /**
+ * Get the list of all animation callbacks that have been requested and have
+ * not been canceled.
+ *
+ * @return the list of callbacks.
+ */
+ public List<AnimationCallback> getAnimationCallbacks() {
+ return callbacks;
+ }
+
+ @Override
+ public StubAnimationHandle requestAnimationFrame(AnimationCallback callback, Element element) {
+ callbacks.add(callback);
+ return new StubAnimationHandle(callback);
+ }
+}
diff --git a/user/src/com/google/gwt/core/client/Scheduler.java b/user/src/com/google/gwt/core/client/Scheduler.java
index 7fceb6f..9620c48 100644
--- a/user/src/com/google/gwt/core/client/Scheduler.java
+++ b/user/src/com/google/gwt/core/client/Scheduler.java
@@ -22,6 +22,12 @@
* thrown by the command objects executed by the scheduler will be passed to the
* {@link GWT.UncaughtExceptionHandler} if one is installed.
*
+ * <p>
+ * NOTE: If you are using a timer to schedule a UI animation, use
+ * {@link com.google.gwt.animation.client.AnimationScheduler} instead. The
+ * browser can optimize your animation for maximum performance.
+ * </p>
+ *
* @see com.google.gwt.core.client.testing.StubScheduler
*/
public abstract class Scheduler {
diff --git a/user/src/com/google/gwt/layout/client/Layout.java b/user/src/com/google/gwt/layout/client/Layout.java
index df84b04..218432a 100644
--- a/user/src/com/google/gwt/layout/client/Layout.java
+++ b/user/src/com/google/gwt/layout/client/Layout.java
@@ -578,7 +578,7 @@
}
};
- animation.run(duration);
+ animation.run(duration, parentElem);
}
/**
diff --git a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
index 26e53ee..f0d25cd 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
@@ -712,7 +712,7 @@
if (isAnimationEnabled()) {
// Animate the scrolling.
startScrollLeft = elem.getScrollLeft();
- run(250);
+ run(250, elem);
} else {
// Scroll instantly.
onComplete();
diff --git a/user/src/com/google/gwt/user/client/Timer.java b/user/src/com/google/gwt/user/client/Timer.java
index d75e2a6..13333b1 100644
--- a/user/src/com/google/gwt/user/client/Timer.java
+++ b/user/src/com/google/gwt/user/client/Timer.java
@@ -25,8 +25,16 @@
* java.util.Timer, but is simplified because of the single-threaded
* environment.
*
+ * <p>
* To schedule a timer, simply create a subclass of it (overriding {@link #run})
* and call {@link #schedule} or {@link #scheduleRepeating}.
+ * </p>
+ *
+ * <p>
+ * NOTE: If you are using a timer to schedule a UI animation, use
+ * {@link com.google.gwt.animation.client.AnimationScheduler} instead. The
+ * browser can optimize your animation for maximum performance.
+ * </p>
*
* <p>
* <h3>Example</h3>
diff --git a/user/src/com/google/gwt/user/client/ui/DeckPanel.java b/user/src/com/google/gwt/user/client/ui/DeckPanel.java
index b0fd606..393a1ed 100644
--- a/user/src/com/google/gwt/user/client/ui/DeckPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DeckPanel.java
@@ -105,7 +105,25 @@
// Start the animation
if (animate) {
- run(ANIMATION_DURATION);
+ // Figure out if the deck panel has a fixed height
+ com.google.gwt.dom.client.Element deckElem = container1.getParentElement();
+ int deckHeight = deckElem.getOffsetHeight();
+ if (growing) {
+ fixedHeight = container2.getOffsetHeight();
+ container2.getStyle().setPropertyPx("height",
+ Math.max(1, fixedHeight - 1));
+ } else {
+ fixedHeight = container1.getOffsetHeight();
+ container1.getStyle().setPropertyPx("height",
+ Math.max(1, fixedHeight - 1));
+ }
+ if (deckElem.getOffsetHeight() != deckHeight) {
+ fixedHeight = -1;
+ }
+
+ // Only scope to the deck if it's fixed height, otherwise it can affect
+ // the rest of the page, even if it's not visible to the user.
+ run(ANIMATION_DURATION, fixedHeight == -1 ? null : deckElem);
} else {
onInstantaneousRun();
}
@@ -139,22 +157,6 @@
@Override
protected void onStart() {
- // Figure out if the deck panel has a fixed height
- com.google.gwt.dom.client.Element deckElem = container1.getParentElement();
- int deckHeight = deckElem.getOffsetHeight();
- if (growing) {
- fixedHeight = container2.getOffsetHeight();
- container2.getStyle().setPropertyPx("height",
- Math.max(1, fixedHeight - 1));
- } else {
- fixedHeight = container1.getOffsetHeight();
- container1.getStyle().setPropertyPx("height",
- Math.max(1, fixedHeight - 1));
- }
- if (deckElem.getOffsetHeight() != deckHeight) {
- fixedHeight = -1;
- }
-
// Start the animation
DOM.setStyleAttribute(container1, "overflow", "hidden");
DOM.setStyleAttribute(container2, "overflow", "hidden");
diff --git a/user/test/com/google/gwt/animation/AnimationSuite.java b/user/test/com/google/gwt/animation/AnimationSuite.java
new file mode 100644
index 0000000..ef8551f
--- /dev/null
+++ b/user/test/com/google/gwt/animation/AnimationSuite.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 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;
+
+import com.google.gwt.animation.client.AnimationSchedulerImplTimerTest;
+import com.google.gwt.animation.client.AnimationTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * Tests of the animation package.
+ */
+public class AnimationSuite {
+ public static Test suite() {
+ TestSuite suite = new TestSuite("Tests of the animation package");
+
+ suite.addTestSuite(AnimationSchedulerImplTimerTest.class);
+ suite.addTestSuite(AnimationTest.class);
+
+ return suite;
+ }
+}
diff --git a/user/test/com/google/gwt/animation/client/AnimationSchedulerImplTimerTest.java b/user/test/com/google/gwt/animation/client/AnimationSchedulerImplTimerTest.java
new file mode 100644
index 0000000..fffa3fe
--- /dev/null
+++ b/user/test/com/google/gwt/animation/client/AnimationSchedulerImplTimerTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2011 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.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Timer;
+
+/**
+ * Tests the {@link AnimationSchedulerImplTimer} class.
+ */
+public class AnimationSchedulerImplTimerTest extends GWTTestCase {
+
+ /**
+ * The default timeout of asynchronous tests.
+ */
+ private static final int TEST_TIMEOUT = 60000;
+
+ /**
+ * Test maximum expected delay before the scheduler calls the callback. If the
+ * browser tab does not have focus, the browser may dramatically reduce the
+ * rate that timers fire, down to 1000ms.
+ */
+ private static final int TIMER_DELAY = 3000;
+
+ private AnimationSchedulerImplTimer scheduler;
+
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.animation.Animation";
+ }
+
+ public void testCancel() {
+ delayTestFinish(TEST_TIMEOUT);
+ AnimationHandle handle = scheduler.requestAnimationFrame(new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ fail("The animation frame was cancelled and should not execute.");
+ }
+ }, null);
+
+ // Cancel the animation frame.
+ handle.cancel();
+
+ // Wait to make sure it doesn't execute.
+ new Timer() {
+ @Override
+ public void run() {
+ finishTest();
+ }
+ }.schedule(TIMER_DELAY);
+ }
+
+ public void testRequestAnimationFrame() {
+ delayTestFinish(TEST_TIMEOUT);
+ DivElement element = Document.get().createDivElement();
+ scheduler.requestAnimationFrame(new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ finishTest();
+ }
+ }, element);
+ }
+
+ public void testRequestAnimationFrameWithoutElement() {
+ delayTestFinish(TEST_TIMEOUT);
+ scheduler.requestAnimationFrame(new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ finishTest();
+ }
+ }, null);
+ }
+
+ @Override
+ protected void gwtSetUp() throws Exception {
+ scheduler = new AnimationSchedulerImplTimer();
+ }
+
+ @Override
+ protected void gwtTearDown() throws Exception {
+ scheduler = null;
+ }
+}
diff --git a/user/test/com/google/gwt/animation/client/AnimationTest.java b/user/test/com/google/gwt/animation/client/AnimationTest.java
index 2aebc0a..eb9465b 100644
--- a/user/test/com/google/gwt/animation/client/AnimationTest.java
+++ b/user/test/com/google/gwt/animation/client/AnimationTest.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -15,28 +15,36 @@
*/
package com.google.gwt.animation.client;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
+import com.google.gwt.animation.client.testing.StubAnimationScheduler;
import com.google.gwt.core.client.Duration;
import com.google.gwt.junit.client.GWTTestCase;
-import com.google.gwt.user.client.Timer;
+
+import java.util.List;
/**
* Tests the {@link Animation} class.
+ *
+ * <p>
+ * This class uses the {@link StubAnimationScheduler} to manually trigger
+ * callbacks.
+ * </p>
*/
public class AnimationTest extends GWTTestCase {
- /**
- * Increase this multiplier to increase the duration of the tests, reducing
- * the potential of an error caused by timing issues.
- */
- private static int DELAY_MULTIPLIER = 100;
/**
* A default implementation of {@link Animation} used for testing.
*/
- private static class DefaultAnimation extends Animation {
+ private class DefaultAnimation extends Animation {
protected boolean canceled = false;
protected boolean completed = false;
- protected boolean started = false;
protected double curProgress = -1.0;
+ protected boolean started = false;
+ protected boolean updated = false;
+
+ public DefaultAnimation() {
+ super(scheduler);
+ }
/**
* Assert the value of canceled.
@@ -60,32 +68,28 @@
}
/**
- * Assert that the progress falls between min and max, inclusively.
- */
- public void assertProgressRange(double min, double max) {
- assertTrue(curProgress >= min && curProgress <= max);
- }
-
- /**
* Assert the value of started.
*/
public void assertStarted(boolean expected) {
assertEquals(expected, started);
}
+ /**
+ * Assert the value of updated.
+ */
+ public void assertUpdated(boolean expected) {
+ assertEquals(expected, updated);
+ }
+
public void reset() {
canceled = false;
completed = false;
+ updated = false;
started = false;
curProgress = -1.0;
}
@Override
- protected void onUpdate(double progress) {
- curProgress = progress;
- }
-
- @Override
protected void onCancel() {
super.onCancel();
canceled = true;
@@ -102,18 +106,18 @@
super.onStart();
started = true;
}
+
+ @Override
+ protected void onUpdate(double progress) {
+ updated = true;
+ curProgress = progress;
+ }
}
/**
* A custom {@link Animation} used for testing.
*/
- private static class TestAnimation extends DefaultAnimation {
- /*
- * TODO: Consider timing issues for test system. Specifically, onUpdate is
- * not guaranteed to be called in the Animation timer if we miss our
- * deadline.
- */
-
+ private class TestAnimation extends DefaultAnimation {
@Override
protected void onCancel() {
canceled = true;
@@ -130,140 +134,208 @@
}
}
+ /**
+ * The maximum delay before an animation will run. Animations may run slowly
+ * if the browser tab is not focused.
+ *
+ * Increase this multiplier to increase the duration of the tests, reducing
+ * the potential of an error caused by timing issues.
+ */
+ private static int DELAY_MULTIPLIER = 3000;
+
+ private List<AnimationCallback> callbacks;
+ private double curTime;
+ private StubAnimationScheduler scheduler;
+
@Override
public String getModuleName() {
- return "com.google.gwt.user.User";
- }
-
- /**
- * Test canceling an {@link Animation} before it starts.
- */
- public void testCancelBeforeStarted() {
- final TestAnimation anim = new TestAnimation();
- double curTime = Duration.currentTimeMillis();
- delayTestFinish(20 * DELAY_MULTIPLIER);
- anim.run(10 * DELAY_MULTIPLIER, curTime + 10 * DELAY_MULTIPLIER);
-
- // Check progress
- new Timer() {
- @Override
- public void run() {
- anim.assertStarted(false);
- anim.assertCompleted(false);
- anim.assertProgress(-1.0);
- anim.cancel();
- anim.assertStarted(false);
- anim.assertCancelled(true);
- anim.assertCompleted(false);
- anim.reset();
- }
- }.schedule(5 * DELAY_MULTIPLIER);
-
- // Check progress
- new Timer() {
- @Override
- public void run() {
- anim.assertStarted(false);
- anim.assertCompleted(false);
- anim.assertProgress(-1.0);
- finishTest();
- }
- }.schedule(15 * DELAY_MULTIPLIER);
+ return "com.google.gwt.animation.Animation";
}
/**
* Test canceling an {@link Animation} after it completes.
*/
- public void testCancelWhenComplete() {
+ public void testCancelAfterOnComplete() {
final TestAnimation anim = new TestAnimation();
- delayTestFinish(25 * DELAY_MULTIPLIER);
+ anim.run(DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.reset();
+
+ // Complete the animation.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER + 100);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(true);
+ anim.assertCancelled(false);
+ assertEquals(0, callbacks.size());
+ anim.reset();
+
+ // Cancel the animation.
+ anim.cancel(); // no-op.
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertProgress(-1);
+ anim.assertCancelled(false);
+ }
+
+ /**
+ * Test canceling an {@link Animation} before onStart is called.
+ */
+ public void testCancelBeforeOnStart() {
+ final TestAnimation anim = new TestAnimation();
+
+ // Run the animation in the future.
+ anim.run(DELAY_MULTIPLIER, curTime + 1000);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ assertEquals(1, callbacks.size());
+ anim.reset();
+
+ // Cancel the animation before it starts.
+ anim.cancel();
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(true);
+ assertEquals(0, callbacks.size());
+ }
+
+ /**
+ * Test canceling an {@link Animation} between updates.
+ */
+ public void testCancelBetweenUpdates() {
+ TestAnimation anim = new TestAnimation();
anim.run(10 * DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- anim.assertStarted(true);
- anim.assertCompleted(true);
- anim.assertProgressRange(0.0, 1.0);
- anim.cancel();
- anim.assertCancelled(false);
- anim.assertCompleted(true);
- anim.reset();
- }
- }.schedule(15 * DELAY_MULTIPLIER);
+ // Update the animation.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(true);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- anim.assertStarted(false);
- anim.assertCompleted(false);
- anim.assertProgress(-1.0);
- finishTest();
- }
- }.schedule(20 * DELAY_MULTIPLIER);
+ // Cancel the animation.
+ assertEquals(1, callbacks.size());
+ anim.cancel();
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(true);
+ anim.assertProgress(-1.0);
+ assertEquals(0, callbacks.size());
}
/**
- * Test canceling an {@link Animation} while it is running.
+ * Test canceling an {@link Animation} within onComplete.
*/
- public void testCancelWhileRunning() {
- final TestAnimation anim = new TestAnimation();
- delayTestFinish(20 * DELAY_MULTIPLIER);
- anim.run(50 * DELAY_MULTIPLIER);
-
- // Check progress
- new Timer() {
+ public void testCancelDuringOnComplete() {
+ final TestAnimation anim = new TestAnimation() {
@Override
- public void run() {
- anim.assertStarted(true);
- anim.assertCompleted(false);
- anim.cancel();
- anim.assertCancelled(true);
- anim.assertCompleted(false);
- anim.reset();
- }
- }.schedule(5 * DELAY_MULTIPLIER);
+ protected void onComplete() {
+ super.onComplete();
+ assertStarted(false);
+ assertUpdated(false);
+ assertCompleted(true);
+ assertCancelled(false);
+ reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- anim.assertStarted(false);
- anim.assertCompleted(false);
- anim.assertProgress(-1.0);
- finishTest();
+ // Cancel the animation.
+ cancel(); // no-op.
}
- }.schedule(15 * DELAY_MULTIPLIER);
+ };
+
+ // Run the animation.
+ anim.run(DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.reset();
+
+ // Force the animation to complete.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER + 100);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ assertEquals(0, callbacks.size());
}
/**
- * Test that an animation runs synchronously if its duration is 0.
+ * Test canceling an {@link Animation} within onStart.
*/
- public void testNoDelay() {
- final TestAnimation animNow = new TestAnimation();
- final TestAnimation animPast = new TestAnimation();
- final TestAnimation animFuture = new TestAnimation();
+ public void testCancelDuringOnStart() {
+ final TestAnimation anim = new TestAnimation() {
+ @Override
+ protected void onStart() {
+ super.onStart();
+ assertStarted(true);
+ assertUpdated(false);
+ assertCompleted(false);
+ assertCancelled(false);
+ reset();
- // Run animations
- double curTime = Duration.currentTimeMillis();
- animNow.run(0);
- animPast.run(0, curTime - 15 * DELAY_MULTIPLIER);
- animFuture.run(0, curTime + 15 * DELAY_MULTIPLIER);
+ // Cancel the animation.
+ cancel();
+ }
+ };
- // Test synchronous start
- animNow.assertStarted(true);
- animNow.assertCompleted(true);
- animNow.assertProgress(-1.0);
+ // Run the animation.
+ anim.run(DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCancelled(true);
+ anim.assertCompleted(false);
+ assertEquals(0, callbacks.size());
+ }
- animPast.assertStarted(true);
- animPast.assertCompleted(true);
- animPast.assertProgress(-1.0);
+ /**
+ * Test canceling an {@link Animation} during an update.
+ */
+ public void testCancelDuringOnUpdate() {
+ final TestAnimation anim = new TestAnimation() {
+ @Override
+ protected void onUpdate(double progress) {
+ super.onUpdate(progress);
+ assertStarted(false);
+ assertUpdated(true);
+ assertCompleted(false);
+ assertCancelled(false);
+ reset();
- animFuture.assertStarted(false);
- animFuture.assertCompleted(false);
- animFuture.assertProgress(-1.0);
+ // Cancel the test while it is running.
+ cancel();
+ }
+ };
+
+ // Run the animation.
+ anim.run(10 * DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.assertProgress(-1.0);
+ anim.reset();
+
+ // Force the update.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(true);
+ anim.assertProgress(-1.0);
+ assertEquals(0, callbacks.size());
}
/**
@@ -272,10 +344,10 @@
public void testDefaultAnimation() {
// Verify initial state
final DefaultAnimation anim = new DefaultAnimation();
- anim.assertProgress(-1.0);
anim.assertStarted(false);
- anim.assertCompleted(false);
+ anim.assertUpdated(false);
anim.assertCancelled(false);
+ anim.assertCompleted(false);
// Starting an animation calls onUpdate(interpolate(0.0))
anim.reset();
@@ -305,8 +377,7 @@
// Canceling an animation before it starts does not call onStart or
// onComplete
anim.reset();
- anim.run(20 * DELAY_MULTIPLIER, Duration.currentTimeMillis() + 100
- * DELAY_MULTIPLIER);
+ anim.run(10 * DELAY_MULTIPLIER, curTime + DELAY_MULTIPLIER);
anim.cancel();
anim.assertProgress(-1.0);
anim.assertStarted(false);
@@ -315,123 +386,164 @@
}
/**
- * Test general functionality.
+ * Test that restarting an {@link Animation} within onComplete does not break.
+ * See issue 5639.
*/
- public void testRun() {
- final TestAnimation animNow = new TestAnimation();
- final TestAnimation animPast = new TestAnimation();
- final TestAnimation animFuture = new TestAnimation();
-
- delayTestFinish(50 * DELAY_MULTIPLIER);
- // Run animations
- double curTime = Duration.currentTimeMillis();
- animNow.run(30 * DELAY_MULTIPLIER);
- animPast.run(30 * DELAY_MULTIPLIER, curTime - 10 * DELAY_MULTIPLIER);
- animFuture.run(30 * DELAY_MULTIPLIER, curTime + 10 * DELAY_MULTIPLIER);
-
- // Test synchronous start
- animNow.assertStarted(true);
- animNow.assertCompleted(false);
- animNow.assertProgress(-1.0);
-
- animPast.assertStarted(true);
- animPast.assertCompleted(false);
- animPast.assertProgress(-1.0);
-
- animFuture.assertStarted(false);
- animFuture.assertCompleted(false);
- animFuture.assertProgress(-1.0);
-
- // Check progress
- new Timer() {
+ public void testRunDuringOnComplete() {
+ final TestAnimation anim = new TestAnimation() {
@Override
- public void run() {
- animNow.assertStarted(true);
- animNow.assertCompleted(false);
- animNow.assertProgressRange(0.0, 1.0);
+ protected void onComplete() {
+ super.onComplete();
+ assertStarted(false);
+ assertUpdated(false);
+ assertCompleted(true);
+ assertCancelled(false);
+ reset();
- animPast.assertStarted(true);
- animPast.assertCompleted(false);
- animPast.assertProgressRange(0.0, 1.0);
-
- animFuture.assertStarted(false);
- animFuture.assertCompleted(false);
- animFuture.assertProgress(-1.0);
+ // Run the animation.
+ run(DELAY_MULTIPLIER);
}
- }.schedule(5 * DELAY_MULTIPLIER);
+ };
- // Check progress
- new Timer() {
- @Override
- public void run() {
- animNow.assertStarted(true);
- animNow.assertCompleted(false);
- animNow.assertProgressRange(0.0, 1.0);
+ // Run the animation.
+ anim.run(DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ anim.reset();
- animPast.assertStarted(true);
- animPast.assertCompleted(false);
- animPast.assertProgressRange(0.0, 1.0);
+ // Force the animation to complete.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER + 100);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.assertCancelled(false);
+ assertEquals(1, callbacks.size());
+ }
- animFuture.assertStarted(true);
- animFuture.assertCompleted(false);
- animFuture.assertProgressRange(0.0, 1.0);
- }
- }.schedule(15 * DELAY_MULTIPLIER);
+ /**
+ * Test that an animation runs in the future.
+ */
+ public void testRunFuture() {
+ final TestAnimation anim = new TestAnimation();
+ anim.run(2 * DELAY_MULTIPLIER, curTime + 2 * DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- animNow.assertStarted(true);
- animNow.assertCompleted(false);
- animNow.assertProgressRange(0.0, 1.0);
+ // Update, but still before the start time.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.reset();
- animPast.assertStarted(true);
- animPast.assertCompleted(true);
- animPast.assertProgressRange(0.0, 1.0);
+ // Start the animation.
+ executeLastCallbackAt(curTime + 2 * DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.reset();
- animFuture.assertStarted(true);
- animFuture.assertCompleted(false);
- animFuture.assertProgressRange(0.0, 1.0);
- }
- }.schedule(25 * DELAY_MULTIPLIER);
+ // Update the animation.
+ executeLastCallbackAt(curTime + 3 * DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(true);
+ anim.assertCompleted(false);
+ anim.reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- animNow.assertStarted(true);
- animNow.assertCompleted(true);
- animNow.assertProgressRange(0.0, 1.0);
+ // Complete the animation.
+ executeLastCallbackAt(curTime + 4 * DELAY_MULTIPLIER + 100);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(true);
+ }
- animPast.assertStarted(true);
- animPast.assertCompleted(true);
- animPast.assertProgressRange(0.0, 1.0);
+ /**
+ * Test that an animation runs synchronously if its duration is 0.
+ */
+ public void testRunNow() {
+ final TestAnimation anim = new TestAnimation();
+ anim.run(2 * DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.reset();
- animFuture.assertStarted(true);
- animFuture.assertCompleted(false);
- animFuture.assertProgressRange(0.0, 1.0);
- }
- }.schedule(35 * DELAY_MULTIPLIER);
+ // Update the progress.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(true);
+ anim.assertCompleted(false);
+ anim.reset();
- // Check progress
- new Timer() {
- @Override
- public void run() {
- animNow.assertStarted(true);
- animNow.assertCompleted(true);
- animNow.assertProgressRange(0.0, 1.0);
+ // Complete the animation.
+ executeLastCallbackAt(curTime + 2 * DELAY_MULTIPLIER + 100);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(true);
+ }
- animPast.assertStarted(true);
- animPast.assertCompleted(true);
- animPast.assertProgressRange(0.0, 1.0);
+ /**
+ * Test running an animation that started in the past.
+ */
+ public void testRunPast() {
+ final TestAnimation anim = new TestAnimation();
+ anim.run(3 * DELAY_MULTIPLIER, curTime - DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(false);
+ anim.reset();
- animFuture.assertStarted(true);
- animFuture.assertCompleted(true);
- animFuture.assertProgressRange(0.0, 1.0);
+ // Update the progress.
+ executeLastCallbackAt(curTime + DELAY_MULTIPLIER);
+ anim.assertStarted(false);
+ anim.assertUpdated(true);
+ anim.assertCompleted(false);
+ anim.reset();
- finishTest();
- }
- }.schedule(45 * DELAY_MULTIPLIER);
+ // Complete the animation.
+ executeLastCallbackAt(curTime + 2 * DELAY_MULTIPLIER + 100);
+ anim.assertStarted(false);
+ anim.assertUpdated(false);
+ anim.assertCompleted(true);
+ }
+
+ /**
+ * Test running an animation that started and finished in the past.
+ */
+ public void testRunPaster() {
+ final TestAnimation anim = new TestAnimation();
+ anim.run(DELAY_MULTIPLIER, curTime - 2 * DELAY_MULTIPLIER);
+ anim.assertStarted(true);
+ anim.assertUpdated(false);
+ anim.assertCompleted(true);
+ }
+
+ @Override
+ protected void gwtSetUp() throws Exception {
+ scheduler = new StubAnimationScheduler();
+ callbacks = scheduler.getAnimationCallbacks();
+ curTime = Duration.currentTimeMillis();
+ }
+
+ @Override
+ protected void gwtTearDown() throws Exception {
+ scheduler = null;
+ callbacks = null;
+ }
+
+ /**
+ * Execute the last callback requested from the scheduler at the specified
+ * time.
+ *
+ * @param timestamp the time to pass to the callback
+ */
+ private void executeLastCallbackAt(double timestamp) {
+ assertTrue(callbacks.size() > 0);
+ AnimationCallback callback = callbacks.remove(callbacks.size() - 1);
+ callback.execute(timestamp);
}
}