Updated logic of animation code by defining default implementations of onStart, onComplete, and onCancel.  Modified existing widget animations to take advantage of the new code.

Patch by: jlabanca, scottb
Review by: jat, kelly



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2900 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/other/CwAnimation.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/other/CwAnimation.java
index 73d6619..9122ee9 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/other/CwAnimation.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/other/CwAnimation.java
@@ -68,26 +68,21 @@
     private int radius = 100;
 
     @Override
-    public void onCancel() {
-      onComplete();
-    }
-
-    @Override
-    public void onComplete() {
-      onUpdate(1.0);
+    protected void onComplete() {
+      super.onComplete();
       startButton.setEnabled(true);
       cancelButton.setEnabled(false);
     }
 
     @Override
-    public void onStart() {
-      onUpdate(1.0);
+    protected void onStart() {
+      super.onStart();
       startButton.setEnabled(false);
       cancelButton.setEnabled(true);
     }
 
     @Override
-    public void onUpdate(double progress) {
+    protected void onUpdate(double progress) {
       double radian = 2 * Math.PI * progress;
       updatePosition(animateeLeft, radian, 0);
       updatePosition(animateeBottom, radian, 0.5 * Math.PI);
diff --git a/user/src/com/google/gwt/animation/client/Animation.java b/user/src/com/google/gwt/animation/client/Animation.java
index 9e55db7..b90fa72 100644
--- a/user/src/com/google/gwt/animation/client/Animation.java
+++ b/user/src/com/google/gwt/animation/client/Animation.java
@@ -32,7 +32,7 @@
   private static final int DEFAULT_FRAME_DELAY = 25;
 
   /**
-   * The {@link Animation}s that are currently in progress.
+   * The {@link Animation Animations} that are currently in progress.
    */
   private static List<Animation> animations = null;
 
@@ -42,7 +42,7 @@
   private static Timer animationTimer = null;
 
   /**
-   * Update all {@link Animation}s.
+   * Update all {@link Animation Animations}.
    */
   private static void updateAnimations() {
     // Iterator through the animations
@@ -77,19 +77,20 @@
   private double startTime = -1;
 
   /**
-   * Immediately cancel this animation.
+   * Immediately cancel this animation. If the animation is running or is
+   * scheduled to run, {@link #onCancel()} will be called.
    */
   public void cancel() {
-    // No animations available
+    // Ignore if no animations are running
     if (animations == null) {
       return;
     }
 
-    // Remove the animation
-    started = false;
+    // Remove this animation from the list
     if (animations.remove(this)) {
       onCancel();
     }
+    started = false;
   }
 
   /**
@@ -155,27 +156,38 @@
   }
 
   /**
-   * Called immediately after the animation is canceled.
+   * 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 abstract void onCancel();
+  protected void onCancel() {
+    if (started) {
+      started = false;
+      onComplete();
+    }
+  }
 
   /**
    * Called immediately after the animation completes.
    */
-  protected abstract void onComplete();
+  protected void onComplete() {
+    onUpdate(interpolate(1.0));
+  }
 
   /**
    * Called immediately before the animation starts.
    */
-  protected abstract void onStart();
+  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 inclusively, but it is not
-   * safe to assume that either 0.0 or 1.0 will be passed in. Use
-   * {@link #onStart()} and {@link #onComplete()} to do setup and tear down
-   * procedures.
+   * The value of progress is between 0.0 and 1.0 inclusively (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.
    */
   protected abstract void onUpdate(double progress);
 
@@ -186,21 +198,24 @@
    * @return true if the animation is complete, false if still running
    */
   private boolean update(double curTime) {
-    // Start the animation
+    boolean finished = curTime >= startTime + duration;
+    if (started && !finished) {
+      // Animation is in progress.
+      double progress = (curTime - startTime) / duration;
+      onUpdate(interpolate(progress));
+      return false;
+    }
     if (!started && curTime >= startTime) {
+      // Start the animation.
       started = true;
       onStart();
+      // Intentional fall through to possibly end the animation.
     }
-
-    if (curTime >= startTime + duration) {
-      // Animation is complete
+    if (finished) {
+      // Animation is complete.
       started = false;
       onComplete();
       return true;
-    } else if (curTime >= startTime) {
-      // Animation is in progress
-      double progress = (curTime - startTime) / duration;
-      onUpdate(interpolate(progress));
     }
     return false;
   }
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 8861888..4ee7274 100644
--- a/user/src/com/google/gwt/user/client/ui/DeckPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DeckPanel.java
@@ -33,6 +33,11 @@
  */
 public class DeckPanel extends ComplexPanel implements HasAnimation {
   /**
+   * The duration of the animation. 
+   */
+  private static final int ANIMATION_DURATION = 350;
+  
+  /**
    * An {@link Animation} used to slide in the new content.
    */
   private static class SlideAnimation extends Animation {
@@ -57,94 +62,6 @@
      */
     private int fixedHeight = -1;
 
-    @Override
-    public void onCancel() {
-      onComplete();
-    }
-
-    @Override
-    public void onComplete() {
-      if (growing) {
-        onUpdate(1.0);
-        DOM.setStyleAttribute(container1, "height", "100%");
-        UIObject.setVisible(container1, true);
-        UIObject.setVisible(container2, false);
-        DOM.setStyleAttribute(container2, "height", "100%");
-      } else {
-        UIObject.setVisible(container1, false);
-        DOM.setStyleAttribute(container1, "height", "100%");
-        DOM.setStyleAttribute(container2, "height", "100%");
-        UIObject.setVisible(container2, true);
-      }
-      DOM.setStyleAttribute(container1, "overflow", "visible");
-      DOM.setStyleAttribute(container2, "overflow", "visible");
-      container1 = null;
-      container2 = null;
-    }
-
-    public void onInstantaneousRun() {
-      UIObject.setVisible(container1, growing);
-      UIObject.setVisible(container2, !growing);
-      container1 = null;
-      container2 = null;
-    }
-
-    @Override
-    public 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", fixedHeight - 1);
-      } else {
-        fixedHeight = container1.getOffsetHeight();
-        container1.getStyle().setPropertyPx("height", fixedHeight - 1);
-      }
-      if (deckElem.getOffsetHeight() != deckHeight) {
-        fixedHeight = -1;
-      }
-
-      // Start the animation
-      DOM.setStyleAttribute(container1, "overflow", "hidden");
-      DOM.setStyleAttribute(container2, "overflow", "hidden");
-      onUpdate(0.0);
-      UIObject.setVisible(container1, true);
-      UIObject.setVisible(container2, true);
-    }
-
-    @Override
-    public void onUpdate(double progress) {
-      if (!growing) {
-        progress = 1.0 - progress;
-      }
-
-      // Container1 expands (shrinks) to its target height
-      int height1;
-      int height2;
-      if (fixedHeight == -1) {
-        height1 = (int) (progress * DOM.getElementPropertyInt(container1,
-            "scrollHeight"));
-        height2 = (int) ((1.0 - progress) * DOM.getElementPropertyInt(
-            container2, "scrollHeight"));
-      } else {
-        height1 = (int) (progress * fixedHeight);
-        height2 = fixedHeight - height1;
-      }
-
-      // Issue 2339: If the height is 0px, IE7 will display the entire content
-      // widget instead of hiding it completely.
-      if (height1 == 0) {
-        height1 = 1;
-        height2 = Math.max(1, height2 - 1);
-      } else if (height2 == 0) {
-        height2 = 1;
-        height1 = Math.max(1, height1 - 1);
-      }
-      DOM.setStyleAttribute(container1, "height", height1 + "px");
-      DOM.setStyleAttribute(container2, "height", height2 + "px");
-    }
-
     /**
      * Switch to a new {@link Widget}.
      * 
@@ -185,11 +102,93 @@
 
       // Start the animation
       if (animate) {
-        run(350);
+        run(ANIMATION_DURATION);
       } else {
         onInstantaneousRun();
       }
     }
+
+    @Override
+    protected void onComplete() {
+      if (growing) {
+        DOM.setStyleAttribute(container1, "height", "100%");
+        UIObject.setVisible(container1, true);
+        UIObject.setVisible(container2, false);
+        DOM.setStyleAttribute(container2, "height", "100%");
+      } else {
+        UIObject.setVisible(container1, false);
+        DOM.setStyleAttribute(container1, "height", "100%");
+        DOM.setStyleAttribute(container2, "height", "100%");
+        UIObject.setVisible(container2, true);
+      }
+      DOM.setStyleAttribute(container1, "overflow", "visible");
+      DOM.setStyleAttribute(container2, "overflow", "visible");
+      container1 = null;
+      container2 = null;
+    }
+
+    @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", fixedHeight - 1);
+      } else {
+        fixedHeight = container1.getOffsetHeight();
+        container1.getStyle().setPropertyPx("height", fixedHeight - 1);
+      }
+      if (deckElem.getOffsetHeight() != deckHeight) {
+        fixedHeight = -1;
+      }
+
+      // Start the animation
+      DOM.setStyleAttribute(container1, "overflow", "hidden");
+      DOM.setStyleAttribute(container2, "overflow", "hidden");
+      onUpdate(0.0);
+      UIObject.setVisible(container1, true);
+      UIObject.setVisible(container2, true);
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      if (!growing) {
+        progress = 1.0 - progress;
+      }
+
+      // Container1 expands (shrinks) to its target height
+      int height1;
+      int height2;
+      if (fixedHeight == -1) {
+        height1 = (int) (progress * DOM.getElementPropertyInt(container1,
+            "scrollHeight"));
+        height2 = (int) ((1.0 - progress) * DOM.getElementPropertyInt(
+            container2, "scrollHeight"));
+      } else {
+        height1 = (int) (progress * fixedHeight);
+        height2 = fixedHeight - height1;
+      }
+
+      // Issue 2339: If the height is 0px, IE7 will display the entire content
+      // widget instead of hiding it completely.
+      if (height1 == 0) {
+        height1 = 1;
+        height2 = Math.max(1, height2 - 1);
+      } else if (height2 == 0) {
+        height2 = 1;
+        height1 = Math.max(1, height1 - 1);
+      }
+      DOM.setStyleAttribute(container1, "height", height1 + "px");
+      DOM.setStyleAttribute(container2, "height", height2 + "px");
+    }
+
+    private void onInstantaneousRun() {
+      UIObject.setVisible(container1, growing);
+      UIObject.setVisible(container2, !growing);
+      container1 = null;
+      container2 = null;
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java b/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
index 89ff7e6..6e7b243 100644
--- a/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DisclosurePanel.java
@@ -50,6 +50,11 @@
 public final class DisclosurePanel extends Composite implements
     FiresDisclosureEvents, HasWidgets, HasAnimation {
   /**
+   * The duration of the animation. 
+   */
+  private static final int ANIMATION_DURATION = 350;
+
+  /**
    * An {@link Animation} used to open the content.
    */
   private static class ContentAnimation extends Animation {
@@ -63,44 +68,6 @@
      */
     private DisclosurePanel curPanel;
 
-    @Override
-    public void onCancel() {
-      onComplete();
-    }
-
-    @Override
-    public void onComplete() {
-      if (!opening) {
-        curPanel.contentWrapper.setVisible(false);
-      }
-      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height",
-          "auto");
-      curPanel = null;
-    }
-
-    @Override
-    public void onStart() {
-      onUpdate(0.0);
-      if (opening) {
-        curPanel.contentWrapper.setVisible(true);
-      }
-    }
-
-    @Override
-    public void onUpdate(double progress) {
-      int scrollHeight = DOM.getElementPropertyInt(
-          curPanel.contentWrapper.getElement(), "scrollHeight");
-      int height = (int) (progress * scrollHeight);
-      if (!opening) {
-        height = scrollHeight - height;
-      }
-      height = Math.max(height, 1);
-      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height",
-          height + "px");
-      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "width",
-          "auto");
-    }
-
     /**
      * Open or close the content.
      * 
@@ -115,11 +82,44 @@
       if (animate) {
         curPanel = panel;
         opening = panel.isOpen;
-        run(350);
+        run(ANIMATION_DURATION);
       } else {
         panel.contentWrapper.setVisible(panel.isOpen);
       }
     }
+
+    @Override
+    protected void onComplete() {
+      if (!opening) {
+        curPanel.contentWrapper.setVisible(false);
+      }
+      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height",
+          "auto");
+      curPanel = null;
+    }
+
+    @Override
+    protected void onStart() {
+      super.onStart();
+      if (opening) {
+        curPanel.contentWrapper.setVisible(true);
+      }
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      int scrollHeight = DOM.getElementPropertyInt(
+          curPanel.contentWrapper.getElement(), "scrollHeight");
+      int height = (int) (progress * scrollHeight);
+      if (!opening) {
+        height = scrollHeight - height;
+      }
+      height = Math.max(height, 1);
+      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height",
+          height + "px");
+      DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "width",
+          "auto");
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/PopupPanel.java b/user/src/com/google/gwt/user/client/ui/PopupPanel.java
index eefc1b1..30be11d 100644
--- a/user/src/com/google/gwt/user/client/ui/PopupPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/PopupPanel.java
@@ -49,6 +49,11 @@
 public class PopupPanel extends SimplePanel implements SourcesPopupEvents,
     EventPreview, HasAnimation {
   /**
+   * The duration of the animation. 
+   */
+  private static final int ANIMATION_DURATION = 200;
+
+  /**
    * The type of animation to use when opening the popup.
    * 
    * <ul>
@@ -57,7 +62,7 @@
    * hiding</li>
    * </ul>
    */
-  public static enum AnimationType {
+  static enum AnimationType {
     CENTER, ONE_WAY_CORNER
   }
 
@@ -80,13 +85,35 @@
      */
     private boolean showing = false;
 
-    @Override
-    public void onCancel() {
-      onComplete();
+    /**
+     * Open or close the content. This method always called immediately after
+     * the PopupPanel showing state has changed, so we base the animation on the
+     * current state.
+     * 
+     * @param panel the panel to open or close
+     */
+    public void setOpen(final PopupPanel panel) {
+      // Immediately complete previous open/close animation
+      cancel();
+
+      // Determine if we need to animate
+      boolean animate = panel.isAnimationEnabled;
+      if (panel.animType == AnimationType.ONE_WAY_CORNER && !panel.showing) {
+        animate = false;
+      }
+
+      // Open the new item
+      showing = panel.showing;
+      curPanel = panel;
+      if (animate) {
+        run(ANIMATION_DURATION);
+      } else {
+        onInstantaneousRun();
+      }
     }
 
     @Override
-    public void onComplete() {
+    protected void onComplete() {
       if (!showing) {
         RootPanel.get().remove(curPanel);
         impl.onHide(curPanel.getElement());
@@ -96,27 +123,8 @@
       curPanel = null;
     }
 
-    public void onInstantaneousRun() {
-      if (showing) {
-        // Set the position attribute, and then attach to the DOM. Otherwise,
-        // the PopupPanel will appear to 'jump' from its static/relative
-        // position to its absolute position (issue #1231).
-        DOM.setStyleAttribute(curPanel.getElement(), "position", "absolute");
-        if (curPanel.topPosition != -1) {
-          curPanel.setPopupPosition(curPanel.leftPosition, curPanel.topPosition);
-        }
-        RootPanel.get().add(curPanel);
-        impl.onShow(curPanel.getElement());
-      } else {
-        RootPanel.get().remove(curPanel);
-        impl.onHide(curPanel.getElement());
-      }
-      DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible");
-      curPanel = null;
-    }
-
     @Override
-    public void onStart() {
+    protected void onStart() {
       // Attach to the page
       if (showing) {
         // Set the position attribute, and then attach to the DOM. Otherwise,
@@ -133,11 +141,11 @@
       offsetHeight = curPanel.getOffsetHeight();
       offsetWidth = curPanel.getOffsetWidth();
       DOM.setStyleAttribute(curPanel.getElement(), "overflow", "hidden");
-      onUpdate(0.0);
+      super.onStart();
     }
 
     @Override
-    public void onUpdate(double progress) {
+    protected void onUpdate(double progress) {
       if (!showing) {
         progress = 1.0 - progress;
       }
@@ -166,39 +174,31 @@
     }
 
     /**
-     * Open or close the content. This method always called immediately after
-     * the PopupPanel showing state has changed, so we base the animation on the
-     * current state.
-     * 
-     * @param panel the panel to open or close
-     */
-    public void setOpen(final PopupPanel panel) {
-      // Immediately complete previous open/close animation
-      cancel();
-
-      // Determine if we need to animate
-      boolean animate = panel.isAnimationEnabled;
-      if (panel.animType == AnimationType.ONE_WAY_CORNER && !panel.showing) {
-        animate = false;
-      }
-
-      // Open the new item
-      showing = panel.showing;
-      curPanel = panel;
-      if (animate) {
-        run(200);
-      } else {
-        onInstantaneousRun();
-      }
-    }
-
-    /**
      * @return a rect string
      */
     private String getRectString(int top, int right, int bottom, int left) {
       return "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left
           + "px)";
     }
+
+    private void onInstantaneousRun() {
+      if (showing) {
+        // Set the position attribute, and then attach to the DOM. Otherwise,
+        // the PopupPanel will appear to 'jump' from its static/relative
+        // position to its absolute position (issue #1231).
+        DOM.setStyleAttribute(curPanel.getElement(), "position", "absolute");
+        if (curPanel.topPosition != -1) {
+          curPanel.setPopupPosition(curPanel.leftPosition, curPanel.topPosition);
+        }
+        RootPanel.get().add(curPanel);
+        impl.onShow(curPanel.getElement());
+      } else {
+        RootPanel.get().remove(curPanel);
+        impl.onHide(curPanel.getElement());
+      }
+      DOM.setStyleAttribute(curPanel.getElement(), "overflow", "visible");
+      curPanel = null;
+    }
   }
 
   /**
@@ -658,15 +658,6 @@
   }
 
   /**
-   * Enable or disable animation of the {@link PopupPanel}.
-   * 
-   * @param type the type of animation to use
-   */
-  protected void setAnimationType(AnimationType type) {
-    animType = type;
-  }
-
-  /**
    * We control size by setting our child widget's size. However, if we don't
    * currently have a child, we record the size the user wanted so that when we
    * do get a child, we can set it correctly. Until size is explicitly cleared,
@@ -693,6 +684,15 @@
   }
 
   /**
+   * Enable or disable animation of the {@link PopupPanel}.
+   * 
+   * @param type the type of animation to use
+   */
+  void setAnimationType(AnimationType type) {
+    animType = type;
+  }
+
+  /**
    * Remove focus from an Element.
    * 
    * @param elt The Element on which <code>blur()</code> will be invoked
diff --git a/user/src/com/google/gwt/user/client/ui/TreeItem.java b/user/src/com/google/gwt/user/client/ui/TreeItem.java
index 9c033dc..511a67d7 100644
--- a/user/src/com/google/gwt/user/client/ui/TreeItem.java
+++ b/user/src/com/google/gwt/user/client/ui/TreeItem.java
@@ -36,7 +36,6 @@
  * </p>
  */
 public class TreeItem extends UIObject implements HasHTML {
-
   /**
    * An {@link Animation} used to open the child elements. If a {@link TreeItem}
    * is in the process of opening, it will immediately be opened and the new
@@ -54,13 +53,29 @@
      */
     private boolean opening = true;
 
-    @Override
-    public void onCancel() {
-      onComplete();
+    /**
+     * Open the specified {@link TreeItem}.
+     * 
+     * @param item the {@link TreeItem} to open
+     * @param animate true to animate, false to open instantly
+     */
+    public void setItemState(TreeItem item, boolean animate) {
+      // Immediately complete previous open
+      cancel();
+
+      // Open the new item
+      if (animate) {
+        curItem = item;
+        opening = item.open;
+        run(Math.min(ANIMATION_DURATION, ANIMATION_DURATION_PER_ITEM
+            * curItem.getChildCount()));
+      } else {
+        UIObject.setVisible(item.childSpanElem, item.open);
+      }
     }
 
     @Override
-    public void onComplete() {
+    protected void onComplete() {
       if (curItem != null) {
         if (opening) {
           UIObject.setVisible(curItem.childSpanElem, true);
@@ -76,16 +91,16 @@
     }
 
     @Override
-    public void onStart() {
+    protected void onStart() {
       DOM.setStyleAttribute(curItem.childSpanElem, "overflow", "hidden");
-      onUpdate(0.0);
+      super.onStart();
       if (opening) {
         UIObject.setVisible(curItem.childSpanElem, true);
       }
     }
 
     @Override
-    public void onUpdate(double progress) {
+    protected void onUpdate(double progress) {
       int scrollHeight = DOM.getElementPropertyInt(curItem.childSpanElem,
           "scrollHeight");
 
@@ -105,26 +120,6 @@
           "scrollWidth");
       DOM.setStyleAttribute(curItem.childSpanElem, "width", scrollWidth + "px");
     }
-
-    /**
-     * Open the specified {@link TreeItem}.
-     * 
-     * @param item the {@link TreeItem} to open
-     * @param animate true to animate, false to open instantly
-     */
-    public void setItemState(TreeItem item, boolean animate) {
-      // Immediately complete previous open
-      cancel();
-
-      // Open the new item
-      if (animate) {
-        curItem = item;
-        opening = item.open;
-        run(Math.min(200, 75 * curItem.getChildCount()));
-      } else {
-        UIObject.setVisible(item.childSpanElem, item.open);
-      }
-    }
   }
 
   // By not overwriting the default tree padding and spacing, we traditionally
@@ -135,7 +130,19 @@
   static final int IMAGE_PAD = 7;
 
   /**
-   * The static animation used to open {@link TreeItem}s.
+   * The duration of the animation. 
+   */
+  private static final int ANIMATION_DURATION = 200;
+
+  /**
+   * The duration of the animation per child {@link TreeItem}. If the per item
+   * duration times the number of child items is less than the duration above,
+   * the smaller duration will be used.
+   */
+  private static final int ANIMATION_DURATION_PER_ITEM = 75;
+
+  /**
+   * The static animation used to open {@link TreeItem TreeItems}.
    */
   private static TreeItemAnimation itemAnimation = new TreeItemAnimation();
 
diff --git a/user/test/com/google/gwt/animation/client/AnimationTest.java b/user/test/com/google/gwt/animation/client/AnimationTest.java
index 874fb29..1ed09cf 100644
--- a/user/test/com/google/gwt/animation/client/AnimationTest.java
+++ b/user/test/com/google/gwt/animation/client/AnimationTest.java
@@ -24,32 +24,53 @@
  */
 public class AnimationTest extends GWTTestCase {
   /**
-   * A customer {@link Animation} used for testing.
+   * Increase this multiplier to increase the duration of the tests, reducing
+   * the potential of an error caused by timing issues.
    */
-  private static class TestAnimation extends Animation {
-    public boolean cancelled = false;
-    public boolean completed = false;
-    public double curProgress = -1.0;
-    public boolean started = false;
+  private static int DELAY_MULTIPLIER = 100;
 
-    @Override
-    public void onCancel() {
-      cancelled = true;
+  /**
+   * A default implementation of {@link Animation} used for testing.
+   */
+  private static class DefaultAnimation extends Animation {
+    protected boolean cancelled = false;
+    protected boolean completed = false;
+    protected boolean started = false;
+    protected double curProgress = -1.0;
+
+    /**
+     * Assert the value of canceled.
+     */
+    public void assertCancelled(boolean expected) {
+      assertEquals(expected, cancelled);
     }
 
-    @Override
-    public void onComplete() {
-      completed = true;
+    /**
+     * Assert the value of completed.
+     */
+    public void assertCompleted(boolean expected) {
+      assertEquals(expected, completed);
     }
 
-    @Override
-    public void onStart() {
-      started = true;
+    /**
+     * Assert that the progress equals the specified value.
+     */
+    public void assertProgress(double expected) {
+      assertEquals(expected, curProgress);
     }
 
-    @Override
-    public void onUpdate(double progress) {
-      curProgress = progress;
+    /**
+     * 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);
     }
 
     public void reset() {
@@ -58,6 +79,55 @@
       started = false;
       curProgress = -1.0;
     }
+
+    @Override
+    protected void onUpdate(double progress) {
+      curProgress = progress;
+    }
+
+    @Override
+    protected void onCancel() {
+      super.onCancel();
+      cancelled = true;
+    }
+
+    @Override
+    protected void onComplete() {
+      super.onComplete();
+      completed = true;
+    }
+
+    @Override
+    protected void onStart() {
+      super.onStart();
+      started = true;
+    }
+  }
+
+  /**
+   * 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.
+     */
+
+    @Override
+    protected void onCancel() {
+      cancelled = true;
+    }
+
+    @Override
+    protected void onComplete() {
+      completed = true;
+    }
+
+    @Override
+    protected void onStart() {
+      started = true;
+    }
   }
 
   @Override
@@ -71,36 +141,36 @@
   public void testCancelBeforeStarted() {
     final TestAnimation anim = new TestAnimation();
     double curTime = Duration.currentTimeMillis();
-    anim.run(100, curTime + 200);
+    anim.run(10 * DELAY_MULTIPLIER, curTime + 10 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertFalse(anim.started);
-        assertFalse(anim.completed);
-        assertEquals(-1.0, anim.curProgress);
+        anim.assertStarted(false);
+        anim.assertCompleted(false);
+        anim.assertProgress(-1.0);
         anim.cancel();
-        assertTrue(anim.cancelled);
-        assertFalse(anim.started);
-        assertFalse(anim.completed);
+        anim.assertStarted(false);
+        anim.assertCancelled(true);
+        anim.assertCompleted(false);
         anim.reset();
       }
-    }.schedule(50);
+    }.schedule(5 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertFalse(anim.started);
-        assertFalse(anim.completed);
-        assertEquals(-1.0, anim.curProgress);
+        anim.assertStarted(false);
+        anim.assertCompleted(false);
+        anim.assertProgress(-1.0);
         finishTest();
       }
-    }.schedule(100);
+    }.schedule(15 * DELAY_MULTIPLIER);
 
     // Wait for test to finish
-    delayTestFinish(150);
+    delayTestFinish(20 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -108,35 +178,35 @@
    */
   public void testCancelWhenComplete() {
     final TestAnimation anim = new TestAnimation();
-    anim.run(100);
+    anim.run(10 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertTrue(anim.started);
-        assertTrue(anim.completed);
-        assertTrue(anim.curProgress > 0.0 && anim.curProgress <= 1.0);
+        anim.assertStarted(true);
+        anim.assertCompleted(true);
+        anim.assertProgressRange(0.0, 1.0);
         anim.cancel();
-        assertFalse(anim.cancelled);
-        assertTrue(anim.completed);
+        anim.assertCancelled(false);
+        anim.assertCompleted(true);
         anim.reset();
       }
-    }.schedule(150);
+    }.schedule(15 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertFalse(anim.started);
-        assertFalse(anim.completed);
-        assertEquals(-1.0, anim.curProgress);
+        anim.assertStarted(false);
+        anim.assertCompleted(false);
+        anim.assertProgress(-1.0);
         finishTest();
       }
-    }.schedule(200);
+    }.schedule(20 * DELAY_MULTIPLIER);
 
     // Wait for test to finish
-    delayTestFinish(250);
+    delayTestFinish(25 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -144,35 +214,34 @@
    */
   public void testCancelWhileRunning() {
     final TestAnimation anim = new TestAnimation();
-    anim.run(500);
+    anim.run(50 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertTrue(anim.started);
-        assertFalse(anim.completed);
-        assertTrue(anim.curProgress > 0.0 && anim.curProgress <= 1.0);
+        anim.assertStarted(true);
+        anim.assertCompleted(false);
         anim.cancel();
-        assertTrue(anim.cancelled);
-        assertFalse(anim.completed);
+        anim.assertCancelled(true);
+        anim.assertCompleted(false);
         anim.reset();
       }
-    }.schedule(50);
+    }.schedule(5 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertFalse(anim.started);
-        assertFalse(anim.completed);
-        assertEquals(-1.0, anim.curProgress);
+        anim.assertStarted(false);
+        anim.assertCompleted(false);
+        anim.assertProgress(-1.0);
         finishTest();
       }
-    }.schedule(150);
+    }.schedule(15 * DELAY_MULTIPLIER);
 
     // Wait for test to finish
-    delayTestFinish(200);
+    delayTestFinish(20 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -186,21 +255,69 @@
     // Run animations
     double curTime = Duration.currentTimeMillis();
     animNow.run(0);
-    animPast.run(0, curTime - 150);
-    animFuture.run(0, curTime + 150);
+    animPast.run(0, curTime - 15 * DELAY_MULTIPLIER);
+    animFuture.run(0, curTime + 15 * DELAY_MULTIPLIER);
 
     // Test synchronous start
-    assertTrue(animNow.started);
-    assertTrue(animNow.completed);
-    assertEquals(-1.0, animNow.curProgress);
+    animNow.assertStarted(true);
+    animNow.assertCompleted(true);
+    animNow.assertProgress(-1.0);
 
-    assertTrue(animPast.started);
-    assertTrue(animPast.completed);
-    assertEquals(-1.0, animFuture.curProgress);
+    animPast.assertStarted(true);
+    animPast.assertCompleted(true);
+    animPast.assertProgress(-1.0);
 
-    assertFalse(animFuture.started);
-    assertFalse(animFuture.completed);
-    assertEquals(-1.0, animFuture.curProgress);
+    animFuture.assertStarted(false);
+    animFuture.assertCompleted(false);
+    animFuture.assertProgress(-1.0);
+  }
+
+  /**
+   * Test the default implementations of events in {@link Animation}.
+   */
+  public void testDefaultAnimation() {
+    // Verify initial state
+    final DefaultAnimation anim = new DefaultAnimation();
+    anim.assertProgress(-1.0);
+    anim.assertStarted(false);
+    anim.assertCompleted(false);
+    anim.assertCancelled(false);
+
+    // Starting an animation calls onUpdate(interpolate(0.0))
+    anim.reset();
+    anim.onStart();
+    anim.assertProgress(0.0);
+    anim.assertStarted(true);
+    anim.assertCompleted(false);
+    anim.assertCancelled(false);
+
+    // Completing an animation calls onUpdate(interpolate(1.0))
+    anim.reset();
+    anim.onComplete();
+    anim.assertProgress(1.0);
+    anim.assertStarted(false);
+    anim.assertCompleted(true);
+    anim.assertCancelled(false);
+
+    // Canceling an animation that is not running does not call onStart or
+    // onComplete
+    anim.reset();
+    anim.onCancel();
+    anim.assertProgress(-1.0);
+    anim.assertStarted(false);
+    anim.assertCompleted(false);
+    anim.assertCancelled(true);
+
+    // 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.cancel();
+    anim.assertProgress(-1.0);
+    anim.assertStarted(false);
+    anim.assertCompleted(false);
+    anim.assertCancelled(true);
   }
 
   /**
@@ -213,62 +330,118 @@
 
     // Run animations
     double curTime = Duration.currentTimeMillis();
-    animNow.run(300);
-    animPast.run(300, curTime - 150);
-    animFuture.run(300, curTime + 150);
+    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
-    assertTrue(animNow.started);
-    assertFalse(animNow.completed);
-    assertTrue(animNow.curProgress >= 0.0 && animNow.curProgress <= 2.0);
+    animNow.assertStarted(true);
+    animNow.assertCompleted(false);
+    animNow.assertProgress(-1.0);
 
-    assertTrue(animPast.started);
-    assertFalse(animPast.completed);
-    assertTrue(animPast.curProgress > 0.0 && animPast.curProgress <= 1.0);
+    animPast.assertStarted(true);
+    animPast.assertCompleted(false);
+    animPast.assertProgress(-1.0);
 
-    assertFalse(animFuture.started);
-    assertFalse(animFuture.completed);
-    assertEquals(-1.0, animFuture.curProgress);
+    animFuture.assertStarted(false);
+    animFuture.assertCompleted(false);
+    animFuture.assertProgress(-1.0);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertTrue(animNow.started);
-        assertFalse(animNow.completed);
-        assertTrue(animNow.curProgress > 0.0 && animNow.curProgress <= 2.0);
+        animNow.assertStarted(true);
+        animNow.assertCompleted(false);
+        animNow.assertProgressRange(0.0, 1.0);
 
-        assertTrue(animPast.started);
-        assertFalse(animPast.completed);
-        assertTrue(animPast.curProgress > 0.0 && animPast.curProgress <= 1.0);
+        animPast.assertStarted(true);
+        animPast.assertCompleted(false);
+        animPast.assertProgressRange(0.0, 1.0);
 
-        assertFalse(animFuture.started);
-        assertFalse(animFuture.completed);
-        assertEquals(-1.0, animFuture.curProgress);
+        animFuture.assertStarted(false);
+        animFuture.assertCompleted(false);
+        animFuture.assertProgress(-1.0);
       }
-    }.schedule(50);
+    }.schedule(5 * DELAY_MULTIPLIER);
 
     // Check progress
     new Timer() {
       @Override
       public void run() {
-        assertTrue(animNow.started);
-        assertTrue(animNow.completed);
-        assertTrue(animNow.curProgress > 0.0 && animNow.curProgress <= 1.0);
+        animNow.assertStarted(true);
+        animNow.assertCompleted(false);
+        animNow.assertProgressRange(0.0, 1.0);
 
-        assertTrue(animPast.started);
-        assertTrue(animPast.completed);
-        assertTrue(animPast.curProgress > 0.0 && animPast.curProgress <= 1.0);
+        animPast.assertStarted(true);
+        animPast.assertCompleted(false);
+        animPast.assertProgressRange(0.0, 1.0);
 
-        assertTrue(animFuture.started);
-        assertFalse(animFuture.completed);
-        assertTrue(animFuture.curProgress > 0.0
-            && animFuture.curProgress <= 1.0);
+        animFuture.assertStarted(true);
+        animFuture.assertCompleted(false);
+        animFuture.assertProgressRange(0.0, 1.0);
+      }
+    }.schedule(15 * DELAY_MULTIPLIER);
+
+    // Check progress
+    new Timer() {
+      @Override
+      public void run() {
+        animNow.assertStarted(true);
+        animNow.assertCompleted(false);
+        animNow.assertProgressRange(0.0, 1.0);
+
+        animPast.assertStarted(true);
+        animPast.assertCompleted(true);
+        animPast.assertProgressRange(0.0, 1.0);
+
+        animFuture.assertStarted(true);
+        animFuture.assertCompleted(false);
+        animFuture.assertProgressRange(0.0, 1.0);
+      }
+    }.schedule(25 * DELAY_MULTIPLIER);
+
+    // Check progress
+    new Timer() {
+      @Override
+      public void run() {
+        animNow.assertStarted(true);
+        animNow.assertCompleted(true);
+        animNow.assertProgressRange(0.0, 1.0);
+
+        animPast.assertStarted(true);
+        animPast.assertCompleted(true);
+        animPast.assertProgressRange(0.0, 1.0);
+
+        animFuture.assertStarted(true);
+        animFuture.assertCompleted(false);
+        animFuture.assertProgressRange(0.0, 1.0);
+
         finishTest();
       }
-    }.schedule(350);
+    }.schedule(35 * DELAY_MULTIPLIER);
+
+    // Check progress
+    new Timer() {
+      @Override
+      public void run() {
+        animNow.assertStarted(true);
+        animNow.assertCompleted(true);
+        animNow.assertProgressRange(0.0, 1.0);
+
+        animPast.assertStarted(true);
+        animPast.assertCompleted(true);
+        animPast.assertProgressRange(0.0, 1.0);
+
+        animFuture.assertStarted(true);
+        animFuture.assertCompleted(true);
+        animFuture.assertProgressRange(0.0, 1.0);
+
+        finishTest();
+      }
+    }.schedule(45 * DELAY_MULTIPLIER);
 
     // Wait for the test to finish
-    delayTestFinish(500);
+    delayTestFinish(50 * DELAY_MULTIPLIER);
   }
 }