Internal cleanup for Animator framework

This CL includes the following changes:
1) Remove redundant field mPlayingState in ValueAnimator.
2) Refactor AnimationHandler so that all of its interaction with Choreographer
   is through an interface. A custom provider that implements this interface can
   be plugged in and provide timing pulse that are independent of Choreographer.
3) Better encapsulate AnimationHandler and ValueAnimator. Interaction between
   the two is done through register/unregister frame callbacks as well as
   AnimationFrameCallback interface.
4) Change how animation delay is handled.

Change-Id: Icd49f727321c362dab49b5b33815333c9ea559e0
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
new file mode 100644
index 0000000..f2f369a
--- /dev/null
+++ b/core/java/android/animation/AnimationHandler.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 android.animation;
+
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.view.Choreographer;
+
+import java.util.ArrayList;
+
+/**
+ * This custom, static handler handles the timing pulse that is shared by all active
+ * ValueAnimators. This approach ensures that the setting of animation values will happen on the
+ * same thread that animations start on, and that all animations will share the same times for
+ * calculating their values, which makes synchronizing animations possible.
+ *
+ * The handler uses the Choreographer by default for doing periodic callbacks. A custom
+ * {@Link AnimationFrameCallbackProvider} can be set on the handler to provide timing pulse that
+ * may be independent of UI frame update. This could be useful in testing.
+ *
+ * @hide
+ */
+public class AnimationHandler {
+    /**
+     * Internal per-thread collections used to avoid set collisions as animations start and end
+     * while being processed.
+     * @hide
+     */
+    private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
+            new ArrayMap<>();
+    private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
+            new ArrayList<>();
+    private final ArrayList<AnimationFrameCallback> mCommitCallbacks =
+            new ArrayList<>();
+    private AnimationFrameCallbackProvider mProvider;
+
+    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
+        @Override
+        public void doFrame(long frameTimeNanos) {
+            doAnimationFrame(getProvider().getFrameTime());
+            if (mAnimationCallbacks.size() > 0) {
+                getProvider().postFrameCallback(this);
+            }
+        }
+    };
+
+    public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+    private boolean mListDirty = false;
+
+    public static AnimationHandler getInstance() {
+        if (sAnimatorHandler.get() == null) {
+            sAnimatorHandler.set(new AnimationHandler());
+        }
+        return sAnimatorHandler.get();
+    }
+
+    /**
+     * By default, the Choreographer is used to provide timing for frame callbacks. A custom
+     * provider can be used here to provide different timing pulse.
+     */
+    public void setProvider(AnimationFrameCallbackProvider provider) {
+        if (provider == null) {
+            mProvider = new MyFrameCallbackProvider();
+        } else {
+            mProvider = provider;
+        }
+    }
+
+    private AnimationFrameCallbackProvider getProvider() {
+        if (mProvider == null) {
+            mProvider = new MyFrameCallbackProvider();
+        }
+        return mProvider;
+    }
+
+    /**
+     * Register to get a callback on the next frame after the delay.
+     */
+    public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
+        if (mAnimationCallbacks.size() == 0) {
+            getProvider().postFrameCallback(mFrameCallback);
+        }
+        if (!mAnimationCallbacks.contains(callback)) {
+            mAnimationCallbacks.add(callback);
+        }
+
+        if (delay > 0) {
+            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
+        }
+    }
+
+    /**
+     * Register to get a one shot callback for frame commit timing. Frame commit timing is the
+     * time *after* traversals are done, as opposed to the animation frame timing, which is
+     * before any traversals. This timing can be used to adjust the start time of an animation
+     * when expensive traversals create big delta between the animation frame timing and the time
+     * that animation is first shown on screen.
+     *
+     * Note this should only be called when the animation has already registered to receive
+     * animation frame callbacks. This callback will be guaranteed to happen *after* the next
+     * animation frame callback.
+     */
+    public void addOneShotCommitCallback(final AnimationFrameCallback callback) {
+        if (!mCommitCallbacks.contains(callback)) {
+            mCommitCallbacks.add(callback);
+        }
+    }
+
+    /**
+     * Removes the given callback from the list, so it will no longer be called for frame related
+     * timing.
+     */
+    public void removeCallback(AnimationFrameCallback callback) {
+        mCommitCallbacks.remove(callback);
+        mDelayedCallbackStartTime.remove(callback);
+        int id = mAnimationCallbacks.indexOf(callback);
+        if (id >= 0) {
+            mAnimationCallbacks.set(id, null);
+            mListDirty = true;
+        }
+    }
+
+    private void doAnimationFrame(long frameTime) {
+        int size = mAnimationCallbacks.size();
+        long currentTime = SystemClock.uptimeMillis();
+        for (int i = 0; i < size; i++) {
+            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
+            if (callback == null) {
+                continue;
+            }
+            if (isCallbackDue(callback, currentTime)) {
+                callback.doAnimationFrame(frameTime);
+                if (mCommitCallbacks.contains(callback)) {
+                    getProvider().postCommitCallback(new Runnable() {
+                        @Override
+                        public void run() {
+                            commitAnimationFrame(callback, getProvider().getFrameTime());
+                        }
+                    });
+                }
+            }
+        }
+        cleanUpList();
+    }
+
+    private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
+        if (!mDelayedCallbackStartTime.containsKey(callback) &&
+                mCommitCallbacks.contains(callback)) {
+            callback.commitAnimationFrame(frameTime);
+            mCommitCallbacks.remove(callback);
+        }
+    }
+
+    /**
+     * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
+     * so that they can start getting frame callbacks.
+     *
+     * @return true if they have passed the initial delay or have no delay, false otherwise.
+     */
+    private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
+        Long startTime = mDelayedCallbackStartTime.get(callback);
+        if (startTime == null) {
+            return true;
+        }
+        if (startTime < currentTime) {
+            mDelayedCallbackStartTime.remove(callback);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Return the number of callbacks that have registered for frame callbacks.
+     */
+    public static int getAnimationCount() {
+        AnimationHandler handler = sAnimatorHandler.get();
+        if (handler == null) {
+            return 0;
+        }
+        return handler.getCallbackSize();
+    }
+
+    public static void setFrameDelay(long delay) {
+        getInstance().getProvider().setFrameDelay(delay);
+    }
+
+    public static long getFrameDelay() {
+        return getInstance().getProvider().getFrameDelay();
+    }
+
+    void autoCancelBasedOn(ObjectAnimator objectAnimator) {
+        for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+            AnimationFrameCallback cb = mAnimationCallbacks.get(i);
+            if (cb == null) {
+                continue;
+            }
+            if (objectAnimator.shouldAutoCancel(cb)) {
+                ((Animator) mAnimationCallbacks.get(i)).cancel();
+            }
+        }
+    }
+
+    private void cleanUpList() {
+        if (mListDirty) {
+            for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
+                if (mAnimationCallbacks.get(i) == null) {
+                    mAnimationCallbacks.remove(i);
+                }
+            }
+            mListDirty = false;
+        }
+    }
+
+    private int getCallbackSize() {
+        int count = 0;
+        int size = mAnimationCallbacks.size();
+        for (int i = size - 1; i >= 0; i--) {
+            if (mAnimationCallbacks.get(i) != null) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Default provider of timing pulse that uses Choreographer for frame callbacks.
+     */
+    private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+        final Choreographer mChoreographer = Choreographer.getInstance();
+
+        @Override
+        public void postFrameCallback(Choreographer.FrameCallback callback) {
+            mChoreographer.postFrameCallback(callback);
+        }
+
+        @Override
+        public void postCommitCallback(Runnable runnable) {
+            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
+        }
+
+        @Override
+        public long getFrameTime() {
+            return mChoreographer.getFrameTime();
+        }
+
+        @Override
+        public long getFrameDelay() {
+            return Choreographer.getFrameDelay();
+        }
+
+        @Override
+        public void setFrameDelay(long delay) {
+            Choreographer.setFrameDelay(delay);
+        }
+    }
+
+    /**
+     * Callbacks that receives notifications for animation timing and frame commit timing.
+     */
+    interface AnimationFrameCallback {
+        /**
+         * Run animation based on the frame time.
+         * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
+         *                  base.
+         */
+        void doAnimationFrame(long frameTime);
+
+        /**
+         * This notifies the callback of frame commit time. Frame commit time is the time after
+         * traversals happen, as opposed to the normal animation frame time that is before
+         * traversals. This is used to compensate expensive traversals that happen as the
+         * animation starts. When traversals take a long time to complete, the rendering of the
+         * initial frame will be delayed (by a long time). But since the startTime of the
+         * animation is set before the traversal, by the time of next frame, a lot of time would
+         * have passed since startTime was set, the animation will consequently skip a few frames
+         * to respect the new frameTime. By having the commit time, we can adjust the start time to
+         * when the first frame was drawn (after any expensive traversals) so that no frames
+         * will be skipped.
+         *
+         * @param frameTime The frame time after traversals happen, if any, in the
+         *                  {@link SystemClock#uptimeMillis()} time base.
+         */
+        void commitAnimationFrame(long frameTime);
+    }
+
+    /**
+     * The intention for having this interface is to increase the testability of ValueAnimator.
+     * Specifically, we can have a custom implementation of the interface below and provide
+     * timing pulse without using Choreographer. That way we could use any arbitrary interval for
+     * our timing pulse in the tests.
+     *
+     * @hide
+     */
+    public interface AnimationFrameCallbackProvider {
+        void postFrameCallback(Choreographer.FrameCallback callback);
+        void postCommitCallback(Runnable runnable);
+        long getFrameTime();
+        long getFrameDelay();
+        void setFrameDelay(long delay);
+    }
+}
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index f9333739..26c886e 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -25,6 +25,7 @@
 import android.util.Property;
 
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 
 /**
  * This subclass of {@link ValueAnimator} provides support for animating properties on target objects.
@@ -809,37 +810,7 @@
 
     @Override
     public void start() {
-        // See if any of the current active/pending animators need to be canceled
-        AnimationHandler handler = sAnimationHandler.get();
-        if (handler != null) {
-            int numAnims = handler.mAnimations.size();
-            for (int i = numAnims - 1; i >= 0; i--) {
-                if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
-                    ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
-                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
-                        anim.cancel();
-                    }
-                }
-            }
-            numAnims = handler.mPendingAnimations.size();
-            for (int i = numAnims - 1; i >= 0; i--) {
-                if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
-                    ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
-                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
-                        anim.cancel();
-                    }
-                }
-            }
-            numAnims = handler.mDelayedAnims.size();
-            for (int i = numAnims - 1; i >= 0; i--) {
-                if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
-                    ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
-                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
-                        anim.cancel();
-                    }
-                }
-            }
-        }
+        AnimationHandler.getInstance().autoCancelBasedOn(this);
         if (DBG) {
             Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
             for (int i = 0; i < mValues.length; ++i) {
@@ -852,6 +823,20 @@
         super.start();
     }
 
+    boolean shouldAutoCancel(AnimationHandler.AnimationFrameCallback anim) {
+        if (anim == null) {
+            return false;
+        }
+
+        if (anim instanceof ObjectAnimator) {
+            ObjectAnimator objAnim = (ObjectAnimator) anim;
+            if (objAnim.mAutoCancel && hasSameTargetAndProperties(objAnim)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * This function is called immediately before processing the first animation
      * frame of an animation. If there is a nonzero <code>startDelay</code>, the
diff --git a/core/java/android/animation/TimeAnimator.java b/core/java/android/animation/TimeAnimator.java
index 1ba68df..113a21f 100644
--- a/core/java/android/animation/TimeAnimator.java
+++ b/core/java/android/animation/TimeAnimator.java
@@ -37,7 +37,7 @@
     }
 
     @Override
-    boolean animationFrame(long currentTime) {
+    boolean animateBasedOnTime(long currentTime) {
         if (mListener != null) {
             long totalTime = currentTime - mStartTime;
             long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime);
@@ -52,7 +52,7 @@
         long currentTime = AnimationUtils.currentAnimationTimeMillis();
         mStartTime = Math.max(mStartTime, currentTime - playTime);
         mStartTimeCommitted = true; // do not allow start time to be compensated for jank
-        animationFrame(currentTime);
+        animateBasedOnTime(currentTime);
     }
 
     /**
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 35a8816..065631c 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -21,7 +21,6 @@
 import android.os.Trace;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
-import android.view.Choreographer;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.LinearInterpolator;
@@ -64,7 +63,7 @@
  * </div>
  */
 @SuppressWarnings("unchecked")
