Java tutorial
/* * 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 || event.getTime() == DURATION_INFINITE) { 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); // TODO: need a function that accounts for infinite duration to compare time if (event.getTime() != DURATION_INFINITE && 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; } } }