Refactor AnimatorSet in prep for adding more functionalities
This refactor changes how relationships (i.e. with/after/before)
among Animators in an AnimatorSet are represented. Specifically,
a directed graph is used: parent-child indicates sequential
relationship, and siblings will fire simultaneously.
This CL also fixed the issue where start delays for Animators that
play together are not handled correctly.
Bug: 11649073
Bug: 18069038
Change-Id: I5dad4889fbe81440e66bf98601e8a247d3aedb2e
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index aa1be9a..d331c2a 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -27,6 +27,11 @@
public abstract class Animator implements Cloneable {
/**
+ * The value used to indicate infinite duration (e.g. when Animators repeat infinitely).
+ * @hide
+ */
+ public static final long DURATION_INFINITE = -1;
+ /**
* The set of listeners to be sent events through the life of an animation.
*/
ArrayList<AnimatorListener> mListeners = null;
@@ -184,6 +189,16 @@
public abstract long getDuration();
/**
+ * Gets the total duration of the animation, accounting for animation sequences, start delay,
+ * and repeating. Return {@link #DURATION_INFINITE} if the duration is infinite.
+ * @hide
+ * TODO: Unhide
+ */
+ public long getTotalDuration() {
+ return getStartDelay() + getDuration();
+ }
+
+ /**
* The time interpolator used in calculating the elapsed fraction of the
* animation. The interpolator determines whether the animation runs with
* linear or non-linear motion, such as acceleration and deceleration. The
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 951fe49..58e370c 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -17,6 +17,7 @@
package android.animation;
import android.util.ArrayMap;
+import android.util.Log;
import java.util.ArrayList;
import java.util.Collection;
@@ -50,6 +51,7 @@
*/
public final class AnimatorSet extends Animator {
+ private static final String TAG = "AnimatorSet";
/**
* Internal variables
* NOTE: This object implements the clone() method, making a deep copy of any referenced
@@ -79,20 +81,10 @@
private ArrayList<Node> mNodes = new ArrayList<Node>();
/**
- * The sorted list of nodes. This is the order in which the animations will
- * be played. The details about when exactly they will be played depend
- * on the dependency relationships of the nodes.
+ * Animator Listener that tracks the lifecycle of each Animator in the set. It will be added
+ * to each Animator before they start and removed after they end.
*/
- private ArrayList<Node> mSortedNodes = new ArrayList<Node>();
-
- /**
- * Flag indicating whether the nodes should be sorted prior to playing. This
- * flag allows us to cache the previous sorted nodes so that if the sequence
- * is replayed with no changes, it does not have to re-sort the nodes again.
- */
- private boolean mNeedsSort = true;
-
- private AnimatorSetListener mSetListener = null;
+ private AnimatorSetListener mSetListener = new AnimatorSetListener(this);
/**
* Flag indicating that the AnimatorSet has been manually
@@ -101,7 +93,13 @@
* child animations of this AnimatorSet end. It also determines whether cancel/end
* notifications are sent out via the normal AnimatorSetListener mechanism.
*/
- boolean mTerminated = false;
+ private boolean mTerminated = false;
+
+ /**
+ * Tracks whether any change has been made to the AnimatorSet, which is then used to
+ * determine whether the dependency graph should be re-constructed.
+ */
+ private boolean mDependencyDirty = false;
/**
* Indicates whether an AnimatorSet has been start()'d, whether or
@@ -113,8 +111,13 @@
private long mStartDelay = 0;
// Animator used for a nonzero startDelay
- private ValueAnimator mDelayAnim = null;
+ 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
@@ -125,7 +128,17 @@
// was set on this AnimatorSet, so it should not be passed down to the children.
private TimeInterpolator mInterpolator = null;
+ // Whether the AnimatorSet can be reversed.
private boolean mReversible = true;
+ // The total duration of finishing all the Animators in the set.
+ private long mTotalDuration = 0;
+
+ public AnimatorSet() {
+ super();
+ mNodeMap.put(mDelayAnim, mRootNode);
+ mNodes.add(mRootNode);
+ }
+
/**
* 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
@@ -139,7 +152,6 @@
*/
public void playTogether(Animator... items) {
if (items != null) {
- mNeedsSort = true;
Builder builder = play(items[0]);
for (int i = 1; i < items.length; ++i) {
builder.with(items[i]);
@@ -154,7 +166,6 @@
*/
public void playTogether(Collection<Animator> items) {
if (items != null && items.size() > 0) {
- mNeedsSort = true;
Builder builder = null;
for (Animator anim : items) {
if (builder == null) {
@@ -174,13 +185,12 @@
*/
public void playSequentially(Animator... items) {
if (items != null) {
- mNeedsSort = true;
if (items.length == 1) {
play(items[0]);
} else {
mReversible = false;
for (int i = 0; i < items.length - 1; ++i) {
- play(items[i]).before(items[i+1]);
+ play(items[i]).before(items[i + 1]);
}
}
}
@@ -194,13 +204,12 @@
*/
public void playSequentially(List<Animator> items) {
if (items != null && items.size() > 0) {
- mNeedsSort = true;
if (items.size() == 1) {
play(items.get(0));
} else {
mReversible = false;
for (int i = 0; i < items.size() - 1; ++i) {
- play(items.get(i)).before(items.get(i+1));
+ play(items.get(i)).before(items.get(i + 1));
}
}
}
@@ -216,8 +225,10 @@
*/
public ArrayList<Animator> getChildAnimations() {
ArrayList<Animator> childList = new ArrayList<Animator>();
- for (Node node : mNodes) {
- childList.add(node.animation);
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ childList.add(node.mAnimation);
}
return childList;
}
@@ -231,8 +242,10 @@
*/
@Override
public void setTarget(Object target) {
- for (Node node : mNodes) {
- Animator animation = node.animation;
+ 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) {
@@ -249,7 +262,7 @@
int conf = super.getChangingConfigurations();
final int nodeCount = mNodes.size();
for (int i = 0; i < nodeCount; i ++) {
- conf |= mNodes.get(i).animation.getChangingConfigurations();
+ conf |= mNodes.get(i).mAnimation.getChangingConfigurations();
}
return conf;
}
@@ -303,7 +316,6 @@
*/
public Builder play(Animator anim) {
if (anim != null) {
- mNeedsSort = true;
return new Builder(anim);
}
return null;
@@ -323,22 +335,20 @@
ArrayList<AnimatorListener> tmpListeners = null;
if (mListeners != null) {
tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone();
- for (AnimatorListener listener : tmpListeners) {
- listener.onAnimationCancel(this);
+ int size = tmpListeners.size();
+ for (int i = 0; i < size; i++) {
+ tmpListeners.get(i).onAnimationCancel(this);
}
}
- if (mDelayAnim != null && mDelayAnim.isRunning()) {
- // If we're currently in the startDelay period, just cancel that animator and
- // send out the end event to all listeners
- mDelayAnim.cancel();
- } else if (mSortedNodes.size() > 0) {
- for (Node node : mSortedNodes) {
- node.animation.cancel();
- }
+ ArrayList<Animator> playingSet = new ArrayList<>(mPlayingSet);
+ int setSize = playingSet.size();
+ for (int i = 0; i < setSize; i++) {
+ playingSet.get(i).cancel();
}
if (tmpListeners != null) {
- for (AnimatorListener listener : tmpListeners) {
- listener.onAnimationEnd(this);
+ int size = tmpListeners.size();
+ for (int i = 0; i < size; i++) {
+ tmpListeners.get(i).onAnimationEnd(this);
}
}
mStarted = false;
@@ -355,35 +365,45 @@
public void end() {
mTerminated = true;
if (isStarted()) {
- if (mSortedNodes.size() != mNodes.size()) {
- // hasn't been started yet - sort the nodes now, then end them
- sortNodes();
- for (Node node : mSortedNodes) {
- if (mSetListener == null) {
- mSetListener = new AnimatorSetListener(this);
+ endRemainingAnimations();
+ }
+ if (mListeners != null) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ for (int i = 0; i < tmpListeners.size(); i++) {
+ tmpListeners.get(i).onAnimationEnd(this);
+ }
+ }
+ mStarted = false;
+ }
+
+ /**
+ * Iterate the animations that haven't finished or haven't started, and end them.
+ */
+ private void endRemainingAnimations() {
+ ArrayList<Animator> remainingList = new ArrayList<Animator>(mNodes.size());
+ remainingList.addAll(mPlayingSet);
+
+ int index = 0;
+ while (index < remainingList.size()) {
+ Animator anim = remainingList.get(index);
+ anim.end();
+ index++;
+ Node node = mNodeMap.get(anim);
+ if (node.mChildNodes != null) {
+ int childSize = node.mChildNodes.size();
+ for (int i = 0; i < childSize; i++) {
+ Node child = node.mChildNodes.get(i);
+ if (child.mLatestParent != node) {
+ continue;
}
- node.animation.addListener(mSetListener);
+ remainingList.add(child.mAnimation);
}
}
- if (mDelayAnim != null) {
- mDelayAnim.cancel();
- }
- if (mSortedNodes.size() > 0) {
- for (Node node : mSortedNodes) {
- node.animation.end();
- }
- }
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- for (AnimatorListener listener : tmpListeners) {
- listener.onAnimationEnd(this);
- }
- }
- mStarted = false;
}
}
+
/**
* Returns true if any of the child animations of this AnimatorSet have been started and have
* not yet ended.
@@ -391,8 +411,9 @@
*/
@Override
public boolean isRunning() {
- for (Node node : mNodes) {
- if (node.animation.isRunning()) {
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ if (mNodes.get(i).mAnimation.isRunning()) {
return true;
}
}
@@ -426,7 +447,24 @@
if (mStartDelay > 0) {
mReversible = false;
}
+ long delta = startDelay - mStartDelay;
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;
+
+ }
+ }
+ }
}
/**
@@ -455,6 +493,7 @@
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;
@@ -462,15 +501,19 @@
@Override
public void setupStartValues() {
- for (Node node : mNodes) {
- node.animation.setupStartValues();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setupStartValues();
}
}
@Override
public void setupEndValues() {
- for (Node node : mNodes) {
- node.animation.setupEndValues();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setupEndValues();
}
}
@@ -482,8 +525,10 @@
if (mDelayAnim != null) {
mDelayAnim.pause();
} else {
- for (Node node : mNodes) {
- node.animation.pause();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.pause();
}
}
}
@@ -497,8 +542,10 @@
if (mDelayAnim != null) {
mDelayAnim.resume();
} else {
- for (Node node : mNodes) {
- node.animation.resume();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.resume();
}
}
}
@@ -518,96 +565,25 @@
mStarted = true;
mPaused = false;
- for (Node node : mNodes) {
- node.animation.setAllowRunningAsynchronously(false);
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mEnded = false;
+ node.mAnimation.setAllowRunningAsynchronously(false);
}
- if (mDuration >= 0) {
- // If the duration was set on this AnimatorSet, pass it along to all child animations
- for (Node node : mNodes) {
- // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to
- // insert "play-after" delays
- node.animation.setDuration(mDuration);
- }
- }
if (mInterpolator != null) {
- for (Node node : mNodes) {
- node.animation.setInterpolator(mInterpolator);
- }
- }
- // First, sort the nodes (if necessary). This will ensure that sortedNodes
- // contains the animation nodes in the correct order.
- sortNodes();
-
- int numSortedNodes = mSortedNodes.size();
- for (int i = 0; i < numSortedNodes; ++i) {
- Node node = mSortedNodes.get(i);
- // First, clear out the old listeners
- ArrayList<AnimatorListener> oldListeners = node.animation.getListeners();
- if (oldListeners != null && oldListeners.size() > 0) {
- final ArrayList<AnimatorListener> clonedListeners = new
- ArrayList<AnimatorListener>(oldListeners);
-
- for (AnimatorListener listener : clonedListeners) {
- if (listener instanceof DependencyListener ||
- listener instanceof AnimatorSetListener) {
- node.animation.removeListener(listener);
- }
- }
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.setInterpolator(mInterpolator);
}
}
- // nodesToStart holds the list of nodes to be started immediately. We don't want to
- // start the animations in the loop directly because we first need to set up
- // dependencies on all of the nodes. For example, we don't want to start an animation
- // when some other animation also wants to start when the first animation begins.
- final ArrayList<Node> nodesToStart = new ArrayList<Node>();
- for (int i = 0; i < numSortedNodes; ++i) {
- Node node = mSortedNodes.get(i);
- if (mSetListener == null) {
- mSetListener = new AnimatorSetListener(this);
- }
- if (node.dependencies == null || node.dependencies.size() == 0) {
- nodesToStart.add(node);
- } else {
- int numDependencies = node.dependencies.size();
- for (int j = 0; j < numDependencies; ++j) {
- Dependency dependency = node.dependencies.get(j);
- dependency.node.animation.addListener(
- new DependencyListener(this, node, dependency.rule));
- }
- node.tmpDependencies = (ArrayList<Dependency>) node.dependencies.clone();
- }
- node.animation.addListener(mSetListener);
- }
+ updateAnimatorsDuration();
+ createDependencyGraph();
+
// Now that all dependencies are set up, start the animations that should be started.
- if (mStartDelay <= 0) {
- for (Node node : nodesToStart) {
- node.animation.start();
- mPlayingSet.add(node.animation);
- }
- } else {
- mDelayAnim = ValueAnimator.ofFloat(0f, 1f);
- mDelayAnim.setDuration(mStartDelay);
- mDelayAnim.addListener(new AnimatorListenerAdapter() {
- boolean canceled = false;
- public void onAnimationCancel(Animator anim) {
- canceled = true;
- }
- public void onAnimationEnd(Animator anim) {
- if (!canceled) {
- int numNodes = nodesToStart.size();
- for (int i = 0; i < numNodes; ++i) {
- Node node = nodesToStart.get(i);
- node.animation.start();
- mPlayingSet.add(node.animation);
- }
- }
- mDelayAnim = null;
- }
- });
- mDelayAnim.start();
- }
+ start(mRootNode);
if (mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
@@ -631,6 +607,27 @@
}
}
+ 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);
+ }
+
+ void start(final Node node) {
+ final Animator anim = node.mAnimation;
+ mPlayingSet.add(anim);
+ anim.addListener(mSetListener);
+ anim.start();
+ }
+
@Override
public AnimatorSet clone() {
final AnimatorSet anim = (AnimatorSet) super.clone();
@@ -643,15 +640,13 @@
* and will populate any appropriate lists, when it is started.
*/
final int nodeCount = mNodes.size();
- anim.mNeedsSort = true;
anim.mTerminated = false;
anim.mStarted = false;
anim.mPlayingSet = new ArrayList<Animator>();
anim.mNodeMap = new ArrayMap<Animator, Node>();
anim.mNodes = new ArrayList<Node>(nodeCount);
- anim.mSortedNodes = new ArrayList<Node>(nodeCount);
anim.mReversible = mReversible;
- anim.mSetListener = null;
+ anim.mSetListener = new AnimatorSetListener(anim);
// 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.
@@ -662,16 +657,10 @@
Node nodeClone = node.clone();
node.mTmpClone = nodeClone;
anim.mNodes.add(nodeClone);
- anim.mNodeMap.put(nodeClone.animation, nodeClone);
- // Clear out the dependencies in the clone; we'll set these up manually later
- nodeClone.dependencies = null;
- nodeClone.tmpDependencies = null;
- nodeClone.nodeDependents = null;
- nodeClone.nodeDependencies = null;
+ anim.mNodeMap.put(nodeClone.mAnimation, nodeClone);
- // clear out any listeners that were set up by the AnimatorSet; these will
- // be set up when the clone's nodes are sorted
- final ArrayList<AnimatorListener> cloneListeners = nodeClone.animation.getListeners();
+ // clear out any listeners that were set up by the AnimatorSet
+ final ArrayList<AnimatorListener> cloneListeners = nodeClone.mAnimation.getListeners();
if (cloneListeners != null) {
for (int i = cloneListeners.size() - 1; i >= 0; i--) {
final AnimatorListener listener = cloneListeners.get(i);
@@ -681,127 +670,37 @@
}
}
}
+
+ 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 n = 0; n < nodeCount; n++) {
- final Node node = mNodes.get(n);
- final Node clone = node.mTmpClone;
- if (node.dependencies != null) {
- clone.dependencies = new ArrayList<Dependency>(node.dependencies.size());
- final int depSize = node.dependencies.size();
- for (int i = 0; i < depSize; i ++) {
- final Dependency dependency = node.dependencies.get(i);
- Dependency cloneDependency = new Dependency(dependency.node.mTmpClone,
- dependency.rule);
- clone.dependencies.add(cloneDependency);
- }
+ 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);
}
- if (node.nodeDependents != null) {
- clone.nodeDependents = new ArrayList<Node>(node.nodeDependents.size());
- for (Node dep : node.nodeDependents) {
- clone.nodeDependents.add(dep.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);
}
- if (node.nodeDependencies != null) {
- clone.nodeDependencies = new ArrayList<Node>(node.nodeDependencies.size());
- for (Node dep : node.nodeDependencies) {
- clone.nodeDependencies.add(dep.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;
}
- /**
- * This class is the mechanism by which animations are started based on events in other
- * animations. If an animation has multiple dependencies on other animations, then
- * all dependencies must be satisfied before the animation is started.
- */
- private static class DependencyListener implements AnimatorListener {
-
- private AnimatorSet mAnimatorSet;
-
- // The node upon which the dependency is based.
- private Node mNode;
-
- // The Dependency rule (WITH or AFTER) that the listener should wait for on
- // the node
- private int mRule;
-
- public DependencyListener(AnimatorSet animatorSet, Node node, int rule) {
- this.mAnimatorSet = animatorSet;
- this.mNode = node;
- this.mRule = rule;
- }
-
- /**
- * Ignore cancel events for now. We may want to handle this eventually,
- * to prevent follow-on animations from running when some dependency
- * animation is canceled.
- */
- public void onAnimationCancel(Animator animation) {
- }
-
- /**
- * An end event is received - see if this is an event we are listening for
- */
- public void onAnimationEnd(Animator animation) {
- if (mRule == Dependency.AFTER) {
- startIfReady(animation);
- }
- }
-
- /**
- * Ignore repeat events for now
- */
- public void onAnimationRepeat(Animator animation) {
- }
-
- /**
- * A start event is received - see if this is an event we are listening for
- */
- public void onAnimationStart(Animator animation) {
- if (mRule == Dependency.WITH) {
- startIfReady(animation);
- }
- }
-
- /**
- * Check whether the event received is one that the node was waiting for.
- * If so, mark it as complete and see whether it's time to start
- * the animation.
- * @param dependencyAnimation the animation that sent the event.
- */
- private void startIfReady(Animator dependencyAnimation) {
- if (mAnimatorSet.mTerminated) {
- // if the parent AnimatorSet was canceled, then don't start any dependent anims
- return;
- }
- Dependency dependencyToRemove = null;
- int numDependencies = mNode.tmpDependencies.size();
- for (int i = 0; i < numDependencies; ++i) {
- Dependency dependency = mNode.tmpDependencies.get(i);
- if (dependency.rule == mRule &&
- dependency.node.animation == dependencyAnimation) {
- // rule fired - remove the dependency and listener and check to
- // see whether it's time to start the animation
- dependencyToRemove = dependency;
- dependencyAnimation.removeListener(this);
- break;
- }
- }
- mNode.tmpDependencies.remove(dependencyToRemove);
- if (mNode.tmpDependencies.size() == 0) {
- // all dependencies satisfied: start the animation
- mNode.animation.start();
- mAnimatorSet.mPlayingSet.add(mNode.animation);
- }
- }
-
- }
private class AnimatorSetListener implements AnimatorListener {
@@ -812,14 +711,16 @@
}
public void onAnimationCancel(Animator animation) {
+
if (!mTerminated) {
// Listeners are already notified of the AnimatorSet canceling in cancel().
// The logic below only kicks in when animations end normally
- if (mPlayingSet.size() == 0) {
- if (mListeners != null) {
- int numListeners = mListeners.size();
+ if (mAnimatorSet.mPlayingSet.size() == 0) {
+ ArrayList<AnimatorListener> listeners = mAnimatorSet.mListeners;
+ if (listeners != null) {
+ int numListeners = listeners.size();
for (int i = 0; i < numListeners; ++i) {
- mListeners.get(i).onAnimationCancel(mAnimatorSet);
+ listeners.get(i).onAnimationCancel(mAnimatorSet);
}
}
}
@@ -829,17 +730,26 @@
@SuppressWarnings("unchecked")
public void onAnimationEnd(Animator animation) {
animation.removeListener(this);
- mPlayingSet.remove(animation);
+ mAnimatorSet.mPlayingSet.remove(animation);
Node animNode = mAnimatorSet.mNodeMap.get(animation);
- animNode.done = true;
+ animNode.mEnded = true;
+
if (!mTerminated) {
+ List<Node> children = animNode.mChildNodes;
+ // Start children animations, if any.
+ int childrenSize = children == null ? 0 : children.size();
+ for (int i = 0; i < childrenSize; i++) {
+ if (children.get(i).mLatestParent == animNode) {
+ mAnimatorSet.start(children.get(i));
+ }
+ }
// Listeners are already notified of the AnimatorSet ending in cancel() or
// end(); the logic below only kicks in when animations end normally
- ArrayList<Node> sortedNodes = mAnimatorSet.mSortedNodes;
boolean allDone = true;
- int numSortedNodes = sortedNodes.size();
- for (int i = 0; i < numSortedNodes; ++i) {
- if (!sortedNodes.get(i).done) {
+ // Traverse the tree and find if there's any unfinished node
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ if (!mNodes.get(i).mEnded) {
allDone = false;
break;
}
@@ -872,79 +782,6 @@
}
/**
- * This method sorts the current set of nodes, if needed. The sort is a simple
- * DependencyGraph sort, which goes like this:
- * - All nodes without dependencies become 'roots'
- * - while roots list is not null
- * - for each root r
- * - add r to sorted list
- * - remove r as a dependency from any other node
- * - any nodes with no dependencies are added to the roots list
- */
- private void sortNodes() {
- if (mNeedsSort) {
- mSortedNodes.clear();
- ArrayList<Node> roots = new ArrayList<Node>();
- int numNodes = mNodes.size();
- for (int i = 0; i < numNodes; ++i) {
- Node node = mNodes.get(i);
- if (node.dependencies == null || node.dependencies.size() == 0) {
- roots.add(node);
- }
- }
- ArrayList<Node> tmpRoots = new ArrayList<Node>();
- while (roots.size() > 0) {
- int numRoots = roots.size();
- for (int i = 0; i < numRoots; ++i) {
- Node root = roots.get(i);
- mSortedNodes.add(root);
- if (root.nodeDependents != null) {
- int numDependents = root.nodeDependents.size();
- for (int j = 0; j < numDependents; ++j) {
- Node node = root.nodeDependents.get(j);
- node.nodeDependencies.remove(root);
- if (node.nodeDependencies.size() == 0) {
- tmpRoots.add(node);
- }
- }
- }
- }
- roots.clear();
- roots.addAll(tmpRoots);
- tmpRoots.clear();
- }
- mNeedsSort = false;
- if (mSortedNodes.size() != mNodes.size()) {
- throw new IllegalStateException("Circular dependencies cannot exist"
- + " in AnimatorSet");
- }
- } else {
- // Doesn't need sorting, but still need to add in the nodeDependencies list
- // because these get removed as the event listeners fire and the dependencies
- // are satisfied
- int numNodes = mNodes.size();
- for (int i = 0; i < numNodes; ++i) {
- Node node = mNodes.get(i);
- if (node.dependencies != null && node.dependencies.size() > 0) {
- int numDependencies = node.dependencies.size();
- for (int j = 0; j < numDependencies; ++j) {
- Dependency dependency = node.dependencies.get(j);
- if (node.nodeDependencies == null) {
- node.nodeDependencies = new ArrayList<Node>();
- }
- if (!node.nodeDependencies.contains(dependency.node)) {
- node.nodeDependencies.add(dependency.node);
- }
- }
- }
- // nodes are 'done' by default; they become un-done when started, and done
- // again when ended
- node.done = false;
- }
- }
- }
-
- /**
* @hide
*/
@Override
@@ -953,8 +790,10 @@
return false;
}
// Loop to make sure all the Nodes can reverse.
- for (Node node : mNodes) {
- if (!node.animation.canReverse() || node.animation.getStartDelay() > 0) {
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (!node.mAnimation.canReverse() || node.mAnimation.getStartDelay() > 0) {
return false;
}
}
@@ -967,8 +806,10 @@
@Override
public void reverse() {
if (canReverse()) {
- for (Node node : mNodes) {
- node.animation.reverse();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ node.mAnimation.reverse();
}
}
}
@@ -976,36 +817,196 @@
@Override
public String toString() {
String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{";
- boolean prevNeedsSort = mNeedsSort;
- sortNodes();
- mNeedsSort = prevNeedsSort;
- for (Node node : mSortedNodes) {
- returnVal += "\n " + node.animation.toString();
+ int size = mNodes.size();
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ returnVal += "\n " + node.mAnimation.toString();
}
return returnVal + "\n}";
}
- /**
- * Dependency holds information about the node that some other node is
- * dependent upon and the nature of that dependency.
- *
- */
- private static class Dependency {
- static final int WITH = 0; // dependent node must start with this dependency node
- static final int AFTER = 1; // dependent node must start when this dependency node finishes
-
- // The node that the other node with this Dependency is dependent upon
- public Node node;
-
- // The nature of the dependency (WITH or AFTER)
- public int rule;
-
- public Dependency(Node node, int rule) {
- this.node = node;
- this.rule = rule;
+ 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) {
+ return;
+ }
+
+ // TODO: In addition to checking the dirty flag, we should also cache the duration for
+ // each animator, so that when the animator's duration is changed, we can detect that and
+ // update the dependency graph.
+
+ 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);
+
+ long maxEndTime = 0;
+ for (int i = 0; i < size; i++) {
+ Node node = mNodes.get(i);
+ if (node.mEndTime == DURATION_INFINITE) {
+ maxEndTime = DURATION_INFINITE;
+ break;
+ } else {
+ maxEndTime = node.mEndTime > maxEndTime ? node.mEndTime : maxEndTime;
+ }
+ }
+ mTotalDuration = maxEndTime;
+ }
+
+ /**
+ * 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) {
+ 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(); i++) {
+ 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
+ */
+ @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
@@ -1013,46 +1014,13 @@
* well as dependencies of other nodes upon this (in the nodeDependents list).
*/
private static class Node implements Cloneable {
- public Animator animation;
+ Animator mAnimation;
/**
- * These are the dependencies that this node's animation has on other
- * nodes. For example, if this node's animation should begin with some
- * other animation ends, then there will be an item in this node's
- * dependencies list for that other animation's node.
+ * Child nodes are the nodes associated with animations that will be played immediately
+ * after current node.
*/
- public ArrayList<Dependency> dependencies = null;
-
- /**
- * tmpDependencies is a runtime detail. We use the dependencies list for sorting.
- * But we also use the list to keep track of when multiple dependencies are satisfied,
- * but removing each dependency as it is satisfied. We do not want to remove
- * the dependency itself from the list, because we need to retain that information
- * if the AnimatorSet is launched in the future. So we create a copy of the dependency
- * list when the AnimatorSet starts and use this tmpDependencies list to track the
- * list of satisfied dependencies.
- */
- public ArrayList<Dependency> tmpDependencies = null;
-
- /**
- * nodeDependencies is just a list of the nodes that this Node is dependent upon.
- * This information is used in sortNodes(), to determine when a node is a root.
- */
- public ArrayList<Node> nodeDependencies = null;
-
- /**
- * nodeDepdendents is the list of nodes that have this node as a dependency. This
- * is a utility field used in sortNodes to facilitate removing this node as a
- * dependency when it is a root node.
- */
- public ArrayList<Node> nodeDependents = 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 done and it's time to send out an end event for the entire AnimatorSet.
- */
- public boolean done = false;
+ ArrayList<Node> mChildNodes = null;
/**
* Temporary field to hold the clone in AnimatorSet#clone. Cleaned after clone is complete
@@ -1060,6 +1028,35 @@
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;
+
+ /**
* Constructs the Node with the animation that it encapsulates. A Node has no
* dependencies by default; dependencies are added via the addDependency()
* method.
@@ -1067,41 +1064,63 @@
* @param animation The animation that the Node encapsulates.
*/
public Node(Animator animation) {
- this.animation = animation;
- }
-
- /**
- * Add a dependency to this Node. The dependency includes information about the
- * node that this node is dependency upon and the nature of the dependency.
- * @param dependency
- */
- public void addDependency(Dependency dependency) {
- if (dependencies == null) {
- dependencies = new ArrayList<Dependency>();
- nodeDependencies = new ArrayList<Node>();
- }
- dependencies.add(dependency);
- if (!nodeDependencies.contains(dependency.node)) {
- nodeDependencies.add(dependency.node);
- }
- Node dependencyNode = dependency.node;
- if (dependencyNode.nodeDependents == null) {
- dependencyNode.nodeDependents = new ArrayList<Node>();
- }
- dependencyNode.nodeDependents.add(this);
+ this.mAnimation = animation;
}
@Override
public Node clone() {
try {
Node node = (Node) super.clone();
- node.animation = animation.clone();
- node.done = false;
+ node.mAnimation = mAnimation.clone();
+ if (mChildNodes != null) {
+ node.mChildNodes = new ArrayList<>(mChildNodes);
+ }
+ 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));
+ }
+ }
}
/**
@@ -1172,12 +1191,8 @@
* the other methods of this Builder object.
*/
Builder(Animator anim) {
- mCurrentNode = mNodeMap.get(anim);
- if (mCurrentNode == null) {
- mCurrentNode = new Node(anim);
- mNodeMap.put(anim, mCurrentNode);
- mNodes.add(mCurrentNode);
- }
+ mDependencyDirty = true;
+ mCurrentNode = getNodeForAnimation(anim);
}
/**
@@ -1188,14 +1203,8 @@
* {@link AnimatorSet#play(Animator)} method starts.
*/
public Builder with(Animator anim) {
- Node node = mNodeMap.get(anim);
- if (node == null) {
- node = new Node(anim);
- mNodeMap.put(anim, node);
- mNodes.add(node);
- }
- Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH);
- node.addDependency(dependency);
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addSibling(node);
return this;
}
@@ -1209,14 +1218,8 @@
*/
public Builder before(Animator anim) {
mReversible = false;
- Node node = mNodeMap.get(anim);
- if (node == null) {
- node = new Node(anim);
- mNodeMap.put(anim, node);
- mNodes.add(node);
- }
- Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER);
- node.addDependency(dependency);
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addChild(node);
return this;
}
@@ -1230,14 +1233,8 @@
*/
public Builder after(Animator anim) {
mReversible = false;
- Node node = mNodeMap.get(anim);
- if (node == null) {
- node = new Node(anim);
- mNodeMap.put(anim, node);
- mNodes.add(node);
- }
- Dependency dependency = new Dependency(node, Dependency.AFTER);
- mCurrentNode.addDependency(dependency);
+ Node node = getNodeForAnimation(anim);
+ mCurrentNode.addParent(node);
return this;
}
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index a455f8b..35a8816 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -575,6 +575,18 @@
}
/**
+ * @hide
+ */
+ @Override
+ public long getTotalDuration() {
+ if (mRepeatCount == INFINITE) {
+ return DURATION_INFINITE;
+ } else {
+ return mUnscaledStartDelay + (mUnscaledDuration * (mRepeatCount + 1));
+ }
+ }
+
+ /**
* 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
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index 379796d..2a3756d 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -336,6 +336,14 @@
return mUnscaledDuration;
}
+ /**
+ * @hide
+ */
+ @Override
+ public long getTotalDuration() {
+ return mUnscaledDuration + mUnscaledStartDelay;
+ }
+
@Override
public boolean isRunning() {
return mState == STATE_DELAYED || mState == STATE_RUNNING;