-public class ValueAnimator extends Animator {
+public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
     private static final String TAG = "ValueAnimator";
     private static final boolean DEBUG = false;
 
@@ -74,14 +73,6 @@
     private static float sDurationScale = 1.0f;
 
     /**
-     * Values used with internal variable mPlayingState to indicate the current state of an
-     * animation.
-     */
-    static final int STOPPED    = 0; // Not yet playing
-    static final int RUNNING    = 1; // Playing normally
-    static final int SEEKED     = 2; // Seeked to some time value
-
-    /**
      * Internal variables
      * NOTE: This object implements the clone() method, making a deep copy of any referenced
      * objects. As other non-trivial fields are added to this class, make sure to add logic
@@ -130,15 +121,6 @@
      */
     private boolean mResumed = false;
 
-
-    // The static sAnimationHandler processes the internal timing loop on which all animations
-    // are based
-    /**
-     * @hide
-     */
-    protected static ThreadLocal<AnimationHandler> sAnimationHandler =
-            new ThreadLocal<AnimationHandler>();
-
     // The time interpolator to be used if none is set on the animation
     private static final TimeInterpolator sDefaultInterpolator =
             new AccelerateDecelerateInterpolator();
@@ -170,25 +152,9 @@
     private float mCurrentFraction = 0f;
 
     /**
-     * Tracks whether a startDelay'd animation has begun playing through the startDelay.
+     * Tracks the time (in milliseconds) when the last frame arrived.
      */
-    private boolean mStartedDelay = false;
-
-    /**
-     * Tracks the time at which the animation began playing through its startDelay. This is
-     * different from the mStartTime variable, which is used to track when the animation became
-     * active (which is when the startDelay expired and the animation was added to the active
-     * animations list).
-     */
-    private long mDelayStartTime;
-
-    /**
-     * Flag that represents the current state of the animation. Used to figure out when to start
-     * an animation (if state == STOPPED). Also used to end an animation that
-     * has been cancel()'d or end()'d since the last animation frame. Possible values are
-     * STOPPED, RUNNING, SEEKED.
-     */
-    int mPlayingState = STOPPED;
+    private long mLastFrameTime = 0;
 
     /**
      * Additional playing state to indicate whether an animator has been start()'d. There is
@@ -224,11 +190,11 @@
     //
 
     // How long the animation should last in ms
-    private long mDuration = (long)(300 * sDurationScale);
     private long mUnscaledDuration = 300;
+    private long mDuration = (long)(mUnscaledDuration * sDurationScale);
 
     // The amount of time in ms to delay starting the animation after start() is called
-    private long mStartDelay = 0;
+    long mStartDelay = 0;
     private long mUnscaledStartDelay = 0;
 
     // The number of times the animation will repeat. The default is 0, which means the animation
@@ -285,7 +251,6 @@
      */
     public static final int INFINITE = -1;
 
-
     /**
      * @hide
      */
@@ -539,7 +504,6 @@
         }
     }
 
