diff options
| author | 2017-01-25 17:22:57 +0000 | |
|---|---|---|
| committer | 2017-01-25 17:23:00 +0000 | |
| commit | 32eaa6722cd37406bfabcf07483c0e17f9513277 (patch) | |
| tree | ec7ed14d44bfc50c7e36a59add748eb2a9f7c391 | |
| parent | 617bbb3ba4bab4e0c3d3c18b160b29d89c84f3ba (diff) | |
| parent | 13351997aa36eb53e5ff0fcee3f5e3da83787278 (diff) | |
Merge "New functionalities for AnimatorSet: Reverse, Seek"
| -rw-r--r-- | api/current.txt | 4 | ||||
| -rw-r--r-- | api/system-current.txt | 4 | ||||
| -rw-r--r-- | api/test-current.txt | 4 | ||||
| -rw-r--r-- | core/java/android/animation/AnimationHandler.java | 3 | ||||
| -rw-r--r-- | core/java/android/animation/Animator.java | 93 | ||||
| -rw-r--r-- | core/java/android/animation/AnimatorSet.java | 970 | ||||
| -rw-r--r-- | core/java/android/animation/ObjectAnimator.java | 5 | ||||
| -rw-r--r-- | core/java/android/animation/ValueAnimator.java | 215 | 
8 files changed, 996 insertions, 302 deletions
diff --git a/api/current.txt b/api/current.txt index 18e4adfc70e6..fe9d8d08c457 100644 --- a/api/current.txt +++ b/api/current.txt @@ -3068,8 +3068,10 @@ package android.animation {    public static abstract interface Animator.AnimatorListener {      method public abstract void onAnimationCancel(android.animation.Animator); +    method public default void onAnimationEnd(android.animation.Animator, boolean);      method public abstract void onAnimationEnd(android.animation.Animator);      method public abstract void onAnimationRepeat(android.animation.Animator); +    method public default void onAnimationStart(android.animation.Animator, boolean);      method public abstract void onAnimationStart(android.animation.Animator);    } @@ -3105,6 +3107,8 @@ package android.animation {      method public void playSequentially(java.util.List<android.animation.Animator>);      method public void playTogether(android.animation.Animator...);      method public void playTogether(java.util.Collection<android.animation.Animator>); +    method public void reverse(); +    method public void setCurrentPlayTime(long);      method public android.animation.AnimatorSet setDuration(long);      method public void setInterpolator(android.animation.TimeInterpolator);      method public void setStartDelay(long); diff --git a/api/system-current.txt b/api/system-current.txt index a192c9265be3..b55bd60bd76c 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3187,8 +3187,10 @@ package android.animation {    public static abstract interface Animator.AnimatorListener {      method public abstract void onAnimationCancel(android.animation.Animator); +    method public default void onAnimationEnd(android.animation.Animator, boolean);      method public abstract void onAnimationEnd(android.animation.Animator);      method public abstract void onAnimationRepeat(android.animation.Animator); +    method public default void onAnimationStart(android.animation.Animator, boolean);      method public abstract void onAnimationStart(android.animation.Animator);    } @@ -3224,6 +3226,8 @@ package android.animation {      method public void playSequentially(java.util.List<android.animation.Animator>);      method public void playTogether(android.animation.Animator...);      method public void playTogether(java.util.Collection<android.animation.Animator>); +    method public void reverse(); +    method public void setCurrentPlayTime(long);      method public android.animation.AnimatorSet setDuration(long);      method public void setInterpolator(android.animation.TimeInterpolator);      method public void setStartDelay(long); diff --git a/api/test-current.txt b/api/test-current.txt index 47440e6637fb..950c3d7d29a8 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -3068,8 +3068,10 @@ package android.animation {    public static abstract interface Animator.AnimatorListener {      method public abstract void onAnimationCancel(android.animation.Animator); +    method public default void onAnimationEnd(android.animation.Animator, boolean);      method public abstract void onAnimationEnd(android.animation.Animator);      method public abstract void onAnimationRepeat(android.animation.Animator); +    method public default void onAnimationStart(android.animation.Animator, boolean);      method public abstract void onAnimationStart(android.animation.Animator);    } @@ -3105,6 +3107,8 @@ package android.animation {      method public void playSequentially(java.util.List<android.animation.Animator>);      method public void playTogether(android.animation.Animator...);      method public void playTogether(java.util.Collection<android.animation.Animator>); +    method public void reverse(); +    method public void setCurrentPlayTime(long);      method public android.animation.AnimatorSet setDuration(long);      method public void setInterpolator(android.animation.TimeInterpolator);      method public void setStartDelay(long); diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index 95262ab46a9e..e2e5a8f66288 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -276,8 +276,9 @@ public class AnimationHandler {           * Run animation based on the frame time.           * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time           *                  base. +         * @return if the animation has finished.           */ -        void doAnimationFrame(long frameTime); +        boolean doAnimationFrame(long frameTime);          /**           * This notifies the callback of frame commit time. Frame commit time is the time after diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index c51725a2caac..634dc1fd6ee7 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -26,7 +26,7 @@ import java.util.ArrayList;   * This is the superclass for classes which provide basic support for animations which can be   * started, ended, and have <code>AnimatorListeners</code> added to them.   */ -public abstract class Animator implements Cloneable { +public abstract class Animator implements Cloneable, AnimationHandler.AnimationFrameCallback {      /**       * The value used to indicate infinite duration (e.g. when Animators repeat infinitely). @@ -465,11 +465,102 @@ public abstract class Animator implements Cloneable {      }      /** +     * @hide +     */ +    @Override +    public boolean doAnimationFrame(long frameTime) { +        // TODO: Need to find a better signal than this +        return getDuration() + getStartDelay() >= frameTime; +    } + +    /** +     * @hide +     */ +    @Override +    public void commitAnimationFrame(long frameTime) {} + + +    /** +     * Internal use only. +     * This call starts the animation in regular or reverse direction without requiring them to +     * register frame callbacks. The caller will be responsible for all the subsequent animation +     * pulses. Specifically, the caller needs to call doAnimationFrame(...) for the animation on +     * every frame. +     * +     * @param inReverse whether the animation should play in reverse direction +     */ +    void startWithoutPulsing(boolean inReverse) { +        if (inReverse) { +            reverse(); +        } else { +            start(); +        } +    } + +    /** +     * Internal use only. +     * Skips the animation value to end/start, depending on whether the play direction is forward +     * or backward. +     * +     * @param inReverse whether the end value is based on a reverse direction. If yes, this is +     *                  equivalent to skip to start value in a forward playing direction. +     */ +    void skipToEndValue(boolean inReverse) {} + + +    /** +     * Internal use only. +     * +     * Returns whether the animation has start/end values setup. For most of the animations, this +     * should always be true. For ObjectAnimators, the start values are setup in the initialization +     * of the animation. +     */ +    boolean isInitialized() { +        return true; +    } + +    /** +     * Internal use only. +     */ +    void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) {} + +    /**       * <p>An animation listener receives notifications from an animation.       * Notifications indicate animation related events, such as the end or the       * repetition of the animation.</p>       */      public static interface AnimatorListener { + +        /** +         * <p>Notifies the start of the animation as well as the animation's overall play direction. +         * This method's default behavior is to call {@link #onAnimationStart(Animator)}. This +         * method can be overridden, though not required, to get the additional play direction info +         * when an animation starts. Skipping calling super when overriding this method results in +         * {@link #onAnimationStart(Animator)} not getting called. +         * +         * @param animation The started animation. +         * @param isReverse Whether the animation is playing in reverse. +         */ +        default void onAnimationStart(Animator animation, boolean isReverse) { +            onAnimationStart(animation); +        } + +        /** +         * <p>Notifies the end of the animation. This callback is not invoked +         * for animations with repeat count set to INFINITE.</p> +         * +         * <p>This method's default behavior is to call {@link #onAnimationEnd(Animator)}. This +         * method can be overridden, though not required, to get the additional play direction info +         * when an animation ends. Skipping calling super when overriding this method results in +         * {@link #onAnimationEnd(Animator)} not getting called. +         * +         * @param animation The animation which reached its end. +         * @param isReverse Whether the animation is playing in reverse. +         */ +        default void onAnimationEnd(Animator animation, boolean isReverse) { +            onAnimationEnd(animation); +        } +          /**           * <p>Notifies the start of the animation.</p>           * diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index a904f9d5447b..d5814a30ac17 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -19,11 +19,15 @@ package android.animation;  import android.app.ActivityThread;  import android.app.Application;  import android.os.Build; +import android.os.Looper; +import android.util.AndroidRuntimeException;  import android.util.ArrayMap;  import android.util.Log; +import android.view.animation.Animation;  import java.util.ArrayList;  import java.util.Collection; +import java.util.Comparator;  import java.util.List;  /** @@ -52,7 +56,7 @@ import java.util.List;   * Animation</a> developer guide.</p>   * </div>   */ -public final class AnimatorSet extends Animator { +public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {      private static final String TAG = "AnimatorSet";      /** @@ -66,7 +70,7 @@ public final class AnimatorSet extends Animator {       * Tracks animations currently being played, so that we know what to       * cancel or end when cancel() or end() is called on this AnimatorSet       */ -    private ArrayList<Animator> mPlayingSet = new ArrayList<Animator>(); +    private ArrayList<Node> mPlayingSet = new ArrayList<Node>();      /**       * Contains all nodes, mapped to their respective Animators. When new @@ -77,6 +81,11 @@ public final class AnimatorSet extends Animator {      private ArrayMap<Animator, Node> mNodeMap = new ArrayMap<Animator, Node>();      /** +     * Contains the start and end events of all the nodes. All these events are sorted in this list. +     */ +    private ArrayList<AnimationEvent> mEvents = new ArrayList<>(); + +    /**       * Set of all nodes created for this AnimatorSet. This list is used upon       * starting the set, and the nodes are placed in sorted order into the       * sortedNodes collection. @@ -84,21 +93,6 @@ public final class AnimatorSet extends Animator {      private ArrayList<Node> mNodes = new ArrayList<Node>();      /** -     * Animator Listener that tracks the lifecycle of each Animator in the set. It will be added -     * to each Animator before they start and removed after they end. -     */ -    private AnimatorSetListener mSetListener = new AnimatorSetListener(this); - -    /** -     * Flag indicating that the AnimatorSet has been manually -     * terminated (by calling cancel() or end()). -     * This flag is used to avoid starting other animations when currently-playing -     * child animations of this AnimatorSet end. It also determines whether cancel/end -     * notifications are sent out via the normal AnimatorSetListener mechanism. -     */ -    private boolean mTerminated = false; - -    /**       * Tracks whether any change has been made to the AnimatorSet, which is then used to       * determine whether the dependency graph should be re-constructed.       */ @@ -131,8 +125,6 @@ public final class AnimatorSet extends Animator {      // was set on this AnimatorSet, so it should not be passed down to the children.      private TimeInterpolator mInterpolator = null; -    // Whether the AnimatorSet can be reversed. -    private boolean mReversible = true;      // The total duration of finishing all the Animators in the set.      private long mTotalDuration = 0; @@ -142,6 +134,46 @@ public final class AnimatorSet extends Animator {      // the animator set and immediately end it for N and forward.      private final boolean mShouldIgnoreEndWithoutStart; +    // In pre-O releases, calling start() doesn't reset all the animators values to start values. +    // As a result, the start of the animation is inconsistent with what setCurrentPlayTime(0) would +    // look like on O. Also it is inconsistent with what reverse() does on O, as reverse would +    // advance all the animations to the right beginning values for before starting to reverse. +    // From O and forward, we will add an additional step of resetting the animation values (unless +    // the animation was previously seeked and therefore doesn't start from the beginning). +    private final boolean mShouldResetValuesAtStart; + +    // The time, in milliseconds, when last frame of the animation came in. -1 when the animation is +    // not running. +    private long mLastFrameTime = -1; + +    // The time, in milliseconds, when the first frame of the animation came in. +    // -1 when the animation is not running. +    private long mFirstFrame = -1; + +    // The time, in milliseconds, when the first frame of the animation came in. +    // -1 when the animation is not running. +    private int mLastEventId = -1; + +    // Indicates whether the animation is reversing. +    private boolean mReversing = false; + +    // Indicates whether the animation should register frame callbacks. If false, the animation will +    // passively wait for an AnimatorSet to pulse it. +    private boolean mSelfPulse = true; + +    // SeekState stores the last seeked play time as well as seek direction. +    private SeekState mSeekState = new SeekState(); + +    // Indicates where children animators are all initialized with their start values captured. +    private boolean mChildrenInitialized = false; + +    /** +     * Set on the next frame after pause() is called, used to calculate a new startTime +     * or delayStartTime which allows the animator set to continue from the point at which +     * it was paused. If negative, has not yet been set. +     */ +    private long mPauseTime = -1; +      public AnimatorSet() {          super();          mNodeMap.put(mDelayAnim, mRootNode); @@ -150,10 +182,19 @@ public final class AnimatorSet extends Animator {          Application app = ActivityThread.currentApplication();          if (app == null || app.getApplicationInfo() == null) {              mShouldIgnoreEndWithoutStart = true; -        } else if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { -            mShouldIgnoreEndWithoutStart = true; +            mShouldResetValuesAtStart = false;          } else { -            mShouldIgnoreEndWithoutStart = false; +            if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { +                mShouldIgnoreEndWithoutStart = true; +            } else { +                mShouldIgnoreEndWithoutStart = false; +            } + +            if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O) { +                mShouldResetValuesAtStart = false; +            } else { +                mShouldResetValuesAtStart = true; +            }          }      } @@ -206,7 +247,6 @@ public final class AnimatorSet extends Animator {              if (items.length == 1) {                  play(items[0]);              } else { -                mReversible = false;                  for (int i = 0; i < items.length - 1; ++i) {                      play(items[i]).before(items[i + 1]);                  } @@ -225,7 +265,6 @@ public final class AnimatorSet extends Animator {              if (items.size() == 1) {                  play(items.get(0));              } else { -                mReversible = false;                  for (int i = 0; i < items.size() - 1; ++i) {                      play(items.get(i)).before(items.get(i + 1));                  } @@ -350,7 +389,9 @@ public final class AnimatorSet extends Animator {      @SuppressWarnings("unchecked")      @Override      public void cancel() { -        mTerminated = true; +        if (Looper.myLooper() == null) { +            throw new AndroidRuntimeException("Animators may only be run on Looper threads"); +        }          if (isStarted()) {              ArrayList<AnimatorListener> tmpListeners = null;              if (mListeners != null) { @@ -360,18 +401,13 @@ public final class AnimatorSet extends Animator {                      tmpListeners.get(i).onAnimationCancel(this);                  }              } -            ArrayList<Animator> playingSet = new ArrayList<>(mPlayingSet); +            ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet);              int setSize = playingSet.size();              for (int i = 0; i < setSize; i++) { -                playingSet.get(i).cancel(); +                playingSet.get(i).mAnimation.cancel();              } -            if (tmpListeners != null) { -                int size = tmpListeners.size(); -                for (int i = 0; i < size; i++) { -                    tmpListeners.get(i).onAnimationEnd(this); -                } -            } -            mStarted = false; +            mPlayingSet.clear(); +            endAnimation();          }      } @@ -383,50 +419,40 @@ public final class AnimatorSet extends Animator {       */      @Override      public void end() { +        if (Looper.myLooper() == null) { +            throw new AndroidRuntimeException("Animators may only be run on Looper threads"); +        }          if (mShouldIgnoreEndWithoutStart && !isStarted()) {              return;          } -        mTerminated = true;          if (isStarted()) { -            endRemainingAnimations(); -        } -        if (mListeners != null) { -            ArrayList<AnimatorListener> tmpListeners = -                    (ArrayList<AnimatorListener>) mListeners.clone(); -            for (int i = 0; i < tmpListeners.size(); i++) { -                tmpListeners.get(i).onAnimationEnd(this); -            } -        } -        mStarted = false; -    } - -    /** -     * Iterate the animations that haven't finished or haven't started, and end them. -     */ -    private void endRemainingAnimations() { -        ArrayList<Animator> remainingList = new ArrayList<Animator>(mNodes.size()); -        remainingList.addAll(mPlayingSet); - -        int index = 0; -        while (index < remainingList.size()) { -            Animator anim = remainingList.get(index); -            anim.end(); -            index++; -            Node node = mNodeMap.get(anim); -            if (node.mChildNodes != null) { -                int childSize = node.mChildNodes.size(); -                for (int i = 0; i < childSize; i++) { -                    Node child = node.mChildNodes.get(i); -                    if (child.mLatestParent != node) { -                        continue; +            // Iterate the animations that haven't finished or haven't started, and end them. +            if (mReversing) { +                // Between start() and first frame, mLastEventId would be unset (i.e. -1) +                mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId; +                for (int j = mLastEventId - 1; j >= 0; j--) { +                    AnimationEvent event = mEvents.get(j); +                    if (event.mEvent == AnimationEvent.ANIMATION_END) { +                        event.mNode.mAnimation.reverse(); +                    } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { +                        event.mNode.mAnimation.end(); +                    } +                } +            } else { +                for (int j = mLastEventId + 1; j < mEvents.size(); j++) { +                    AnimationEvent event = mEvents.get(j); +                    if (event.mEvent == AnimationEvent.ANIMATION_START) { +                        event.mNode.mAnimation.start(); +                    } else if (event.mEvent == AnimationEvent.ANIMATION_END) { +                        event.mNode.mAnimation.end();                      } -                    remainingList.add(child.mAnimation);                  }              } +            mPlayingSet.clear();          } +        endAnimation();      } -      /**       * Returns true if any of the child animations of this AnimatorSet have been started and have       * not yet ended. Child animations will not be started until the AnimatorSet has gone past @@ -437,14 +463,12 @@ public final class AnimatorSet extends Animator {       */      @Override      public boolean isRunning() { -        int size = mNodes.size(); -        for (int i = 0; i < size; i++) { -            Node node = mNodes.get(i); -            if (node != mRootNode && node.mAnimation.isStarted()) { -                return true; -            } +        if (mStartDelay > 0) { +            return mStarted && !mDelayAnim.isRunning(); +        } else { +            // No start delay, animation should start right away +            return mStarted;          } -        return false;      }      @Override @@ -482,9 +506,6 @@ public final class AnimatorSet extends Animator {              return;          }          mStartDelay = startDelay; -        if (mStartDelay > 0) { -            mReversible = false; -        }          if (!mDependencyDirty) {              // Dependency graph already constructed, update all the nodes' start/end time              int size = mNodes.size(); @@ -562,40 +583,26 @@ public final class AnimatorSet extends Animator {      @Override      public void pause() { +        if (Looper.myLooper() == null) { +            throw new AndroidRuntimeException("Animators may only be run on Looper threads"); +        }          boolean previouslyPaused = mPaused;          super.pause();          if (!previouslyPaused && mPaused) { -            if (mDelayAnim.isStarted()) { -                // If delay hasn't passed, pause the start delay animator. -                mDelayAnim.pause(); -            } else { -                int size = mNodes.size(); -                for (int i = 0; i < size; i++) { -                    Node node = mNodes.get(i); -                    if (node != mRootNode) { -                        node.mAnimation.pause(); -                    } -                } -            } +            mPauseTime = -1;          }      }      @Override      public void resume() { +        if (Looper.myLooper() == null) { +            throw new AndroidRuntimeException("Animators may only be run on Looper threads"); +        }          boolean previouslyPaused = mPaused;          super.resume();          if (previouslyPaused && !mPaused) { -            if (mDelayAnim.isStarted()) { -                // If start delay hasn't passed, resume the previously paused start delay animator -                mDelayAnim.resume(); -            } else { -                int size = mNodes.size(); -                for (int i = 0; i < size; i++) { -                    Node node = mNodes.get(i); -                    if (node != mRootNode) { -                        node.mAnimation.resume(); -                    } -                } +            if (mPauseTime >= 0) { +                addAnimationCallback(0);              }          }      } @@ -610,9 +617,33 @@ public final class AnimatorSet extends Animator {      @SuppressWarnings("unchecked")      @Override      public void start() { -        mTerminated = false; +        start(false, true); +    } + +    @Override +    void startWithoutPulsing(boolean inReverse) { +        start(inReverse, false); +    } + +    private void initAnimation() { +        if (mInterpolator != null) { +            for (int i = 0; i < mNodes.size(); i++) { +                Node node = mNodes.get(i); +                node.mAnimation.setInterpolator(mInterpolator); +            } +        } +        updateAnimatorsDuration(); +        createDependencyGraph(); +    } + +    private void start(boolean inReverse, boolean selfPulse) { +        if (Looper.myLooper() == null) { +            throw new AndroidRuntimeException("Animators may only be run on Looper threads"); +        }          mStarted = true; +        mSelfPulse = selfPulse;          mPaused = false; +        mPauseTime = -1;          int size = mNodes.size();          for (int i = 0; i < size; i++) { @@ -621,26 +652,17 @@ public final class AnimatorSet extends Animator {              node.mAnimation.setAllowRunningAsynchronously(false);          } -        if (mInterpolator != null) { -            for (int i = 0; i < size; i++) { -                Node node = mNodes.get(i); -                node.mAnimation.setInterpolator(mInterpolator); -            } +        initAnimation(); +        if (inReverse && !canReverse()) { +            throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet");          } -        updateAnimatorsDuration(); -        createDependencyGraph(); +        mReversing = inReverse;          // Now that all dependencies are set up, start the animations that should be started. -        boolean setIsEmpty = false; -        if (mStartDelay > 0) { -            start(mRootNode); -        } else if (isEmptySet(this)) { -            // Set is empty or contains only empty animator sets. Skip to end in this case. -            setIsEmpty = true; -        } else { -            // No delay, but there are other animators in the set -            onChildAnimatorEnded(mDelayAnim); +        boolean setIsEmpty = isEmptySet(this); +        if (!setIsEmpty) { +            startAnimation();          }          if (mListeners != null) { @@ -648,12 +670,12 @@ public final class AnimatorSet extends Animator {                      (ArrayList<AnimatorListener>) mListeners.clone();              int numListeners = tmpListeners.size();              for (int i = 0; i < numListeners; ++i) { -                tmpListeners.get(i).onAnimationStart(this); +                tmpListeners.get(i).onAnimationStart(this, inReverse);              }          }          if (setIsEmpty) {              // In the case of empty AnimatorSet, we will trigger the onAnimationEnd() right away. -            onChildAnimatorEnded(mDelayAnim); +            end();          }      } @@ -690,11 +712,419 @@ public final class AnimatorSet extends Animator {          mDelayAnim.setDuration(mStartDelay);      } -    void start(final Node node) { -        final Animator anim = node.mAnimation; -        mPlayingSet.add(anim); -        anim.addListener(mSetListener); -        anim.start(); +    @Override +    void skipToEndValue(boolean inReverse) { +        if (!isInitialized()) { +            throw new UnsupportedOperationException("Children must be initialized."); +        } + +        // This makes sure the animation events are sorted an up to date. +        initAnimation(); + +        // Calling skip to the end in the sequence that they would be called in a forward/reverse +        // run, such that the sequential animations modifying the same property would have +        // the right value in the end. +        if (inReverse) { +            for (int i = mEvents.size() - 1; i >= 0; i--) { +                if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { +                    mEvents.get(i).mNode.mAnimation.skipToEndValue(true); +                } +            } +        } else { +            for (int i = 0; i < mEvents.size(); i++) { +                if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_END) { +                    mEvents.get(i).mNode.mAnimation.skipToEndValue(false); +                } +            } +        } +    } + +    /** +     * Internal only. +     * +     * This method sets the animation values based on the play time. It also fast forward or +     * backward all the child animations progress accordingly. +     * +     * This method is also responsible for calling +     * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)}, +     * as needed, based on the last play time and current play time. +     */ +    @Override +    void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) { +        if (currentPlayTime < 0 || lastPlayTime < 0) { +            throw new UnsupportedOperationException("Error: Play time should never be negative."); +        } +        // TODO: take into account repeat counts and repeat callback when repeat is implemented. +        // Clamp currentPlayTime and lastPlayTime + +        // TODO: Make this more efficient + +        // Convert the play times to the forward direction. +        if (inReverse) { +            if (getTotalDuration() == DURATION_INFINITE) { +                throw new UnsupportedOperationException("Cannot reverse AnimatorSet with infinite" +                        + " duration"); +            } +            long duration = getTotalDuration() - mStartDelay; +            currentPlayTime = Math.min(currentPlayTime, duration); +            currentPlayTime = duration - currentPlayTime; +            lastPlayTime = duration - lastPlayTime; +            inReverse = false; +        } +        // Skip all values to start, and iterate mEvents to get animations to the right fraction. +        skipToStartValue(false); + +        ArrayList<Node> unfinishedNodes = new ArrayList<>(); +        // Assumes forward playing from here on. +        for (int i = 0; i < mEvents.size(); i++) { +            AnimationEvent event = mEvents.get(i); +            if (event.getTime() > currentPlayTime) { +                break; +            } + +            // This animation started prior to the current play time, and won't finish before the +            // play time, add to the unfinished list. +            if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { +                if (event.mNode.mEndTime == DURATION_INFINITE +                        || event.mNode.mEndTime > currentPlayTime) { +                    unfinishedNodes.add(event.mNode); +                } +            } +            // For animations that do finish before the play time, end them in the sequence that +            // they would in a normal run. +            if (event.mEvent == AnimationEvent.ANIMATION_END) { +                // Skip to the end of the animation. +                event.mNode.mAnimation.skipToEndValue(false); +            } +        } + +        // Seek unfinished animation to the right time. +        for (int i = 0; i < unfinishedNodes.size(); i++) { +            Node node = unfinishedNodes.get(i); +            long playTime = getPlayTimeForNode(currentPlayTime, node, inReverse); +            node.mAnimation.animateBasedOnPlayTime(playTime, lastPlayTime, inReverse); +        } +    } + +    @Override +    boolean isInitialized() { +        if (mChildrenInitialized) { +            return true; +        } + +        boolean allInitialized = true; +        for (int i = 0; i < mNodes.size(); i++) { +            if (!mNodes.get(i).mAnimation.isInitialized()) { +                allInitialized = false; +                break; +            } +        } +        mChildrenInitialized = allInitialized; +        return mChildrenInitialized; +    } + +    private void skipToStartValue(boolean inReverse) { +        skipToEndValue(!inReverse); +    } + +    /** +     * Sets the position of the animation to the specified point in time. This time should +     * be between 0 and the total duration of the animation, including any repetition. If +     * the animation has not yet been started, then it will not advance forward after it is +     * set to this time; it will simply set the time to this value and perform any appropriate +     * actions based on that time. If the animation is already running, then setCurrentPlayTime() +     * will set the current playing time to this value and continue playing from that point. +     * +     * @param playTime The time, in milliseconds, to which the animation is advanced or rewound. +     *                 Unless the animation is reversing, the playtime is considered the time since +     *                 the end of the start delay of the AnimatorSet in a forward playing direction. +     * +     */ +    public void setCurrentPlayTime(long playTime) { +        if (mReversing && getTotalDuration() == DURATION_INFINITE) { +            // Should never get here +            throw new UnsupportedOperationException("Error: Cannot seek in reverse in an infinite" +                    + " AnimatorSet"); +        } + +        if ((getTotalDuration() != DURATION_INFINITE && playTime > getTotalDuration() - mStartDelay) +                || playTime < 0) { +            throw new UnsupportedOperationException("Error: Play time should always be in between" +                    + "0 and duration."); +        } + +        initAnimation(); + +        if (!isStarted()) { +            if (mReversing) { +                throw new UnsupportedOperationException("Error: Something went wrong. mReversing" +                        + " should not be set when AnimatorSet is not started."); +            } +            if (!mSeekState.isActive()) { +                findLatestEventIdForTime(0); +                // Set all the values to start values. +                initChildren(); +                skipToStartValue(mReversing); +                mSeekState.setPlayTime(0, mReversing); +            } +            animateBasedOnPlayTime(playTime, 0, mReversing); +            mSeekState.setPlayTime(playTime, mReversing); +        } else { +            // If the animation is running, just set the seek time and wait until the next frame +            // (i.e. doAnimationFrame(...)) to advance the animation. +            mSeekState.setPlayTime(playTime, mReversing); +        } +    } + +    private void initChildren() { +        if (!isInitialized()) { +            mChildrenInitialized = true; +            // Forcefully initialize all children based on their end time, so that if the start +            // value of a child is dependent on a previous animation, the animation will be +            // initialized after the the previous animations have been advanced to the end. +            skipToEndValue(false); +        } +    } + +    /** +     * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time +     *                  base. +     * @return +     * @hide +     */ +    @Override +    public boolean doAnimationFrame(long frameTime) { +        if (mLastFrameTime < 0) { +            mFirstFrame = mLastFrameTime = frameTime; +        } + +        // Handle pause/resume +        if (mPaused) { +            // Note: Child animations don't receive pause events. Since it's never a contract that +            // the child animators will be paused when set is paused, this is unlikely to be an +            // issue. +            mPauseTime = frameTime; +            removeAnimationCallback(); +            return false; +        } else if (mPauseTime > 0) { +                // Offset by the duration that the animation was paused +            mFirstFrame += (frameTime - mPauseTime); +            mPauseTime = -1; +        } + +        // Continue at seeked position +        if (mSeekState.isActive()) { +            mSeekState.updateSeekDirection(mReversing); +            mFirstFrame = frameTime - mSeekState.getPlayTime() - mStartDelay; +            mSeekState.reset(); +        } + +        // This playTime includes the start delay. +        long playTime = frameTime - mFirstFrame; + +        // 1. Pulse the animators that will start or end in this frame +        // 2. Pulse the animators that will finish in a later frame +        int latestId = findLatestEventIdForTime(playTime); +        int startId = mLastEventId; + +        handleAnimationEvents(startId, latestId, playTime); + +        mLastEventId = latestId; + +        // Pump a frame to the on-going animators +        for (int i = 0; i < mPlayingSet.size(); i++) { +            Node node = mPlayingSet.get(i); +            if (!node.mEnded) { +                node.mEnded = node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node)); +            } +        } + +        // Remove all the finished anims +        for (int i = mPlayingSet.size() - 1; i >= 0; i--) { +            if (mPlayingSet.get(i).mEnded) { +                mPlayingSet.remove(i); +            } +        } + +        mLastFrameTime = frameTime; +        if (mPlayingSet.isEmpty()) { +            boolean finished; +            if (mReversing) { +                // Make sure there's no more END event before current event id and after start delay +                finished = mLastEventId <= 3; +            } else { +                // Make sure there's no more START event before current event id: +                finished = (mLastEventId == mEvents.size() - 1); +            } +            if (finished) { +                endAnimation(); +                return true; +            } +        } +        return false; +    } + +    /** +     * When playing forward, we call start() at the animation's scheduled start time, and make sure +     * to pump a frame at the animation's scheduled end time. +     * +     * When playing in reverse, we should reverse the animation when we hit animation's end event, +     * and expect the animation to end at the its delay ended event, rather than start event. +     */ +    private void handleAnimationEvents(int startId, int latestId, long playTime) { +        if (mReversing) { +            startId = startId == -1 ? mEvents.size() : startId; +            for (int i = startId - 1; i >= latestId; i--) { +                AnimationEvent event = mEvents.get(i); +                Node node = event.mNode; +                if (event.mEvent == AnimationEvent.ANIMATION_END) { +                    mPlayingSet.add(event.mNode); +                    node.mAnimation.startWithoutPulsing(true); +                    node.mAnimation.doAnimationFrame(0); +                } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) { +                    // end event: +                    node.mEnded = +                            node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node)); +                } +            } +        } else { +            for (int i = startId + 1; i <= latestId; i++) { +                AnimationEvent event = mEvents.get(i); +                Node node = event.mNode; +                if (event.mEvent == AnimationEvent.ANIMATION_START) { +                    mPlayingSet.add(event.mNode); +                    node.mAnimation.startWithoutPulsing(false); +                    node.mAnimation.doAnimationFrame(0); +                } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) { +                    // start event: +                    node.mEnded = +                            node.mAnimation.doAnimationFrame(getPlayTimeForNode(playTime, node)); +                } +            } +        } +    } + +    private long getPlayTimeForNode(long overallPlayTime, Node node) { +        return getPlayTimeForNode(overallPlayTime, node, mReversing); +    } + +    private long getPlayTimeForNode(long overallPlayTime, Node node, boolean inReverse) { +        if (inReverse) { +            overallPlayTime = getTotalDuration() - overallPlayTime; +            return node.mEndTime - overallPlayTime; +        } else { +            return overallPlayTime - node.mStartTime; +        } +    } + +    private void startAnimation() { +        // Register animation callback +        addAnimationCallback(mStartDelay); + +        if (mSeekState.getPlayTimeNormalized() == 0 && mReversing) { +            // Maintain old behavior, if seeked to 0 then call reverse, we'll treat the case +            // the same as no seeking at all. +            mSeekState.reset(); +        } +        // Set the child animators to the right end: +        if (mShouldResetValuesAtStart) { +            if (mReversing || isInitialized()) { +                skipToEndValue(!mReversing); +            } else { +                // If not all children are initialized and play direction is forward +                for (int i = mEvents.size() - 1; i >= 0; i--) { +                    if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { +                        Animator anim = mEvents.get(i).mNode.mAnimation; +                        // Only reset the animations that have been initialized to start value, +                        // so that if they are defined without a start value, they will get the +                        // values set at the right time (i.e. the next animation run) +                        if (anim.isInitialized()) { +                            anim.skipToEndValue(true); +                        } +                    } +                } +            } +        } + +        if (mReversing || mStartDelay == 0 || mSeekState.isActive()) { +            long playTime; +            // If no delay, we need to call start on the first animations to be consistent with old +            // behavior. +            if (mSeekState.isActive()) { +                mSeekState.updateSeekDirection(mReversing); +                playTime = mSeekState.getPlayTime(); +            } else { +                playTime = 0; +            } +            int toId = findLatestEventIdForTime(playTime); +            handleAnimationEvents(-1, toId, playTime); +            mLastEventId = toId; +        } +    } + +    private int findLatestEventIdForTime(long currentPlayTime) { +        int size = mEvents.size(); +        int latestId = mLastEventId; +        // Call start on the first animations now to be consistent with the old behavior +        if (mReversing) { +            currentPlayTime = getTotalDuration() - currentPlayTime; +            mLastEventId = mLastEventId == -1 ? size : mLastEventId; +            for (int j = mLastEventId - 1; j >= 0; j--) { +                AnimationEvent event = mEvents.get(j); +                if (event.getTime() >= currentPlayTime) { +                    latestId = j; +                } +            } +        } else { +            for (int i = mLastEventId + 1; i < size; i++) { +                AnimationEvent event = mEvents.get(i); +                if (event.getTime() <= currentPlayTime) { +                    latestId = i; +                } +            } +        } +        return latestId; +    } + +    private void endAnimation() { +        mStarted = false; +        mLastFrameTime = -1; +        mFirstFrame = -1; +        mLastEventId = -1; +        mPaused = false; +        mPauseTime = -1; +        mSeekState.reset(); +        mPlayingSet.clear(); + +        // No longer receive callbacks +        removeAnimationCallback(); +        // Call end listener +        if (mListeners != null) { +            ArrayList<AnimatorListener> tmpListeners = +                    (ArrayList<AnimatorListener>) mListeners.clone(); +            int numListeners = tmpListeners.size(); +            for (int i = 0; i < numListeners; ++i) { +                tmpListeners.get(i).onAnimationEnd(this, mReversing); +            } +        } +        mSelfPulse = true; +        mReversing = false; +    } + +    private void removeAnimationCallback() { +        if (!mSelfPulse) { +            return; +        } +        AnimationHandler handler = AnimationHandler.getInstance(); +        handler.removeCallback(this); +    } + +    private void addAnimationCallback(long delay) { +        if (!mSelfPulse) { +            return; +        } +        AnimationHandler handler = AnimationHandler.getInstance(); +        handler.addAnimationFrameCallback(this, delay);      }      @Override @@ -709,13 +1139,20 @@ public final class AnimatorSet extends Animator {           * and will populate any appropriate lists, when it is started.           */          final int nodeCount = mNodes.size(); -        anim.mTerminated = false;          anim.mStarted = false; -        anim.mPlayingSet = new ArrayList<Animator>(); +        anim.mLastFrameTime = -1; +        anim.mFirstFrame = -1; +        anim.mLastEventId = -1; +        anim.mPaused = false; +        anim.mPauseTime = -1; +        anim.mSeekState = new SeekState(); +        anim.mSelfPulse = true; +        anim.mPlayingSet = new ArrayList<Node>();          anim.mNodeMap = new ArrayMap<Animator, Node>();          anim.mNodes = new ArrayList<Node>(nodeCount); -        anim.mReversible = mReversible; -        anim.mSetListener = new AnimatorSetListener(anim); +        anim.mEvents = new ArrayList<AnimationEvent>(); +        anim.mReversing = false; +        anim.mDependencyDirty = true;          // Walk through the old nodes list, cloning each node and adding it to the new nodemap.          // One problem is that the old node dependencies point to nodes in the old AnimatorSet. @@ -727,17 +1164,6 @@ public final class AnimatorSet extends Animator {              node.mTmpClone = nodeClone;              anim.mNodes.add(nodeClone);              anim.mNodeMap.put(nodeClone.mAnimation, nodeClone); - -            // clear out any listeners that were set up by the AnimatorSet -            final ArrayList<AnimatorListener> cloneListeners = nodeClone.mAnimation.getListeners(); -            if (cloneListeners != null) { -                for (int i = cloneListeners.size() - 1; i >= 0; i--) { -                    final AnimatorListener listener = cloneListeners.get(i); -                    if (listener instanceof AnimatorSetListener) { -                        cloneListeners.remove(i); -                    } -                } -            }          }          anim.mRootNode = mRootNode.mTmpClone; @@ -771,89 +1197,6 @@ public final class AnimatorSet extends Animator {      } -    private static class AnimatorSetListener implements AnimatorListener { - -        private AnimatorSet mAnimatorSet; - -        AnimatorSetListener(AnimatorSet animatorSet) { -            mAnimatorSet = animatorSet; -        } - -        public void onAnimationCancel(Animator animation) { - -            if (!mAnimatorSet.mTerminated) { -                // Listeners are already notified of the AnimatorSet canceling in cancel(). -                // The logic below only kicks in when animations end normally -                if (mAnimatorSet.mPlayingSet.size() == 0) { -                    ArrayList<AnimatorListener> listeners = mAnimatorSet.mListeners; -                    if (listeners != null) { -                        int numListeners = listeners.size(); -                        for (int i = 0; i < numListeners; ++i) { -                            listeners.get(i).onAnimationCancel(mAnimatorSet); -                        } -                    } -                } -            } -        } - -        @SuppressWarnings("unchecked") -        public void onAnimationEnd(Animator animation) { -            animation.removeListener(this); -            mAnimatorSet.mPlayingSet.remove(animation); -            mAnimatorSet.onChildAnimatorEnded(animation); -        } - -        // Nothing to do -        public void onAnimationRepeat(Animator animation) { -        } - -        // Nothing to do -        public void onAnimationStart(Animator animation) { -        } - -    } - -    private void onChildAnimatorEnded(Animator animation) { -        Node animNode = mNodeMap.get(animation); -        animNode.mEnded = true; - -        if (!mTerminated) { -            List<Node> children = animNode.mChildNodes; -            // Start children animations, if any. -            int childrenSize = children == null ? 0 : children.size(); -            for (int i = 0; i < childrenSize; i++) { -                if (children.get(i).mLatestParent == animNode) { -                    start(children.get(i)); -                } -            } -            // Listeners are already notified of the AnimatorSet ending in cancel() or -            // end(); the logic below only kicks in when animations end normally -            boolean allDone = true; -            // Traverse the tree and find if there's any unfinished node -            int size = mNodes.size(); -            for (int i = 0; i < size; i++) { -                if (!mNodes.get(i).mEnded) { -                    allDone = false; -                    break; -                } -            } -            if (allDone) { -                mStarted = false; -                mPaused = false; -                // If this was the last child animation to end, then notify listeners that this -                // AnimatorSet has ended -                if (mListeners != null) { -                    ArrayList<AnimatorListener> tmpListeners = -                            (ArrayList<AnimatorListener>) mListeners.clone(); -                    int numListeners = tmpListeners.size(); -                    for (int i = 0; i < numListeners; ++i) { -                        tmpListeners.get(i).onAnimationEnd(this); -                    } -                } -            } -        } -    } -      /**       * AnimatorSet is only reversible when the set contains no sequential animation, and no child       * animators have a start delay. @@ -861,32 +1204,21 @@ public final class AnimatorSet extends Animator {       */      @Override      public boolean canReverse() { -        if (!mReversible)  { -            return false; -        } -        // Loop to make sure all the Nodes can reverse. -        int size = mNodes.size(); -        for (int i = 0; i < size; i++) { -            Node node = mNodes.get(i); -            if (!node.mAnimation.canReverse() || node.mAnimation.getStartDelay() > 0) { -                return false; -            } -        } -        return true; +        return getTotalDuration() != DURATION_INFINITE;      }      /** -     * @hide +     * Plays the AnimatorSet in reverse. If the animation has been seeked to a specific play time +     * using {@link #setCurrentPlayTime(long)}, it will play backwards from the point seeked when +     * reverse was called. Otherwise, then it will start from the end and play backwards. This +     * behavior is only set for the current animation; future playing of the animation will use the +     * default behavior of playing forward. +     * <p> +     * Note: reverse is not supported for infinite AnimatorSet.       */      @Override      public void reverse() { -        if (canReverse()) { -            int size = mNodes.size(); -            for (int i = 0; i < size; i++) { -                Node node = mNodes.get(i); -                node.mAnimation.reverse(); -            } -        } +        start(true, true);      }      @Override @@ -993,18 +1325,61 @@ public final class AnimatorSet extends Animator {          mRootNode.mEndTime = mDelayAnim.getDuration();          updatePlayTime(mRootNode, visited); -        long maxEndTime = 0; -        for (int i = 0; i < size; i++) { +        sortAnimationEvents(); +        mTotalDuration = mEvents.get(mEvents.size() - 1).getTime(); +    } + +    private void sortAnimationEvents() { +        // Sort the list of events in ascending order of their time +        // Create the list including the delay animation. +        mEvents.clear(); +        for (int i = 0; i < mNodes.size(); i++) {              Node node = mNodes.get(i); -            node.mTotalDuration = node.mAnimation.getTotalDuration(); -            if (node.mEndTime == DURATION_INFINITE) { -                maxEndTime = DURATION_INFINITE; -                break; -            } else { -                maxEndTime = node.mEndTime > maxEndTime ? node.mEndTime : maxEndTime; +            mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_START)); +            mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_DELAY_ENDED)); +            mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_END)); +        } +        mEvents.sort(new Comparator<AnimationEvent>() { +            @Override +            public int compare(AnimationEvent e1, AnimationEvent e2) { +                long t1 = e1.getTime(); +                long t2 = e2.getTime(); +                if (t1 == t2) { +                    if (e1.mNode == e2.mNode) { +                        // For the same animation, start event has to happen before end. +                        return e1.mEvent - e2.mEvent; +                    } +                    // For different animation, end events need to happen before start, to ensure +                    // sequential animations finish the previous one before starting the next one. +                    return e2.mEvent - e1.mEvent; +                } +                if (t2 == DURATION_INFINITE) { +                    return -1; +                } +                if (t1 == DURATION_INFINITE) { +                    return 1; +                } +                // When neither event happens at INFINITE time: +                return (int) (t1 - t2);              } +        }); + +        if (mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_START +                || mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { +            throw new UnsupportedOperationException( +                    "Something went wrong, the last event is not an end event"); +        } +        if (mEvents.get(1).mEvent != AnimationEvent.ANIMATION_DELAY_ENDED +                || mEvents.get(1).mNode != mRootNode) { +            throw new UnsupportedOperationException( +                    "Sorting went bad, the root node's start delay end event should always be at" +                            + " index 1"); +        } +        if (mEvents.get(2).mEvent != AnimationEvent.ANIMATION_END +                || mEvents.get(2).mNode != mRootNode) { +            throw new UnsupportedOperationException( +                    "Sorting went bad, the start delay end event should always be at index 2");          } -        mTotalDuration = maxEndTime;      }      /** @@ -1235,6 +1610,95 @@ public final class AnimatorSet extends Animator {      }      /** +     * This class is a wrapper around a node and an event for the animation corresponding to the +     * node. The 3 types of events represent the start of an animation, the end of a start delay of +     * an animation, and the end of an animation. When playing forward (i.e. in the non-reverse +     * direction), start event marks when start() should be called, and end event corresponds to +     * when the animation should finish. When playing in reverse, start delay will not be a part +     * of the animation. Therefore, reverse() is called at the end event, and animation should end +     * at the delay ended event. +     */ +    private static class AnimationEvent { +        static final int ANIMATION_START = 0; +        static final int ANIMATION_DELAY_ENDED = 1; +        static final int ANIMATION_END = 2; +        final Node mNode; +        final int mEvent; + +        AnimationEvent(Node node, int event) { +            mNode = node; +            mEvent = event; +        } + +        long getTime() { +            if (mEvent == ANIMATION_START) { +                return mNode.mStartTime; +            } else if (mEvent == ANIMATION_DELAY_ENDED) { +                return mNode.mStartTime + mNode.mAnimation.getStartDelay(); +            } else { +                return mNode.mEndTime; +            } +        } + +        public String toString() { +            String eventStr = mEvent == ANIMATION_START ? "start" : ( +                    mEvent == ANIMATION_DELAY_ENDED ? "delay ended" : "end"); +            return eventStr + " " + mNode.mAnimation.toString(); +        } +    } + +    private class SeekState { +        private long mPlayTime = -1; +        private boolean mSeekingInReverse = false; +        void reset() { +            mPlayTime = -1; +            mSeekingInReverse = false; +        } + +        void setPlayTime(long playTime, boolean inReverse) { +            // TODO: This can be simplified. + +            // Clamp the play time +            if (getTotalDuration() != DURATION_INFINITE) { +                mPlayTime = Math.min(playTime, getTotalDuration() - mStartDelay); +            } +            mPlayTime = Math.max(0, mPlayTime); +            mSeekingInReverse = inReverse; +        } + +        void updateSeekDirection(boolean inReverse) { +            // Change seek direction without changing the overall fraction +            if (inReverse && getTotalDuration() == DURATION_INFINITE) { +                throw new UnsupportedOperationException("Error: Cannot reverse infinite animator" +                        + " set"); +            } +            if (mPlayTime >= 0) { +                if (inReverse != mSeekingInReverse) { +                    mPlayTime = getTotalDuration() - mStartDelay - mPlayTime; +                } +            } +        } + +        long getPlayTime() { +            return mPlayTime; +        } + +        /** +         * Returns the playtime assuming the animation is forward playing +         */ +        long getPlayTimeNormalized() { +            if (mReversing) { +                return getTotalDuration() - mStartDelay - mPlayTime; +            } +            return mPlayTime; +        } + +        boolean isActive() { +            return mPlayTime != -1; +        } +    } + +    /**       * The <code>Builder</code> object is a utility class to facilitate adding animations to a       * <code>AnimatorSet</code> along with the relationships between the various animations. The       * intention of the <code>Builder</code> methods, along with the {@link @@ -1328,7 +1792,6 @@ public final class AnimatorSet extends Animator {           * {@link AnimatorSet#play(Animator)} method ends.           */          public Builder before(Animator anim) { -            mReversible = false;              Node node = getNodeForAnimation(anim);              mCurrentNode.addChild(node);              return this; @@ -1343,7 +1806,6 @@ public final class AnimatorSet extends Animator {           * {@link AnimatorSet#play(Animator)} method to play.           */          public Builder after(Animator anim) { -            mReversible = false;              Node node = getNodeForAnimation(anim);              mCurrentNode.addParent(node);              return this; diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 4707bed3ee14..1e1f1554d3a2 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -992,6 +992,11 @@ public final class ObjectAnimator extends ValueAnimator {      }      @Override +    boolean isInitialized() { +        return mInitialized; +    } + +    @Override      public ObjectAnimator clone() {          final ObjectAnimator anim = (ObjectAnimator) super.clone();          return anim; diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index f0fc8af9d504..470523fc4252 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -24,6 +24,7 @@ import android.os.Trace;  import android.util.AndroidRuntimeException;  import android.util.Log;  import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation;  import android.view.animation.AnimationUtils;  import android.view.animation.LinearInterpolator; @@ -67,7 +68,7 @@ import java.util.HashMap;   * </div>   */  @SuppressWarnings("unchecked") -public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback { +public class ValueAnimator extends Animator {      private static final String TAG = "ValueAnimator";      private static final boolean DEBUG = false; @@ -90,7 +91,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio       *       * Whenever mStartTime is set, you must also update mStartTimeCommitted.       */ -    long mStartTime; +    long mStartTime = -1;      /**       * When true, the start time has been firmly committed as a chosen reference point in @@ -152,7 +153,13 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio      /**       * Tracks the time (in milliseconds) when the last frame arrived.       */ -    private long mLastFrameTime = 0; +    private long mLastFrameTime = -1; + +    /** +     * Tracks the time (in milliseconds) when the first frame arrived. Note the frame may arrive +     * during the start delay. +     */ +    private long mFirstFrameTime = -1;      /**       * Additional playing state to indicate whether an animator has been start()'d. There is @@ -212,6 +219,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio      private int mRepeatMode = RESTART;      /** +     * Whether or not the animator should register for its own animation callback to receive +     * animation pulse. +     */ +    private boolean mSelfPulse = true; + +    /**       * The time interpolator to be used. The elapsed fraction of the animation will be passed       * through this interpolator to calculate the interpolated fraction, which is then used to       * calculate the animated values. @@ -628,7 +641,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio              mSeekFraction = fraction;          }          mOverallFraction = fraction; -        final float currentIterationFraction = getCurrentIterationFraction(fraction); +        final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);          animateValue(currentIterationFraction);      } @@ -654,11 +667,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio       * should be played backwards. E.g. When the animation is played backwards in an iteration,       * the fraction for that iteration will go from 1f to 0f.       */ -    private float getCurrentIterationFraction(float fraction) { +    private float getCurrentIterationFraction(float fraction, boolean inReverse) {          fraction = clampFraction(fraction);          int iteration = getCurrentIteration(fraction);          float currentFraction = fraction - iteration; -        return shouldPlayBackward(iteration) ? 1f - currentFraction : currentFraction; +        return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;      }      /** @@ -682,18 +695,18 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio       * whether the entire animation is being reversed, 2) repeat mode applied to the current       * iteration.       */ -    private boolean shouldPlayBackward(int iteration) { +    private boolean shouldPlayBackward(int iteration, boolean inReverse) {          if (iteration > 0 && mRepeatMode == REVERSE &&                  (iteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {              // if we were seeked to some other iteration in a reversing animator,              // figure out the correct direction to start playing based on the iteration -            if (mReversing) { +            if (inReverse) {                  return (iteration % 2) == 0;              } else {                  return (iteration % 2) != 0;              }          } else { -            return mReversing; +            return inReverse;          }      } @@ -965,7 +978,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio                      (ArrayList<AnimatorListener>) mListeners.clone();              int numListeners = tmpListeners.size();              for (int i = 0; i < numListeners; ++i) { -                tmpListeners.get(i).onAnimationStart(this); +                tmpListeners.get(i).onAnimationStart(this, mReversing);              }          }          mStartListenersCalled = true; @@ -984,11 +997,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio       *       * @param playBackwards Whether the ValueAnimator should start playing in reverse.       */ -    private void start(boolean playBackwards) { +    private void start(boolean playBackwards, boolean selfPulse) {          if (Looper.myLooper() == null) {              throw new AndroidRuntimeException("Animators may only be run on Looper threads");          }          mReversing = playBackwards; +        mSelfPulse = selfPulse;          // Special case: reversing from seek-to-0 should act as if not seeked at all.          if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {              if (mRepeatCount == INFINITE) { @@ -1006,11 +1020,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          // Resets mLastFrameTime when start() is called, so that if the animation was running,          // calling start() would put the animation in the          // started-but-not-yet-reached-the-first-frame phase. -        mLastFrameTime = 0; -        AnimationHandler animationHandler = AnimationHandler.getInstance(); -        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale)); +        mLastFrameTime = -1; +        mFirstFrameTime = -1; +        addAnimationCallback((long) (mStartDelay * sDurationScale)); -        if (mStartDelay == 0 || mSeekFraction >= 0) { +        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {              // If there's no start delay, init the animation and notify start listeners right away              // to be consistent with the previous behavior. Otherwise, postpone this until the first              // frame after the start delay. @@ -1026,9 +1040,13 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          }      } +    void startWithoutPulsing(boolean inReverse) { +        start(inReverse, false); +    } +      @Override      public void start() { -        start(false); +        start(false, true);      }      @Override @@ -1073,7 +1091,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          } else if (!mInitialized) {              initAnimation();          } -        animateValue(shouldPlayBackward(mRepeatCount) ? 0f : 1f); +        animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);          endAnimation();      } @@ -1086,8 +1104,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          if (mPaused && !mResumed) {              mResumed = true;              if (mPauseTime > 0) { -                AnimationHandler handler = AnimationHandler.getInstance(); -                handler.addAnimationFrameCallback(this, 0); +                addAnimationCallback(0);              }          }          super.resume(); @@ -1133,7 +1150,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio              mReversing = !mReversing;              end();          } else { -            start(true); +            start(true, true);          }      } @@ -1153,8 +1170,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          if (mAnimationEndRequested) {              return;          } -        AnimationHandler handler = AnimationHandler.getInstance(); -        handler.removeCallback(this); +        removeAnimationCallback();          mAnimationEndRequested = true;          mPaused = false; @@ -1166,16 +1182,18 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          mRunning = false;          mStarted = false;          mStartListenersCalled = false; +        mLastFrameTime = -1; +        mFirstFrameTime = -1;          mReversing = false; -        mLastFrameTime = 0;          if (notify && mListeners != null) {              ArrayList<AnimatorListener> tmpListeners =                      (ArrayList<AnimatorListener>) mListeners.clone();              int numListeners = tmpListeners.size();              for (int i = 0; i < numListeners; ++i) { -                tmpListeners.get(i).onAnimationEnd(this); +                tmpListeners.get(i).onAnimationEnd(this, mReversing);              }          } +        mReversing = false;          if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {              Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),                      System.identityHashCode(this)); @@ -1211,7 +1229,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio       * is called (or after start delay if any), which may be before the animation loop starts.       */      private boolean isPulsingInternal() { -        return mLastFrameTime > 0; +        return mLastFrameTime >= 0;      }      /** @@ -1276,24 +1294,116 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio                  done = true;              }              mOverallFraction = clampFraction(fraction); -            float currentIterationFraction = getCurrentIterationFraction(mOverallFraction); +            float currentIterationFraction = getCurrentIterationFraction( +                    mOverallFraction, mReversing);              animateValue(currentIterationFraction);          }          return done;      }      /** +     * Internal use only. +     * +     * This method does not modify any fields of the animation. It should be called when seeking +     * in an AnimatorSet. When the last play time and current play time are of different repeat +     * iterations, +     * {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)} +     * will be called. +     */ +    @Override +    void animateBasedOnPlayTime(long currentPlayTime, long lastPlayTime, boolean inReverse) { +        if (currentPlayTime < 0 || lastPlayTime < 0) { +            throw new UnsupportedOperationException("Error: Play time should never be negative."); +        } + +        initAnimation(); +        // Check whether repeat callback is needed only when repeat count is non-zero +        if (mRepeatCount > 0) { +            int iteration = (int) (currentPlayTime / mDuration); +            int lastIteration = (int) (lastPlayTime / mDuration); + +            // Clamp iteration to [0, mRepeatCount] +            iteration = Math.min(iteration, mRepeatCount); +            lastIteration = Math.min(lastIteration, mRepeatCount); + +            if (iteration != lastIteration) { +                if (mListeners != null) { +                    int numListeners = mListeners.size(); +                    for (int i = 0; i < numListeners; ++i) { +                        mListeners.get(i).onAnimationRepeat(this); +                    } +                } +            } +        } + +        if (mRepeatCount != INFINITE && currentPlayTime >= (mRepeatCount + 1) * mDuration) { +            skipToEndValue(inReverse); +        } else { +            // Find the current fraction: +            float fraction = currentPlayTime / (float) mDuration; +            fraction = getCurrentIterationFraction(fraction, inReverse); +            animateValue(fraction); +        } +    } + +    /** +     * Internal use only. +     * Skips the animation value to end/start, depending on whether the play direction is forward +     * or backward. +     * +     * @param inReverse whether the end value is based on a reverse direction. If yes, this is +     *                  equivalent to skip to start value in a forward playing direction. +     */ +    void skipToEndValue(boolean inReverse) { +        initAnimation(); +        float endFraction = inReverse ? 0f : 1f; +        if (mRepeatCount % 2 == 1 && mRepeatMode == REVERSE) { +            // This would end on fraction = 0 +            endFraction = 0f; +        } +        animateValue(endFraction); +    } + +    /**       * Processes a frame of the animation, adjusting the start time if needed.       *       * @param frameTime The frame time.       * @return true if the animation has ended.       * @hide       */ -    public final void doAnimationFrame(long frameTime) { -        AnimationHandler handler = AnimationHandler.getInstance(); -        if (mLastFrameTime == 0) { +    public final boolean doAnimationFrame(long frameTime) { +        if (!mRunning && mStartTime < 0) { +            // First frame during delay +            mStartTime = frameTime + mStartDelay; +        } + +        // Handle pause/resume +        if (mPaused) { +            mPauseTime = frameTime; +            removeAnimationCallback(); +            return false; +        } else if (mResumed) { +            mResumed = false; +            if (mPauseTime > 0) { +                // Offset by the duration that the animation was paused +                mStartTime += (frameTime - mPauseTime); +            } +        } + +        if (!mRunning) { +            // If not running, that means the animation is in the start delay phase. In the case of +            // reversing, we want to run start delay in the end. +            if (mStartTime > frameTime) { +                // During start delay +                return false; +            } else { +                // Start delay has passed. +                mRunning = true; +            } +        } + +        if (mLastFrameTime < 0) {              // First frame -            handler.addOneShotCommitCallback(this);              if (mStartDelay > 0) {                  startAnimation();              } @@ -1307,19 +1417,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio              mStartTimeCommitted = false; // allow start time to be compensated for jank          }          mLastFrameTime = frameTime; -        if (mPaused) { -            mPauseTime = frameTime; -            handler.removeCallback(this); -            return; -        } else if (mResumed) { -            mResumed = false; -            if (mPauseTime > 0) { -                // Offset by the duration that the animation was paused -                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 @@ -1330,6 +1427,31 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          if (finished) {              endAnimation();          } +        return finished; +    } + +    private void addOneShotCommitCallback() { +        if (!mSelfPulse) { +            return; +        } +        AnimationHandler handler = AnimationHandler.getInstance(); +        handler.addOneShotCommitCallback(this); +    } + +    private void removeAnimationCallback() { +        if (!mSelfPulse) { +            return; +        } +        AnimationHandler handler = AnimationHandler.getInstance(); +        handler.removeCallback(this); +    } + +    private void addAnimationCallback(long delay) { +        if (!mSelfPulse) { +            return; +        } +        AnimationHandler handler = AnimationHandler.getInstance(); +        handler.addAnimationFrameCallback(this, delay);      }      /** @@ -1384,11 +1506,12 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio          anim.mPaused = false;          anim.mResumed = false;          anim.mStartListenersCalled = false; -        anim.mStartTime = 0; +        anim.mStartTime = -1;          anim.mStartTimeCommitted = false;          anim.mAnimationEndRequested = false; -        anim.mPauseTime = 0; -        anim.mLastFrameTime = 0; +        anim.mPauseTime = -1; +        anim.mLastFrameTime = -1; +        anim.mFirstFrameTime = -1;          anim.mOverallFraction = 0;          anim.mCurrentFraction = 0;  |