) 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.
* @return Whether this AnimatorSet has been started and has not yet ended.
*/
@Override
public boolean isRunning() {
for (Node node : mNodes) {
if (node.animation.isRunning()) {
return true;
}
}
return false;
}
@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.
* @param startDelay The amount of the delay, in milliseconds
*/
@Override
public void setStartDelay(long startDelay) {
if (mStartDelay > 0) {
mReversible = false;
}
mStartDelay = startDelay;
}
/**
* 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");
}
// Just record the value for now - it will be used later when the AnimatorSet starts
mDuration = duration;
return this;
}
@Override
public void setupStartValues() {
for (Node node : mNodes) {
node.animation.setupStartValues();
}
}
@Override
public void setupEndValues() {
for (Node node : mNodes) {
node.animation.setupEndValues();
}
}
@Override
public void pause() {
boolean previouslyPaused = mPaused;
super.pause();
if (!previouslyPaused && mPaused) {
if (mDelayAnim != null) {
mDelayAnim.pause();
} else {
for (Node node : mNodes) {
node.animation.pause();
}
}
}
}
@Override
public void resume() {
boolean previouslyPaused = mPaused;
super.resume();
if (previouslyPaused && !mPaused) {
if (mDelayAnim != null) {
mDelayAnim.resume();
} else {
for (Node node : mNodes) {
node.animation.resume();
}
}
}
}
/**
* {@inheritDoc}
*
* Starting this AnimatorSet
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() {
mTerminated = false;
mStarted = true;
mPaused = false;
for (Node node : mNodes) {
node.animation.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 oldListeners = node.animation.getListeners();
if (oldListeners != null && oldListeners.size() > 0) {
final ArrayList clonedListeners = new
ArrayList(oldListeners);
for (AnimatorListener listener : clonedListeners) {
if (listener instanceof DependencyListener ||
listener instanceof AnimatorSetListener) {
node.animation.removeListener(listener);
}
}
}
}
// 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 nodesToStart = new ArrayList();
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) node.dependencies.clone();
}
node.animation.addListener(mSetListener);
}
// 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();
}
if (mListeners != null) {
ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationStart(this);
}
}
if (mNodes.size() == 0 && mStartDelay == 0) {
// Handle unusual case where empty AnimatorSet is started - should send out
// end event immediately since the event will not be sent out at all otherwise
mStarted = false;
if (mListeners != null) {
ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationEnd(this);
}
}
}
}
@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.
*/
anim.mNeedsSort = true;
anim.mTerminated = false;
anim.mStarted = false;
anim.mPlayingSet = new ArrayList();
anim.mNodeMap = new HashMap();
anim.mNodes = new ArrayList();
anim.mSortedNodes = new ArrayList();
anim.mReversible = mReversible;
anim.mSetListener = null;
// Walk through the old nodes list, cloning each node and adding it to the new nodemap.
// One problem is that the old node dependencies point to nodes in the old AnimatorSet.
// We need to track the old/new nodes in order to reconstruct the dependencies in the clone.
HashMap nodeCloneMap = new HashMap(); //
for (Node node : mNodes) {
Node nodeClone = node.clone();
nodeCloneMap.put(node, 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;
// clear out any listeners that were set up by the AnimatorSet; these will
// be set up when the clone's nodes are sorted
ArrayList cloneListeners = nodeClone.animation.getListeners();
if (cloneListeners != null) {
ArrayList listenersToRemove = null;
for (AnimatorListener listener : cloneListeners) {
if (listener instanceof AnimatorSetListener) {
if (listenersToRemove == null) {
listenersToRemove = new ArrayList();
}
listenersToRemove.add(listener);
}
}
if (listenersToRemove != null) {
for (AnimatorListener listener : listenersToRemove) {
cloneListeners.remove(listener);
}
}
}
}
// 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 (Node node : mNodes) {
Node nodeClone = nodeCloneMap.get(node);
if (node.dependencies != null) {
for (Dependency dependency : node.dependencies) {
Node clonedDependencyNode = nodeCloneMap.get(dependency.node);
Dependency cloneDependency = new Dependency(clonedDependencyNode,
dependency.rule);
nodeClone.addDependency(cloneDependency);
}
}
}
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 {
private AnimatorSet mAnimatorSet;
AnimatorSetListener(AnimatorSet animatorSet) {
mAnimatorSet = animatorSet;
}
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();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationCancel(mAnimatorSet);
}
}
}
}
}
@SuppressWarnings("unchecked")
public void onAnimationEnd(Animator animation) {
animation.removeListener(this);
mPlayingSet.remove(animation);
Node animNode = mAnimatorSet.mNodeMap.get(animation);
animNode.done = true;
if (!mTerminated) {
// Listeners are already notified of the AnimatorSet ending in cancel() or
// end(); the logic below only kicks in when animations end normally
ArrayList sortedNodes = mAnimatorSet.mSortedNodes;
boolean allDone = true;
int numSortedNodes = sortedNodes.size();
for (int i = 0; i < numSortedNodes; ++i) {
if (!sortedNodes.get(i).done) {
allDone = false;
break;
}
}
if (allDone) {
// If this was the last child animation to end, then notify listeners that this
// AnimatorSet has ended
if (mListeners != null) {
ArrayList tmpListeners =
(ArrayList) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationEnd(mAnimatorSet);
}
}
mAnimatorSet.mStarted = false;
mAnimatorSet.mPaused = false;
}
}
}
// Nothing to do
public void onAnimationRepeat(Animator animation) {
}
// Nothing to do
public void onAnimationStart(Animator animation) {
}
}
/**
* 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 roots = new ArrayList();
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 tmpRoots = new ArrayList();
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();
}
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
public boolean canReverse() {
if (!mReversible) {
return false;
}
// Loop to make sure all the Nodes can reverse.
for (Node node : mNodes) {
if (!node.animation.canReverse() || node.animation.getStartDelay() > 0) {
return false;
}
}
return true;
}
/**
* @hide
*/
@Override
public void reverse() {
if (canReverse()) {
for (Node node : mNodes) {
node.animation.reverse();
}
}
}
/**
* 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;
}
}
/**
* 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 {
public Animator animation;
/**
* 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.
*/
public ArrayList 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 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 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 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;
/**
* 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.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();
nodeDependencies = new ArrayList();
}
dependencies.add(dependency);
if (!nodeDependencies.contains(dependency.node)) {
nodeDependencies.add(dependency.node);
}
Node dependencyNode = dependency.node;
if (dependencyNode.nodeDependents == null) {
dependencyNode.nodeDependents = new ArrayList();
}
dependencyNode.nodeDependents.add(this);
}
@Override
public Node clone() {
try {
Node node = (Node) super.clone();
node.animation = (Animator) animation.clone();
return node;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
/**
* The Builder
object is a utility class to facilitate adding animations to a
* AnimatorSet
along with the relationships between the various animations. The
* intention of the Builder
methods, along with the {@link
* AnimatorSet#play(Animator) play()} method of AnimatorSet
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.
*
* The Builder
object cannot be constructed directly, but is rather constructed
* internally via a call to {@link AnimatorSet#play(Animator)}.
*
* 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:
*
* AnimatorSet s = new AnimatorSet();
* s.play(anim1).with(anim2);
* s.play(anim2).before(anim3);
* s.play(anim4).after(anim3);
*
*
* 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.
*
* It is possible to make several calls into the same Builder
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 Builder
object. For example, the following code starts both anim2
* and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and
* anim3:
*
* AnimatorSet s = new AnimatorSet();
* s.play(anim1).before(anim2).before(anim3);
*
* If the desired result is to play anim1 then anim2 then anim3, this code expresses the
* relationship correctly:
*
* AnimatorSet s = new AnimatorSet();
* s.play(anim1).before(anim2);
* s.play(anim2).before(anim3);
*
*
* Note that it is possible to express relationships that cannot be resolved and will not
* result in sensible results. For example, play(anim1).after(anim1)
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.
*/
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) {
mCurrentNode = mNodeMap.get(anim);
if (mCurrentNode == null) {
mCurrentNode = new Node(anim);
mNodeMap.put(anim, mCurrentNode);
mNodes.add(mCurrentNode);
}
}
/**
* 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 Builder
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 = 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);
return this;
}
/**
* Sets up the given animation to play when the animation supplied in the
* {@link AnimatorSet#play(Animator)} call that created this Builder
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) {
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);
return this;
}
/**
* Sets up the given animation to play when the animation supplied in the
* {@link AnimatorSet#play(Animator)} call that created this Builder
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) {
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);
return this;
}
/**
* Sets up the animation supplied in the
* {@link AnimatorSet#play(Animator)} call that created this Builder
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;
}
}
}