-
     /**
      * Sets the length of the animation. The default duration is 300 milliseconds.
      *
@@ -644,9 +608,8 @@
         long currentTime = AnimationUtils.currentAnimationTimeMillis();
         mStartTime = currentTime - seekTime;
         mStartTimeCommitted = true; // do not allow start time to be compensated for jank
-        if (mPlayingState != RUNNING) {
+        if (!mRunning) {
             mSeekFraction = fraction;
-            mPlayingState = SEEKED;
         }
         if (mPlayingBackwards) {
             fraction = 1f - fraction;
@@ -662,168 +625,13 @@
      * @return The current position in time of the animation.
      */
     public long getCurrentPlayTime() {
-        if (!mInitialized || mPlayingState == STOPPED) {
+        if (!mInitialized || !mStarted) {
             return 0;
         }
         return AnimationUtils.currentAnimationTimeMillis() - mStartTime;
     }
 
     /**
-     * This custom, static handler handles the timing pulse that is shared by
-     * all active animations. This approach ensures that the setting of animation
-     * values will happen on the UI thread and that all animations will share
-     * the same times for calculating their values, which makes synchronizing
-     * animations possible.
-     *
-     * The handler uses the Choreographer for executing periodic callbacks.
-     *
-     * @hide
-     */
-    @SuppressWarnings("unchecked")
-    protected static class AnimationHandler {
-        // The per-thread list of all active animations
-        /** @hide */
-        protected final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
-
-        // Used in doAnimationFrame() to avoid concurrent modifications of mAnimations
-        private final ArrayList<ValueAnimator> mTmpAnimations = new ArrayList<ValueAnimator>();
-
-        // The per-thread set of animations to be started on the next animation frame
-        /** @hide */
-        protected final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();
-
-        /**
-         * Internal per-thread collections used to avoid set collisions as animations start and end
-         * while being processed.
-         * @hide
-         */
-        protected final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
-        private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
-        private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();
-
-        private final Choreographer mChoreographer;
-        private boolean mAnimationScheduled;
-        private long mLastFrameTime;
-
-        private AnimationHandler() {
-            mChoreographer = Choreographer.getInstance();
-        }
-
-        /**
-         * Start animating on the next frame.
-         */
-        public void start() {
-            scheduleAnimation();
-        }
-
-        void doAnimationFrame(long frameTime) {
-            mLastFrameTime = frameTime;
-
-            // mPendingAnimations holds any animations that have requested to be started
-            // We're going to clear mPendingAnimations, but starting animation may
-            // cause more to be added to the pending list (for example, if one animation
-            // starting triggers another starting). So we loop until mPendingAnimations
-            // is empty.
-            while (mPendingAnimations.size() > 0) {
-                ArrayList<ValueAnimator> pendingCopy =
-                        (ArrayList<ValueAnimator>) mPendingAnimations.clone();
-                mPendingAnimations.clear();
-                int count = pendingCopy.size();
-                for (int i = 0; i < count; ++i) {
-                    ValueAnimator anim = pendingCopy.get(i);
-                    // If the animation has a startDelay, place it on the delayed list
-                    if (anim.mStartDelay == 0) {
-                        anim.startAnimation(this);
-                    } else {
-                        mDelayedAnims.add(anim);
-                    }
-                }
-            }
-
-            // Next, process animations currently sitting on the delayed queue, adding
-            // them to the active animations if they are ready
-            int numDelayedAnims = mDelayedAnims.size();
-            for (int i = 0; i < numDelayedAnims; ++i) {
-                ValueAnimator anim = mDelayedAnims.get(i);
-                if (anim.delayedAnimationFrame(frameTime)) {
-                    mReadyAnims.add(anim);
-                }
-            }
-            int numReadyAnims = mReadyAnims.size();
-            if (numReadyAnims > 0) {
-                for (int i = 0; i < numReadyAnims; ++i) {
-                    ValueAnimator anim = mReadyAnims.get(i);
-                    anim.startAnimation(this);
-                    anim.mRunning = true;
-                    mDelayedAnims.remove(anim);
-                }
-                mReadyAnims.clear();
-            }
-
-            // Now process all active animations. The return value from animationFrame()
-            // tells the handler whether it should now be ended
-            int numAnims = mAnimations.size();
-            for (int i = 0; i < numAnims; ++i) {
-                mTmpAnimations.add(mAnimations.get(i));
-            }
-            for (int i = 0; i < numAnims; ++i) {
-                ValueAnimator anim = mTmpAnimations.get(i);
-                if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
-                    mEndingAnims.add(anim);
-                }
-            }
-            mTmpAnimations.clear();
-            if (mEndingAnims.size() > 0) {
-                for (int i = 0; i < mEndingAnims.size(); ++i) {
-                    mEndingAnims.get(i).endAnimation(this);
-                }
-                mEndingAnims.clear();
-            }
-
-            // Schedule final commit for the frame.
-            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, mCommit, null);
-
-            // If there are still active or delayed animations, schedule a future call to
-            // onAnimate to process the next frame of the animations.
-            if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
-                scheduleAnimation();
-            }
-        }
-
-        void commitAnimationFrame(long frameTime) {
-            final long adjustment = frameTime - mLastFrameTime;
-            final int numAnims = mAnimations.size();
-            for (int i = 0; i < numAnims; ++i) {
-                mAnimations.get(i).commitAnimationFrame(adjustment);
-            }
-        }
-
-        private void scheduleAnimation() {
-            if (!mAnimationScheduled) {
-                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null);
-                mAnimationScheduled = true;
-            }
-        }
-
-        // Called by the Choreographer.
-        final Runnable mAnimate = new Runnable() {
-            @Override
-            public void run() {
-                mAnimationScheduled = false;
-                doAnimationFrame(mChoreographer.getFrameTime());
-            }
-        };
-
-        // Called by the Choreographer.
-        final Runnable mCommit = new Runnable() {
-            @Override
-            public void run() {
-                commitAnimationFrame(mChoreographer.getFrameTime());
-            }
-        };
-    }
-
-    /**
      * The amount of time, in milliseconds, to delay starting the animation after
      * {@link #start()} is called.
      *
@@ -859,7 +667,7 @@
      * @return the requested time between frames, in milliseconds
      */
     public static long getFrameDelay() {
-        return Choreographer.getFrameDelay();
+        return AnimationHandler.getInstance().getFrameDelay();
     }
 
     /**
@@ -875,7 +683,7 @@
      * @param frameDelay the requested time between frames, in milliseconds
      */
     public static void setFrameDelay(long frameDelay) {
-        Choreographer.setFrameDelay(frameDelay);
+        AnimationHandler.getInstance().setFrameDelay(frameDelay);
     }
 
     /**
@@ -1104,24 +912,12 @@
                 mPlayingBackwards = (mCurrentIteration % 2) != 0;
             }
         }
-        int prevPlayingState = mPlayingState;
-        mPlayingState = STOPPED;
         mStarted = true;
-        mStartedDelay = false;
         mPaused = false;
+        mRunning = false;
         updateScaledDuration(); // in case the scale factor has changed since creation time
-        AnimationHandler animationHandler = getOrCreateAnimationHandler();
-        animationHandler.mPendingAnimations.add(this);
-        if (mStartDelay == 0) {
-            // This sets the initial value of the animation, prior to actually starting it running
-            if (prevPlayingState != SEEKED) {
-                setCurrentPlayTime(0);
-            }
-            mPlayingState = STOPPED;
-            mRunning = true;
-            notifyStartListeners();
-        }
-        animationHandler.start();
+        AnimationHandler animationHandler = AnimationHandler.getInstance();
+        animationHandler.addAnimationFrameCallback(this, mStartDelay);
     }
 
     @Override
@@ -1131,41 +927,42 @@
 
     @Override
     public void cancel() {
+        if (Looper.myLooper() == null) {
+            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+        }
         // Only cancel if the animation is actually running or has been started and is about
         // to run
-        AnimationHandler handler = getOrCreateAnimationHandler();
-        if (mPlayingState != STOPPED
-                || handler.mPendingAnimations.contains(this)
-                || handler.mDelayedAnims.contains(this)) {
-            // Only notify listeners if the animator has actually started
-            if ((mStarted || mRunning) && mListeners != null) {
-                if (!mRunning) {
-                    // If it's not yet running, then start listeners weren't called. Call them now.
-                    notifyStartListeners();
-                }
-                ArrayList<AnimatorListener> tmpListeners =
-                        (ArrayList<AnimatorListener>) mListeners.clone();
-                for (AnimatorListener listener : tmpListeners) {
-                    listener.onAnimationCancel(this);
-                }
+
+        // Only notify listeners if the animator has actually started
+        if ((mStarted || mRunning) && mListeners != null) {
+            if (!mRunning) {
+                // If it's not yet running, then start listeners weren't called. Call them now.
+                notifyStartListeners();
             }
-            endAnimation(handler);
+            ArrayList<AnimatorListener> tmpListeners =
+                    (ArrayList<AnimatorListener>) mListeners.clone();
+            for (AnimatorListener listener : tmpListeners) {
+                listener.onAnimationCancel(this);
+            }
         }
+        endAnimation();
+
     }
 
     @Override
     public void end() {
-        AnimationHandler handler = getOrCreateAnimationHandler();
-        if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) {
+        if (Looper.myLooper() == null) {
+            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
+        }
+        if (!mRunning) {
             // Special case if the animation has not yet started; get it ready for ending
-            mStartedDelay = false;
-            startAnimation(handler);
+            startAnimation();
             mStarted = true;
         } else if (!mInitialized) {
             initAnimation();
         }
         animateValue(mPlayingBackwards ? 0f : 1f);
-        endAnimation(handler);
+        endAnimation();
     }
 
     @Override
@@ -1188,7 +985,7 @@
 
     @Override
     public boolean isRunning() {
-        return (mPlayingState == RUNNING || mRunning);
+        return mRunning;
     }
 
     @Override
@@ -1206,7 +1003,7 @@
     @Override
     public void reverse() {
         mPlayingBackwards = !mPlayingBackwards;
-        if (mPlayingState == RUNNING) {
+        if (mRunning) {
             long currentTime = AnimationUtils.currentAnimationTimeMillis();
             long currentPlayTime = currentTime - mStartTime;
             long timeLeft = mDuration - currentPlayTime;
@@ -1231,13 +1028,10 @@
     /**
      * Called internally to end an animation by removing it from the animations list. Must be
      * called on the UI thread.
-     * @hide
      */
