| /* |
| * 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.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; |
| for (int j = mLastEventId - 1; j >= 0; j--) { |
| AnimationEvent event = mEvents.get(j); |
| Animator anim = event.mNode.mAnimation; |
| 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 { |
| for (int j = mLastEventId + 1; j < mEvents.size(); j++) { |
| AnimationEvent event = mEvents.get(j); |
| Animator anim = event.mNode.mAnimation; |
| 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. |
| */ |
| @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); |
| } |
| } |
| |
| /** |
| * Gets the current position of the animation in time, which is equal to the current |
| * time minus the time that the animation started. An animation that is not yet started will |
| * return a value of zero, unless the animation has has its play time set via |
| * {@link #setCurrentPlayTime(long)}, in which case it will return the time that was set. |
| * |
| * @return The current position in time of the animation. |
| */ |
| 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. |
| |
| for (int n = 0; n < nodeCount; n++) { |
| final Node node = mNodes.get(n); |
| Node nodeClone = node.clone(); |
| node.mTmpClone = nodeClone; |
| anim.mNodes.add(nodeClone); |
| anim.mNodeMap.put(nodeClone.mAnimation, nodeClone); |
| } |
| |
| anim.mRootNode = mRootNode.mTmpClone; |
| 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.mTmpClone.mLatestParent = node.mLatestParent == null ? |
| null : node.mLatestParent.mTmpClone; |
| int size = node.mChildNodes == null ? 0 : node.mChildNodes.size(); |
| for (int j = 0; j < size; j++) { |
| node.mTmpClone.mChildNodes.set(j, node.mChildNodes.get(j).mTmpClone); |
| } |
| size = node.mSiblings == null ? 0 : node.mSiblings.size(); |
| for (int j = 0; j < size; j++) { |
| node.mTmpClone.mSiblings.set(j, node.mSiblings.get(j).mTmpClone); |
| } |
| size = node.mParents == null ? 0 : node.mParents.size(); |
| for (int j = 0; j < size; j++) { |
| node.mTmpClone.mParents.set(j, node.mParents.get(j).mTmpClone); |
| } |
| } |
| |
| for (int n = 0; n < nodeCount; n++) { |
| mNodes.get(n).mTmpClone = null; |
| } |
| 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); |
| 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; |
| } |
| |
| long duration = child.mAnimation.getTotalDuration(); |
| child.mEndTime = duration == DURATION_INFINITE ? |
| DURATION_INFINITE : child.mStartTime + duration; |
| } |
| } |
| 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; |
| |
| /** |
| * Temporary field to hold the clone in AnimatorSet#clone. Cleaned after clone is complete |
| */ |
| private Node mTmpClone = 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; |
| } |
| |
| } |
| |
| } |