diff options
Diffstat (limited to 'android/animation/AnimatorSet.java')
-rw-r--r-- | android/animation/AnimatorSet.java | 2091 |
1 files changed, 2091 insertions, 0 deletions
diff --git a/android/animation/AnimatorSet.java b/android/animation/AnimatorSet.java new file mode 100644 index 00000000..00d6657e --- /dev/null +++ b/android/animation/AnimatorSet.java @@ -0,0 +1,2091 @@ +/* + * Copyright (C) 2010 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.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.HashMap; +import java.util.List; + +/** + * This class plays a set of {@link Animator} objects in the specified order. Animations + * can be set up to play together, in sequence, or after a specified delay. + * + * <p>There are two different approaches to adding animations to a <code>AnimatorSet</code>: + * either the {@link AnimatorSet#playTogether(Animator[]) playTogether()} or + * {@link AnimatorSet#playSequentially(Animator[]) playSequentially()} methods can be called to add + * a set of animations all at once, or the {@link AnimatorSet#play(Animator)} can be + * used in conjunction with methods in the {@link AnimatorSet.Builder Builder} + * class to add animations + * one by one.</p> + * + * <p>It is possible to set up a <code>AnimatorSet</code> with circular dependencies between + * its animations. For example, an animation a1 could be set up to start before animation a2, a2 + * before a3, and a3 before a1. The results of this configuration are undefined, but will typically + * result in none of the affected animations being played. Because of this (and because + * circular dependencies do not make logical sense anyway), circular dependencies + * should be avoided, and the dependency flow of animations should only be in one direction. + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about animating with {@code AnimatorSet}, read the + * <a href="{@docRoot}guide/topics/graphics/prop-animation.html#choreography">Property + * Animation</a> developer guide.</p> + * </div> + */ +public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback { + + private static final String TAG = "AnimatorSet"; + /** + * 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 + * to clone() to make deep copies of them. + */ + + /** + * 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<Node> mPlayingSet = new ArrayList<Node>(); + + /** + * Contains all nodes, mapped to their respective Animators. When new + * dependency information is added for an Animator, we want to add it + * to a single node representing that Animator, not create a new Node + * if one already exists. + */ + 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. + */ + private ArrayList<Node> mNodes = new ArrayList<Node>(); + + /** + * Tracks whether any change has been made to the AnimatorSet, which is then used to + * determine whether the dependency graph should be re-constructed. + */ + private boolean mDependencyDirty = false; + + /** + * Indicates whether an AnimatorSet has been start()'d, whether or + * not there is a nonzero startDelay. + */ + private boolean mStarted = false; + + // The amount of time in ms to delay starting the animation after start() is called + private long mStartDelay = 0; + + // Animator used for a nonzero startDelay + private ValueAnimator mDelayAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(0); + + // Root of the dependency tree of all the animators in the set. In this tree, parent-child + // relationship captures the order of animation (i.e. parent and child will play sequentially), + // and sibling relationship indicates "with" relationship, as sibling animators start at the + // same time. + private Node mRootNode = new Node(mDelayAnim); + + // How long the child animations should last in ms. The default value is negative, which + // simply means that there is no duration set on the AnimatorSet. When a real duration is + // set, it is passed along to the child animations. + private long mDuration = -1; + + // Records the interpolator for the set. Null value indicates that no interpolator + // was set on this AnimatorSet, so it should not be passed down to the children. + private TimeInterpolator mInterpolator = null; + + // The total duration of finishing all the Animators in the set. + private long mTotalDuration = 0; + + // In pre-N releases, calling end() before start() on an animator set is no-op. But that is not + // consistent with the behavior for other animator types. In order to keep the behavior + // consistent within Animation framework, when end() is called without start(), we will start + // 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; + + // In pre-O releases, end() may never explicitly called on a child animator. As a result, end() + // may not even be properly implemented in a lot of cases. After a few apps crashing on this, + // it became necessary to use an sdk target guard for calling end(). + private final boolean mEndCanBeCalled; + + // 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. This is the + // frame before we start counting down the start delay, if any. + // -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; + + // This is to work around a bug in b/34736819. This needs to be removed once app team + // fixes their side. + private AnimatorListenerAdapter mDummyListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mNodeMap.get(animation) == null) { + throw new AndroidRuntimeException("Error: animation ended is not in the node map"); + } + mNodeMap.get(animation).mEnded = true; + + } + }; + + public AnimatorSet() { + super(); + mNodeMap.put(mDelayAnim, mRootNode); + mNodes.add(mRootNode); + boolean isPreO; + // Set the flag to ignore calling end() without start() for pre-N releases + Application app = ActivityThread.currentApplication(); + if (app == null || app.getApplicationInfo() == null) { + mShouldIgnoreEndWithoutStart = true; + isPreO = true; + } else { + if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { + mShouldIgnoreEndWithoutStart = true; + } else { + mShouldIgnoreEndWithoutStart = false; + } + + isPreO = app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O; + } + mShouldResetValuesAtStart = !isPreO; + mEndCanBeCalled = !isPreO; + } + + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + * This is equivalent to calling {@link #play(Animator)} with the first animator in the + * set and then {@link Builder#with(Animator)} with each of the other animators. Note that + * an Animator with a {@link Animator#setStartDelay(long) startDelay} will not actually + * start until that delay elapses, which means that if the first animator in the list + * supplied to this constructor has a startDelay, none of the other animators will start + * until that first animator's startDelay has elapsed. + * + * @param items The animations that will be started simultaneously. + */ + public void playTogether(Animator... items) { + if (items != null) { + Builder builder = play(items[0]); + for (int i = 1; i < items.length; ++i) { + builder.with(items[i]); + } + } + } + + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + * + * @param items The animations that will be started simultaneously. + */ + public void playTogether(Collection<Animator> items) { + if (items != null && items.size() > 0) { + Builder builder = null; + for (Animator anim : items) { + if (builder == null) { + builder = play(anim); + } else { + builder.with(anim); + } + } + } + } + + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The animations that will be started one after another. + */ + public void playSequentially(Animator... items) { + if (items != null) { + if (items.length == 1) { + play(items[0]); + } else { + for (int i = 0; i < items.length - 1; ++i) { + play(items[i]).before(items[i + 1]); + } + } + } + } + + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The animations that will be started one after another. + */ + public void playSequentially(List<Animator> items) { + if (items != null && items.size() > 0) { + if (items.size() == 1) { + play(items.get(0)); + } else { + for (int i = 0; i < items.size() - 1; ++i) { + play(items.get(i)).before(items.get(i + 1)); + } + } + } + } + + /** + * Returns the current list of child Animator objects controlled by this + * AnimatorSet. This is a copy of the internal list; modifications to the returned list + * will not affect the AnimatorSet, although changes to the underlying Animator objects + * will affect those objects being managed by the AnimatorSet. + * + * @return ArrayList<Animator> The list of child animations of this AnimatorSet. + */ + public ArrayList<Animator> getChildAnimations() { + ArrayList<Animator> childList = new ArrayList<Animator>(); + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node != mRootNode) { + childList.add(node.mAnimation); + } + } + return childList; + } + + /** + * Sets the target object for all current {@link #getChildAnimations() child animations} + * of this AnimatorSet that take targets ({@link ObjectAnimator} and + * AnimatorSet). + * + * @param target The object being animated + */ + @Override + public void setTarget(Object target) { + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + Animator animation = node.mAnimation; + if (animation instanceof AnimatorSet) { + ((AnimatorSet)animation).setTarget(target); + } else if (animation instanceof ObjectAnimator) { + ((ObjectAnimator)animation).setTarget(target); + } + } + } + + /** + * @hide + */ + @Override + public int getChangingConfigurations() { + int conf = super.getChangingConfigurations(); + final int nodeCount = mNodes.size(); + for (int i = 0; i < nodeCount; i ++) { + conf |= mNodes.get(i).mAnimation.getChangingConfigurations(); + } + return conf; + } + + /** + * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations} + * of this AnimatorSet. The default value is null, which means that no interpolator + * is set on this AnimatorSet. Setting the interpolator to any non-null value + * will cause that interpolator to be set on the child animations + * when the set is started. + * + * @param interpolator the interpolator to be used by each child animation of this AnimatorSet + */ + @Override + public void setInterpolator(TimeInterpolator interpolator) { + mInterpolator = interpolator; + } + + @Override + public TimeInterpolator getInterpolator() { + return mInterpolator; + } + + /** + * This method creates a <code>Builder</code> object, which is used to + * set up playing constraints. This initial <code>play()</code> method + * tells the <code>Builder</code> the animation that is the dependency for + * the succeeding commands to the <code>Builder</code>. For example, + * calling <code>play(a1).with(a2)</code> sets up the AnimatorSet to play + * <code>a1</code> and <code>a2</code> at the same time, + * <code>play(a1).before(a2)</code> sets up the AnimatorSet to play + * <code>a1</code> first, followed by <code>a2</code>, and + * <code>play(a1).after(a2)</code> sets up the AnimatorSet to play + * <code>a2</code> first, followed by <code>a1</code>. + * + * <p>Note that <code>play()</code> is the only way to tell the + * <code>Builder</code> the animation upon which the dependency is created, + * so successive calls to the various functions in <code>Builder</code> + * will all refer to the initial parameter supplied in <code>play()</code> + * as the dependency of the other animations. For example, calling + * <code>play(a1).before(a2).before(a3)</code> will play both <code>a2</code> + * and <code>a3</code> when a1 ends; it does not set up a dependency between + * <code>a2</code> and <code>a3</code>.</p> + * + * @param anim The animation that is the dependency used in later calls to the + * methods in the returned <code>Builder</code> object. A null parameter will result + * in a null <code>Builder</code> return value. + * @return Builder The object that constructs the AnimatorSet based on the dependencies + * outlined in the calls to <code>play</code> and the other methods in the + * <code>Builder</code object. + */ + public Builder play(Animator anim) { + if (anim != null) { + return new Builder(anim); + } + return null; + } + + /** + * {@inheritDoc} + * + * <p>Note that canceling a <code>AnimatorSet</code> also cancels all of the animations that it + * is responsible for.</p> + */ + @SuppressWarnings("unchecked") + @Override + public void cancel() { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException("Animators may only be run on Looper threads"); + } + if (isStarted()) { + ArrayList<AnimatorListener> tmpListeners = null; + if (mListeners != null) { + tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone(); + int size = tmpListeners.size(); + for (int i = 0; i < size; i++) { + tmpListeners.get(i).onAnimationCancel(this); + } + } + ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet); + int setSize = playingSet.size(); + for (int i = 0; i < setSize; i++) { + playingSet.get(i).mAnimation.cancel(); + } + mPlayingSet.clear(); + endAnimation(); + } + } + + // Force all the animations to end when the duration scale is 0. + private void forceToEnd() { + if (mEndCanBeCalled) { + end(); + return; + } + + // Note: we don't want to combine this case with the end() method below because in + // the case of developer calling end(), we still need to make sure end() is explicitly + // called on the child animators to maintain the old behavior. + if (mReversing) { + handleAnimationEvents(mLastEventId, 0, getTotalDuration()); + } else { + long zeroScalePlayTime = getTotalDuration(); + if (zeroScalePlayTime == DURATION_INFINITE) { + // Use a large number for the play time. + zeroScalePlayTime = Integer.MAX_VALUE; + } + handleAnimationEvents(mLastEventId, mEvents.size() - 1, zeroScalePlayTime); + } + mPlayingSet.clear(); + endAnimation(); + } + + /** + * {@inheritDoc} + * + * <p>Note that ending a <code>AnimatorSet</code> also ends all of the animations that it is + * responsible for.</p> + */ + @Override + public void end() { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException("Animators may only be run on Looper threads"); + } + if (mShouldIgnoreEndWithoutStart && !isStarted()) { + return; + } + if (isStarted()) { + // 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; + while (mLastEventId > 0) { + mLastEventId = mLastEventId - 1; + AnimationEvent event = mEvents.get(mLastEventId); + Animator anim = event.mNode.mAnimation; + if (mNodeMap.get(anim).mEnded) { + continue; + } + if (event.mEvent == AnimationEvent.ANIMATION_END) { + anim.reverse(); + } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED + && anim.isStarted()) { + // Make sure anim hasn't finished before calling end() so that we don't end + // already ended animations, which will cause start and end callbacks to be + // triggered again. + anim.end(); + } + } + } else { + while (mLastEventId < mEvents.size() - 1) { + // Avoid potential reentrant loop caused by child animators manipulating + // AnimatorSet's lifecycle (i.e. not a recommended approach). + mLastEventId = mLastEventId + 1; + AnimationEvent event = mEvents.get(mLastEventId); + Animator anim = event.mNode.mAnimation; + if (mNodeMap.get(anim).mEnded) { + continue; + } + if (event.mEvent == AnimationEvent.ANIMATION_START) { + anim.start(); + } else if (event.mEvent == AnimationEvent.ANIMATION_END && anim.isStarted()) { + // Make sure anim hasn't finished before calling end() so that we don't end + // already ended animations, which will cause start and end callbacks to be + // triggered again. + anim.end(); + } + } + } + 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 + * its initial delay set through {@link #setStartDelay(long)}. + * + * @return Whether this AnimatorSet has gone past the initial delay, and at least one child + * animation has been started and not yet ended. + */ + @Override + public boolean isRunning() { + if (mStartDelay == 0) { + return mStarted; + } + return mLastFrameTime > 0; + } + + @Override + public boolean isStarted() { + return mStarted; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + @Override + public long getStartDelay() { + return mStartDelay; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. Note that the start delay should always be non-negative. Any + * negative start delay will be clamped to 0 on N and above. + * + * @param startDelay The amount of the delay, in milliseconds + */ + @Override + public void setStartDelay(long startDelay) { + // Clamp start delay to non-negative range. + if (startDelay < 0) { + Log.w(TAG, "Start delay should always be non-negative"); + startDelay = 0; + } + long delta = startDelay - mStartDelay; + if (delta == 0) { + return; + } + mStartDelay = startDelay; + if (!mDependencyDirty) { + // Dependency graph already constructed, update all the nodes' start/end time + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node == mRootNode) { + node.mEndTime = mStartDelay; + } else { + node.mStartTime = node.mStartTime == DURATION_INFINITE ? + DURATION_INFINITE : node.mStartTime + delta; + node.mEndTime = node.mEndTime == DURATION_INFINITE ? + DURATION_INFINITE : node.mEndTime + delta; + } + } + // Update total duration, if necessary. + if (mTotalDuration != DURATION_INFINITE) { + mTotalDuration += delta; + } + } + } + + /** + * Gets the length of each of the child animations of this AnimatorSet. This value may + * be less than 0, which indicates that no duration has been set on this AnimatorSet + * and each of the child animations will use their own duration. + * + * @return The length of the animation, in milliseconds, of each of the child + * animations of this AnimatorSet. + */ + @Override + public long getDuration() { + return mDuration; + } + + /** + * Sets the length of each of the current child animations of this AnimatorSet. By default, + * each child animation will use its own duration. If the duration is set on the AnimatorSet, + * then each child animation inherits this duration. + * + * @param duration The length of the animation, in milliseconds, of each of the child + * animations of this AnimatorSet. + */ + @Override + public AnimatorSet setDuration(long duration) { + if (duration < 0) { + throw new IllegalArgumentException("duration must be a value of zero or greater"); + } + mDependencyDirty = true; + // Just record the value for now - it will be used later when the AnimatorSet starts + mDuration = duration; + return this; + } + + @Override + public void setupStartValues() { + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node != mRootNode) { + node.mAnimation.setupStartValues(); + } + } + } + + @Override + public void setupEndValues() { + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node != mRootNode) { + node.mAnimation.setupEndValues(); + } + } + } + + @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) { + 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 (mPauseTime >= 0) { + addAnimationCallback(0); + } + } + } + + /** + * {@inheritDoc} + * + * <p>Starting this <code>AnimatorSet</code> will, in turn, start the animations for which + * it is responsible. The details of when exactly those animations are started depends on + * the dependency relationships that have been set up between the animations. + * + * <b>Note:</b> Manipulating AnimatorSet's lifecycle in the child animators' listener callbacks + * will lead to undefined behaviors. Also, AnimatorSet will ignore any seeking in the child + * animators once {@link #start()} is called. + */ + @SuppressWarnings("unchecked") + @Override + public void start() { + 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++) { + Node node = mNodes.get(i); + node.mEnded = false; + node.mAnimation.setAllowRunningAsynchronously(false); + } + + initAnimation(); + if (inReverse && !canReverse()) { + throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet"); + } + + mReversing = inReverse; + + // Now that all dependencies are set up, start the animations that should be started. + boolean isEmptySet = isEmptySet(this); + if (!isEmptySet) { + startAnimation(); + } + + if (mListeners != null) { + ArrayList<AnimatorListener> tmpListeners = + (ArrayList<AnimatorListener>) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationStart(this, inReverse); + } + } + if (isEmptySet) { + // In the case of empty AnimatorSet, or 0 duration scale, we will trigger the + // onAnimationEnd() right away. + end(); + } + } + + // Returns true if set is empty or contains nothing but animator sets with no start delay. + private static boolean isEmptySet(AnimatorSet set) { + if (set.getStartDelay() > 0) { + return false; + } + for (int i = 0; i < set.getChildAnimations().size(); i++) { + Animator anim = set.getChildAnimations().get(i); + if (!(anim instanceof AnimatorSet)) { + // Contains non-AnimatorSet, not empty. + return false; + } else { + if (!isEmptySet((AnimatorSet) anim)) { + return false; + } + } + } + return true; + } + + private void updateAnimatorsDuration() { + if (mDuration >= 0) { + // If the duration was set on this AnimatorSet, pass it along to all child animations + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to + // insert "play-after" delays + node.mAnimation.setDuration(mDuration); + } + } + mDelayAnim.setDuration(mStartDelay); + } + + @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); + if (!inReverse) { + playTime -= node.mAnimation.getStartDelay(); + } + 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); + } + } + + /** + * Returns the milliseconds elapsed since the start of the animation. + * + * <p>For ongoing animations, this method returns the current progress of the animation in + * terms of play time. For an animation that has not yet been started: if the animation has been + * seeked to a certain time via {@link #setCurrentPlayTime(long)}, the seeked play time will + * be returned; otherwise, this method will return 0. + * + * @return the current position in time of the animation in milliseconds + */ + public long getCurrentPlayTime() { + if (mSeekState.isActive()) { + return mSeekState.getPlayTime(); + } + if (mLastFrameTime == -1) { + // Not yet started or during start delay + return 0; + } + float durationScale = ValueAnimator.getDurationScale(); + durationScale = durationScale == 0 ? 1 : durationScale; + if (mReversing) { + return (long) ((mLastFrameTime - mFirstFrame) / durationScale); + } else { + return (long) ((mLastFrameTime - mFirstFrame - mStartDelay) / durationScale); + } + } + + 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) { + float durationScale = ValueAnimator.getDurationScale(); + if (durationScale == 0f) { + // Duration scale is 0, end the animation right away. + forceToEnd(); + return true; + } + + // After the first frame comes in, we need to wait for start delay to pass before updating + // any animation values. + if (mFirstFrame < 0) { + mFirstFrame = 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); + if (mReversing) { + mFirstFrame = (long) (frameTime - mSeekState.getPlayTime() * durationScale); + } else { + mFirstFrame = (long) (frameTime - (mSeekState.getPlayTime() + mStartDelay) + * durationScale); + } + mSeekState.reset(); + } + + if (!mReversing && frameTime < mFirstFrame + mStartDelay * durationScale) { + // Still during start delay in a forward playing case. + return false; + } + + // From here on, we always use unscaled play time. Note this unscaled playtime includes + // the start delay. + long unscaledPlayTime = (long) ((frameTime - mFirstFrame) / durationScale); + mLastFrameTime = frameTime; + + // 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(unscaledPlayTime); + int startId = mLastEventId; + + handleAnimationEvents(startId, latestId, unscaledPlayTime); + + 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) { + pulseFrame(node, getPlayTimeForNode(unscaledPlayTime, node)); + } + } + + // Remove all the finished anims + for (int i = mPlayingSet.size() - 1; i >= 0; i--) { + if (mPlayingSet.get(i).mEnded) { + mPlayingSet.remove(i); + } + } + + boolean finished = false; + if (mReversing) { + if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) { + // The only animation that is running is the delay animation. + finished = true; + } else if (mPlayingSet.isEmpty() && mLastEventId < 3) { + // The only remaining animation is the delay animation + finished = true; + } + } else { + finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1; + } + + if (finished) { + endAnimation(); + return true; + } + return false; + } + + /** + * @hide + */ + @Override + public void commitAnimationFrame(long frameTime) { + // No op. + } + + @Override + boolean pulseAnimationFrame(long frameTime) { + return doAnimationFrame(frameTime); + } + + /** + * 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) { + if (node.mAnimation.isStarted()) { + // If the animation has already been started before its due time (i.e. + // the child animator is being manipulated outside of the AnimatorSet), we + // need to cancel the animation to reset the internal state (e.g. frame + // time tracking) and remove the self pulsing callbacks + node.mAnimation.cancel(); + } + node.mEnded = false; + mPlayingSet.add(event.mNode); + node.mAnimation.startWithoutPulsing(true); + pulseFrame(node, 0); + } else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) { + // end event: + pulseFrame(node, 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); + if (node.mAnimation.isStarted()) { + // If the animation has already been started before its due time (i.e. + // the child animator is being manipulated outside of the AnimatorSet), we + // need to cancel the animation to reset the internal state (e.g. frame + // time tracking) and remove the self pulsing callbacks + node.mAnimation.cancel(); + } + node.mEnded = false; + node.mAnimation.startWithoutPulsing(false); + pulseFrame(node, 0); + } else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) { + // start event: + pulseFrame(node, getPlayTimeForNode(playTime, node)); + } + } + } + } + + /** + * This method pulses frames into child animations. It scales the input animation play time + * with the duration scale and pass that to the child animation via pulseAnimationFrame(long). + * + * @param node child animator node + * @param animPlayTime unscaled play time (including start delay) for the child animator + */ + private void pulseFrame(Node node, long animPlayTime) { + if (!node.mEnded) { + float durationScale = ValueAnimator.getDurationScale(); + durationScale = durationScale == 0 ? 1 : durationScale; + node.mEnded = node.mAnimation.pulseAnimationFrame( + (long) (animPlayTime * durationScale)); + } + } + + 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() { + addDummyListener(); + + // Register animation callback + addAnimationCallback(0); + + 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 (isInitialized()) { + skipToEndValue(!mReversing); + } else if (mReversing) { + // Reversing but haven't initialized all the children yet. + initChildren(); + 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); + for (int i = mPlayingSet.size() - 1; i >= 0; i--) { + if (mPlayingSet.get(i).mEnded) { + mPlayingSet.remove(i); + } + } + mLastEventId = toId; + } + } + + // This is to work around the issue in b/34736819, as the old behavior in AnimatorSet had + // masked a real bug in play movies. TODO: remove this and below once the root cause is fixed. + private void addDummyListener() { + for (int i = 1; i < mNodes.size(); i++) { + mNodes.get(i).mAnimation.addListener(mDummyListener); + } + } + + private void removeDummyListener() { + for (int i = 1; i < mNodes.size(); i++) { + mNodes.get(i).mAnimation.removeListener(mDummyListener); + } + } + + 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); + } + } + removeDummyListener(); + 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 + public AnimatorSet clone() { + final AnimatorSet anim = (AnimatorSet) super.clone(); + /* + * The basic clone() operation copies all items. This doesn't work very well for + * AnimatorSet, because it will copy references that need to be recreated and state + * that may not apply. What we need to do now is put the clone in an uninitialized + * state, with fresh, empty data structures. Then we will build up the nodes list + * manually, as we clone each Node (and its animation). The clone will then be sorted, + * and will populate any appropriate lists, when it is started. + */ + final int nodeCount = mNodes.size(); + anim.mStarted = false; + 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.mEvents = new ArrayList<AnimationEvent>(); + anim.mDummyListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (anim.mNodeMap.get(animation) == null) { + throw new AndroidRuntimeException("Error: animation ended is not in the node" + + " map"); + } + anim.mNodeMap.get(animation).mEnded = true; + + } + }; + 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. + // We need to track the old/new nodes in order to reconstruct the dependencies in the clone. + + HashMap<Node, Node> clonesMap = new HashMap<>(nodeCount); + for (int n = 0; n < nodeCount; n++) { + final Node node = mNodes.get(n); + Node nodeClone = node.clone(); + // Remove the old internal listener from the cloned child + nodeClone.mAnimation.removeListener(mDummyListener); + clonesMap.put(node, nodeClone); + anim.mNodes.add(nodeClone); + anim.mNodeMap.put(nodeClone.mAnimation, nodeClone); + } + + anim.mRootNode = clonesMap.get(mRootNode); + anim.mDelayAnim = (ValueAnimator) anim.mRootNode.mAnimation; + + // Now that we've cloned all of the nodes, we're ready to walk through their + // dependencies, mapping the old dependencies to the new nodes + for (int i = 0; i < nodeCount; i++) { + Node node = mNodes.get(i); + // Update dependencies for node's clone + Node nodeClone = clonesMap.get(node); + nodeClone.mLatestParent = node.mLatestParent == null + ? null : clonesMap.get(node.mLatestParent); + int size = node.mChildNodes == null ? 0 : node.mChildNodes.size(); + for (int j = 0; j < size; j++) { + nodeClone.mChildNodes.set(j, clonesMap.get(node.mChildNodes.get(j))); + } + size = node.mSiblings == null ? 0 : node.mSiblings.size(); + for (int j = 0; j < size; j++) { + nodeClone.mSiblings.set(j, clonesMap.get(node.mSiblings.get(j))); + } + size = node.mParents == null ? 0 : node.mParents.size(); + for (int j = 0; j < size; j++) { + nodeClone.mParents.set(j, clonesMap.get(node.mParents.get(j))); + } + } + return anim; + } + + + /** + * AnimatorSet is only reversible when the set contains no sequential animation, and no child + * animators have a start delay. + * @hide + */ + @Override + public boolean canReverse() { + return getTotalDuration() != DURATION_INFINITE; + } + + /** + * 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() { + start(true, true); + } + + @Override + public String toString() { + String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{"; + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + returnVal += "\n " + node.mAnimation.toString(); + } + return returnVal + "\n}"; + } + + private void printChildCount() { + // Print out the child count through a level traverse. + ArrayList<Node> list = new ArrayList<>(mNodes.size()); + list.add(mRootNode); + Log.d(TAG, "Current tree: "); + int index = 0; + while (index < list.size()) { + int listSize = list.size(); + StringBuilder builder = new StringBuilder(); + for (; index < listSize; index++) { + Node node = list.get(index); + int num = 0; + if (node.mChildNodes != null) { + for (int i = 0; i < node.mChildNodes.size(); i++) { + Node child = node.mChildNodes.get(i); + if (child.mLatestParent == node) { + num++; + list.add(child); + } + } + } + builder.append(" "); + builder.append(num); + } + Log.d(TAG, builder.toString()); + } + } + + private void createDependencyGraph() { + if (!mDependencyDirty) { + // Check whether any duration of the child animations has changed + boolean durationChanged = false; + for (int i = 0; i < mNodes.size(); i++) { + Animator anim = mNodes.get(i).mAnimation; + if (mNodes.get(i).mTotalDuration != anim.getTotalDuration()) { + durationChanged = true; + break; + } + } + if (!durationChanged) { + return; + } + } + + mDependencyDirty = false; + // Traverse all the siblings and make sure they have all the parents + int size = mNodes.size(); + for (int i = 0; i < size; i++) { + mNodes.get(i).mParentsAdded = false; + } + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node.mParentsAdded) { + continue; + } + + node.mParentsAdded = true; + if (node.mSiblings == null) { + continue; + } + + // Find all the siblings + findSiblings(node, node.mSiblings); + node.mSiblings.remove(node); + + // Get parents from all siblings + int siblingSize = node.mSiblings.size(); + for (int j = 0; j < siblingSize; j++) { + node.addParents(node.mSiblings.get(j).mParents); + } + + // Now make sure all siblings share the same set of parents + for (int j = 0; j < siblingSize; j++) { + Node sibling = node.mSiblings.get(j); + sibling.addParents(node.mParents); + sibling.mParentsAdded = true; + } + } + + for (int i = 0; i < size; i++) { + Node node = mNodes.get(i); + if (node != mRootNode && node.mParents == null) { + node.addParent(mRootNode); + } + } + + // Do a DFS on the tree + ArrayList<Node> visited = new ArrayList<Node>(mNodes.size()); + // Assign start/end time + mRootNode.mStartTime = 0; + mRootNode.mEndTime = mDelayAnim.getDuration(); + updatePlayTime(mRootNode, visited); + + 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 = 1; i < mNodes.size(); i++) { + Node node = mNodes.get(i); + 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) { + // For events that happen at the same time, we need them to be in the sequence + // (end, start, start delay ended) + if (e2.mEvent + e1.mEvent == AnimationEvent.ANIMATION_START + + AnimationEvent.ANIMATION_DELAY_ENDED) { + // Ensure start delay happens after start + return e1.mEvent - e2.mEvent; + } else { + 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); + } + }); + + int eventSize = mEvents.size(); + // For the same animation, start event has to happen before end. + for (int i = 0; i < eventSize;) { + AnimationEvent event = mEvents.get(i); + if (event.mEvent == AnimationEvent.ANIMATION_END) { + boolean needToSwapStart; + if (event.mNode.mStartTime == event.mNode.mEndTime) { + needToSwapStart = true; + } else if (event.mNode.mEndTime == event.mNode.mStartTime + + event.mNode.mAnimation.getStartDelay()) { + // Swapping start delay + needToSwapStart = false; + } else { + i++; + continue; + } + + int startEventId = eventSize; + int startDelayEndId = eventSize; + for (int j = i + 1; j < eventSize; j++) { + if (startEventId < eventSize && startDelayEndId < eventSize) { + break; + } + if (mEvents.get(j).mNode == event.mNode) { + if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_START) { + // Found start event + startEventId = j; + } else if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) { + startDelayEndId = j; + } + } + + } + if (needToSwapStart && startEventId == mEvents.size()) { + throw new UnsupportedOperationException("Something went wrong, no start is" + + "found after stop for an animation that has the same start and end" + + "time."); + + } + if (startDelayEndId == mEvents.size()) { + throw new UnsupportedOperationException("Something went wrong, no start" + + "delay end is found after stop for an animation"); + + } + + // We need to make sure start is inserted before start delay ended event, + // because otherwise inserting start delay ended events first would change + // the start event index. + if (needToSwapStart) { + AnimationEvent startEvent = mEvents.remove(startEventId); + mEvents.add(i, startEvent); + i++; + } + + AnimationEvent startDelayEndEvent = mEvents.remove(startDelayEndId); + mEvents.add(i, startDelayEndEvent); + i += 2; + } else { + i++; + } + } + + if (!mEvents.isEmpty() && mEvents.get(0).mEvent != AnimationEvent.ANIMATION_START) { + throw new UnsupportedOperationException( + "Sorting went bad, the start event should always be at index 0"); + } + + // Add AnimatorSet's start delay node to the beginning + mEvents.add(0, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_START)); + mEvents.add(1, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_DELAY_ENDED)); + mEvents.add(2, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_END)); + + 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"); + } + } + + /** + * Based on parent's start/end time, calculate children's start/end time. If cycle exists in + * the graph, all the nodes on the cycle will be marked to start at {@link #DURATION_INFINITE}, + * meaning they will ever play. + */ + private void updatePlayTime(Node parent, ArrayList<Node> visited) { + if (parent.mChildNodes == null) { + if (parent == mRootNode) { + // All the animators are in a cycle + for (int i = 0; i < mNodes.size(); i++) { + Node node = mNodes.get(i); + if (node != mRootNode) { + node.mStartTime = DURATION_INFINITE; + node.mEndTime = DURATION_INFINITE; + } + } + } + return; + } + + visited.add(parent); + int childrenSize = parent.mChildNodes.size(); + for (int i = 0; i < childrenSize; i++) { + Node child = parent.mChildNodes.get(i); + child.mTotalDuration = child.mAnimation.getTotalDuration(); // Update cached duration. + + int index = visited.indexOf(child); + if (index >= 0) { + // Child has been visited, cycle found. Mark all the nodes in the cycle. + for (int j = index; j < visited.size(); j++) { + visited.get(j).mLatestParent = null; + visited.get(j).mStartTime = DURATION_INFINITE; + visited.get(j).mEndTime = DURATION_INFINITE; + } + child.mStartTime = DURATION_INFINITE; + child.mEndTime = DURATION_INFINITE; + child.mLatestParent = null; + Log.w(TAG, "Cycle found in AnimatorSet: " + this); + continue; + } + + if (child.mStartTime != DURATION_INFINITE) { + if (parent.mEndTime == DURATION_INFINITE) { + child.mLatestParent = parent; + child.mStartTime = DURATION_INFINITE; + child.mEndTime = DURATION_INFINITE; + } else { + if (parent.mEndTime >= child.mStartTime) { + child.mLatestParent = parent; + child.mStartTime = parent.mEndTime; + } + + child.mEndTime = child.mTotalDuration == DURATION_INFINITE + ? DURATION_INFINITE : child.mStartTime + child.mTotalDuration; + } + } + updatePlayTime(child, visited); + } + visited.remove(parent); + } + + // Recursively find all the siblings + private void findSiblings(Node node, ArrayList<Node> siblings) { + if (!siblings.contains(node)) { + siblings.add(node); + if (node.mSiblings == null) { + return; + } + for (int i = 0; i < node.mSiblings.size(); i++) { + findSiblings(node.mSiblings.get(i), siblings); + } + } + } + + /** + * @hide + * TODO: For animatorSet defined in XML, we can use a flag to indicate what the play order + * if defined (i.e. sequential or together), then we can use the flag instead of calculating + * dynamically. Note that when AnimatorSet is empty this method returns true. + * @return whether all the animators in the set are supposed to play together + */ + public boolean shouldPlayTogether() { + updateAnimatorsDuration(); + createDependencyGraph(); + // All the child nodes are set out to play right after the delay animation + return mRootNode.mChildNodes == null || mRootNode.mChildNodes.size() == mNodes.size() - 1; + } + + @Override + public long getTotalDuration() { + updateAnimatorsDuration(); + createDependencyGraph(); + return mTotalDuration; + } + + private Node getNodeForAnimation(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + return node; + } + + /** + * A Node is an embodiment of both the Animator that it wraps as well as + * any dependencies that are associated with that Animation. This includes + * both dependencies upon other nodes (in the dependencies list) as + * well as dependencies of other nodes upon this (in the nodeDependents list). + */ + private static class Node implements Cloneable { + Animator mAnimation; + + /** + * Child nodes are the nodes associated with animations that will be played immediately + * after current node. + */ + ArrayList<Node> mChildNodes = null; + + /** + * Flag indicating whether the animation in this node is finished. This flag + * is used by AnimatorSet to check, as each animation ends, whether all child animations + * are mEnded and it's time to send out an end event for the entire AnimatorSet. + */ + boolean mEnded = false; + + /** + * Nodes with animations that are defined to play simultaneously with the animation + * associated with this current node. + */ + ArrayList<Node> mSiblings; + + /** + * Parent nodes are the nodes with animations preceding current node's animation. Parent + * nodes here are derived from user defined animation sequence. + */ + ArrayList<Node> mParents; + + /** + * Latest parent is the parent node associated with a animation that finishes after all + * the other parents' animations. + */ + Node mLatestParent = null; + + boolean mParentsAdded = false; + long mStartTime = 0; + long mEndTime = 0; + long mTotalDuration = 0; + + /** + * Constructs the Node with the animation that it encapsulates. A Node has no + * dependencies by default; dependencies are added via the addDependency() + * method. + * + * @param animation The animation that the Node encapsulates. + */ + public Node(Animator animation) { + this.mAnimation = animation; + } + + @Override + public Node clone() { + try { + Node node = (Node) super.clone(); + node.mAnimation = mAnimation.clone(); + if (mChildNodes != null) { + node.mChildNodes = new ArrayList<>(mChildNodes); + } + if (mSiblings != null) { + node.mSiblings = new ArrayList<>(mSiblings); + } + if (mParents != null) { + node.mParents = new ArrayList<>(mParents); + } + node.mEnded = false; + return node; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + void addChild(Node node) { + if (mChildNodes == null) { + mChildNodes = new ArrayList<>(); + } + if (!mChildNodes.contains(node)) { + mChildNodes.add(node); + node.addParent(this); + } + } + + public void addSibling(Node node) { + if (mSiblings == null) { + mSiblings = new ArrayList<Node>(); + } + if (!mSiblings.contains(node)) { + mSiblings.add(node); + node.addSibling(this); + } + } + + public void addParent(Node node) { + if (mParents == null) { + mParents = new ArrayList<Node>(); + } + if (!mParents.contains(node)) { + mParents.add(node); + node.addChild(this); + } + } + + public void addParents(ArrayList<Node> parents) { + if (parents == null) { + return; + } + int size = parents.size(); + for (int i = 0; i < size; i++) { + addParent(parents.get(i)); + } + } + } + + /** + * 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 == DURATION_INFINITE + ? DURATION_INFINITE : 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; + mSeekingInReverse = inReverse; + } + } + } + + 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 + * AnimatorSet#play(Animator) play()} method of <code>AnimatorSet</code> is to make it possible + * to express the dependency relationships of animations in a natural way. Developers can also + * use the {@link AnimatorSet#playTogether(Animator[]) playTogether()} and {@link + * AnimatorSet#playSequentially(Animator[]) playSequentially()} methods if these suit the need, + * but it might be easier in some situations to express the AnimatorSet of animations in pairs. + * <p/> + * <p>The <code>Builder</code> object cannot be constructed directly, but is rather constructed + * internally via a call to {@link AnimatorSet#play(Animator)}.</p> + * <p/> + * <p>For example, this sets up a AnimatorSet to play anim1 and anim2 at the same time, anim3 to + * play when anim2 finishes, and anim4 to play when anim3 finishes:</p> + * <pre> + * AnimatorSet s = new AnimatorSet(); + * s.play(anim1).with(anim2); + * s.play(anim2).before(anim3); + * s.play(anim4).after(anim3); + * </pre> + * <p/> + * <p>Note in the example that both {@link Builder#before(Animator)} and {@link + * Builder#after(Animator)} are used. These are just different ways of expressing the same + * relationship and are provided to make it easier to say things in a way that is more natural, + * depending on the situation.</p> + * <p/> + * <p>It is possible to make several calls into the same <code>Builder</code> object to express + * multiple relationships. However, note that it is only the animation passed into the initial + * {@link AnimatorSet#play(Animator)} method that is the dependency in any of the successive + * calls to the <code>Builder</code> object. For example, the following code starts both anim2 + * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and + * anim3: + * <pre> + * AnimatorSet s = new AnimatorSet(); + * s.play(anim1).before(anim2).before(anim3); + * </pre> + * If the desired result is to play anim1 then anim2 then anim3, this code expresses the + * relationship correctly:</p> + * <pre> + * AnimatorSet s = new AnimatorSet(); + * s.play(anim1).before(anim2); + * s.play(anim2).before(anim3); + * </pre> + * <p/> + * <p>Note that it is possible to express relationships that cannot be resolved and will not + * result in sensible results. For example, <code>play(anim1).after(anim1)</code> makes no + * sense. In general, circular dependencies like this one (or more indirect ones where a depends + * on b, which depends on c, which depends on a) should be avoided. Only create AnimatorSets + * that can boil down to a simple, one-way relationship of animations starting with, before, and + * after other, different, animations.</p> + */ + public class Builder { + + /** + * This tracks the current node being processed. It is supplied to the play() method + * of AnimatorSet and passed into the constructor of Builder. + */ + private Node mCurrentNode; + + /** + * package-private constructor. Builders are only constructed by AnimatorSet, when the + * play() method is called. + * + * @param anim The animation that is the dependency for the other animations passed into + * the other methods of this Builder object. + */ + Builder(Animator anim) { + mDependencyDirty = true; + mCurrentNode = getNodeForAnimation(anim); + } + + /** + * Sets up the given animation to play at the same time as the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object. + * + * @param anim The animation that will play when the animation supplied to the + * {@link AnimatorSet#play(Animator)} method starts. + */ + public Builder with(Animator anim) { + Node node = getNodeForAnimation(anim); + mCurrentNode.addSibling(node); + return this; + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object + * ends. + * + * @param anim The animation that will play when the animation supplied to the + * {@link AnimatorSet#play(Animator)} method ends. + */ + public Builder before(Animator anim) { + Node node = getNodeForAnimation(anim); + mCurrentNode.addChild(node); + return this; + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object + * to start when the animation supplied in this method call ends. + * + * @param anim The animation whose end will cause the animation supplied to the + * {@link AnimatorSet#play(Animator)} method to play. + */ + public Builder after(Animator anim) { + Node node = getNodeForAnimation(anim); + mCurrentNode.addParent(node); + return this; + } + + /** + * Sets up the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this <code>Builder</code> object + * to play when the given amount of time elapses. + * + * @param delay The number of milliseconds that should elapse before the + * animation starts. + */ + public Builder after(long delay) { + // setup dummy ValueAnimator just to run the clock + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(delay); + after(anim); + return this; + } + + } + +} |