-    protected void endAnimation(AnimationHandler handler) {
-        handler.mAnimations.remove(this);
-        handler.mPendingAnimations.remove(this);
-        handler.mDelayedAnims.remove(this);
-        mPlayingState = STOPPED;
+     private void endAnimation() {
+        AnimationHandler handler = AnimationHandler.getInstance();
+        handler.removeCallback(this);
         mPaused = false;
         if ((mStarted || mRunning) && mListeners != null) {
             if (!mRunning) {
@@ -1267,16 +1061,14 @@
      * Called internally to start an animation by adding it to the active animations list. Must be
      * called on the UI thread.
      */
-    private void startAnimation(AnimationHandler handler) {
+    private void startAnimation() {
         if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
             Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                     System.identityHashCode(this));
         }
         initAnimation();
-        handler.mAnimations.add(this);
-        if (mStartDelay > 0 && mListeners != null) {
-            // Listeners were already notified in start() if startDelay is 0; this is
-            // just for delayed animations
+        mRunning = true;
+        if (mListeners != null) {
             notifyStartListeners();
         }
     }
@@ -1288,53 +1080,16 @@
         return "animator";
     }
 
-
-    /**
-     * Internal function called to process an animation frame on an animation that is currently
-     * sleeping through its <code>startDelay</code> phase. The return value indicates whether it
-     * should be woken up and put on the active animations queue.
-     *
-     * @param currentTime The current animation time, used to calculate whether the animation
-     * has exceeded its <code>startDelay</code> and should be started.
-     * @return True if the animation's <code>startDelay</code> has been exceeded and the animation
-     * should be added to the set of active animations.
-     */
-    private boolean delayedAnimationFrame(long currentTime) {
-        if (!mStartedDelay) {
-            mStartedDelay = true;
-            mDelayStartTime = currentTime;
-        }
-        if (mPaused) {
-            if (mPauseTime < 0) {
-                mPauseTime = currentTime;
-            }
-            return false;
-        } else if (mResumed) {
-            mResumed = false;
-            if (mPauseTime > 0) {
-                // Offset by the duration that the animation was paused
-                mDelayStartTime += (currentTime - mPauseTime);
-            }
-        }
-        long deltaTime = currentTime - mDelayStartTime;
-        if (deltaTime > mStartDelay) {
-            // startDelay ended - start the anim and record the mStartTime appropriately
-            mStartTime = mDelayStartTime + mStartDelay;
-            mStartTimeCommitted = true; // do not allow start time to be compensated for jank
-            mPlayingState = RUNNING;
-            return true;
-        }
-        return false;
-    }
-
     /**
      * Applies an adjustment to the animation to compensate for jank between when
      * the animation first ran and when the frame was drawn.
+     * @hide
      */
-    void commitAnimationFrame(long adjustment) {
+    public void commitAnimationFrame(long frameTime) {
         if (!mStartTimeCommitted) {
             mStartTimeCommitted = true;
-            if (mPlayingState == RUNNING && adjustment > 0) {
+            long adjustment = frameTime - mLastFrameTime;
+            if (adjustment > 0) {
                 mStartTime += adjustment;
                 if (DEBUG) {
                     Log.d(TAG, "Adjusted start time by " + adjustment + " ms: " + toString());
@@ -1353,13 +1108,11 @@
      *
      * @param currentTime The current time, as tracked by the static timing handler
      * @return true if the animation's duration, including any repetitions due to
-     * <code>repeatCount</code>, has been exceeded and the animation should be ended.
+     * <code>repeatCount</code> has been exceeded and the animation should be ended.
      */
-    boolean animationFrame(long currentTime) {
+    boolean animateBasedOnTime(long currentTime) {
         boolean done = false;
-        switch (mPlayingState) {
-        case RUNNING:
-        case SEEKED:
+        if (mRunning) {
             float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
             if (mDuration == 0 && mRepeatCount != INFINITE) {
                 // Skip to the end
@@ -1394,9 +1147,7 @@
                 fraction = 1f - fraction;
             }
             animateValue(fraction);
-            break;
         }
-
         return done;
     }
 
@@ -1405,10 +1156,15 @@
      *
      * @param frameTime The frame time.
      * @return true if the animation has ended.
+     * @hide
      */
-    final boolean doAnimationFrame(long frameTime) {
-        if (mPlayingState == STOPPED) {
-            mPlayingState = RUNNING;
+    public final void doAnimationFrame(long frameTime) {
+        mLastFrameTime = frameTime;
+        AnimationHandler handler = AnimationHandler.getInstance();
+        if (!mRunning) {
+            // First frame
+            handler.addOneShotCommitCallback(this);
+            startAnimation();
             if (mSeekFraction < 0) {
                 mStartTime = frameTime;
             } else {
@@ -1422,7 +1178,7 @@
             if (mPauseTime < 0) {
                 mPauseTime = frameTime;
             }
-            return false;
+            return;
         } else if (mResumed) {
             mResumed = false;
             if (mPauseTime > 0) {
@@ -1430,13 +1186,18 @@
                 mStartTime += (frameTime - mPauseTime);
                 mStartTimeCommitted = false; // allow start time to be compensated for jank
             }
+            handler.addOneShotCommitCallback(this);
         }
         // The frame time might be before the start time during the first frame of
         // an animation.  The "current time" must always be on or after the start
         // time to avoid animating frames at negative time intervals.  In practice, this
         // is very rare and only happens when seeking backwards.
         final long currentTime = Math.max(frameTime, mStartTime);
-        return animationFrame(currentTime);
+        boolean finished = animateBasedOnTime(currentTime);
+
+        if (finished) {
+            endAnimation();
+        }
     }
 
     /**
@@ -1488,8 +1249,6 @@
         anim.mReversing = false;
         anim.mCurrentIteration = 0;
         anim.mInitialized = false;
-        anim.mPlayingState = STOPPED;
-        anim.mStartedDelay = false;
         anim.mStarted = false;
         anim.mRunning = false;
         anim.mPaused = false;
@@ -1498,8 +1257,8 @@
         anim.mStartTime = 0;
         anim.mStartTimeCommitted = false;
         anim.mPauseTime = 0;
+        anim.mLastFrameTime = 0;
         anim.mCurrentFraction = 0;
-        anim.mDelayStartTime = 0;
 
         PropertyValuesHolder[] oldValues = mValues;
         if (oldValues != null) {
@@ -1540,32 +1299,7 @@
      * @hide
      */
     public static int getCurrentAnimationsCount() {
-        AnimationHandler handler = sAnimationHandler.get();
-        return handler != null ? handler.mAnimations.size() : 0;
-    }
-
-    /**
-     * Clear all animations on this thread, without canceling or ending them.
-     * This should be used with caution.
-     *
-     * @hide
-     */
-    public static void clearAllAnimations() {
-        AnimationHandler handler = sAnimationHandler.get();
-        if (handler != null) {
-            handler.mAnimations.clear();
-            handler.mPendingAnimations.clear();
-            handler.mDelayedAnims.clear();
-        }
-    }
-
-    private static AnimationHandler getOrCreateAnimationHandler() {
-        AnimationHandler handler = sAnimationHandler.get();
-        if (handler == null) {
-            handler = new AnimationHandler();
-            sAnimationHandler.set(handler);
-        }
-        return handler;
+        return AnimationHandler.getAnimationCount();
     }
 
     @Override