mirror of
https://github.com/classilla/tenfourfox.git
synced 2025-02-08 01:31:00 +00:00
1114 lines
45 KiB
Java
Executable File
1114 lines
45 KiB
Java
Executable File
/*
|
|
* 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 com.nineoldandroids.animation;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
|
|
import android.view.animation.Interpolator;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public final class AnimatorSet extends Animator {
|
|
|
|
/**
|
|
* 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<Animator> mPlayingSet = new ArrayList<Animator>();
|
|
|
|
/**
|
|
* 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 HashMap<Animator, Node> mNodeMap = new HashMap<Animator, Node>();
|
|
|
|
/**
|
|
* 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>();
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* Flag indicating that the AnimatorSet has been manually
|
|
* terminated (by calling cancel() or end()).
|
|
* This flag is used to avoid starting other animations when currently-playing
|
|
* child animations of this AnimatorSet end. It also determines whether cancel/end
|
|
* notifications are sent out via the normal AnimatorSetListener mechanism.
|
|
*/
|
|
boolean mTerminated = 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 = null;
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
/**
|
|
* 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(Animator... items) {
|
|
if (items != null) {
|
|
mNeedsSort = true;
|
|
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) {
|
|
mNeedsSort = true;
|
|
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) {
|
|
mNeedsSort = true;
|
|
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) {
|
|
mNeedsSort = true;
|
|
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>();
|
|
for (Node node : mNodes) {
|
|
childList.add(node.animation);
|
|
}
|
|
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) {
|
|
for (Node node : mNodes) {
|
|
Animator animation = node.animation;
|
|
if (animation instanceof AnimatorSet) {
|
|
((AnimatorSet)animation).setTarget(target);
|
|
} else if (animation instanceof ObjectAnimator) {
|
|
((ObjectAnimator)animation).setTarget(target);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations}
|
|
* of this AnimatorSet.
|
|
*
|
|
* @param interpolator the interpolator to be used by each child animation of this AnimatorSet
|
|
*/
|
|
@Override
|
|
public void setInterpolator(/*Time*/Interpolator interpolator) {
|
|
for (Node node : mNodes) {
|
|
node.animation.setInterpolator(interpolator);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
mNeedsSort = true;
|
|
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() {
|
|
mTerminated = true;
|
|
if (isStarted()) {
|
|
ArrayList<AnimatorListener> tmpListeners = null;
|
|
if (mListeners != null) {
|
|
tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone();
|
|
for (AnimatorListener listener : tmpListeners) {
|
|
listener.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();
|
|
}
|
|
}
|
|
if (tmpListeners != null) {
|
|
for (AnimatorListener listener : tmpListeners) {
|
|
listener.onAnimationEnd(this);
|
|
}
|
|
}
|
|
mStarted = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@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() {
|
|
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);
|
|
}
|
|
node.animation.addListener(mSetListener);
|
|
}
|
|
}
|
|
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.
|
|
* @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) {
|
|
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");
|
|
}
|
|
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(duration);
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@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() {
|
|
mTerminated = false;
|
|
mStarted = true;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
// 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.start();
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
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<AnimatorListener> tmpListeners =
|
|
(ArrayList<AnimatorListener>) 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<Animator>();
|
|
anim.mNodeMap = new HashMap<Animator, Node>();
|
|
anim.mNodes = new ArrayList<Node>();
|
|
anim.mSortedNodes = new ArrayList<Node>();
|
|
|
|
// Walk through the old nodes list, cloning each node and adding it to the new nodemap.
|
|
// One problem is that the old node dependencies point to nodes in the old AnimatorSet.
|
|
// We need to track the old/new nodes in order to reconstruct the dependencies in the clone.
|
|
HashMap<Node, Node> nodeCloneMap = new HashMap<Node, Node>(); // <old, new>
|
|
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<AnimatorListener> cloneListeners = nodeClone.animation.getListeners();
|
|
if (cloneListeners != null) {
|
|
ArrayList<AnimatorListener> listenersToRemove = null;
|
|
for (AnimatorListener listener : cloneListeners) {
|
|
if (listener instanceof AnimatorSetListener) {
|
|
if (listenersToRemove == null) {
|
|
listenersToRemove = new ArrayList<AnimatorListener>();
|
|
}
|
|
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<Node> 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<AnimatorListener> tmpListeners =
|
|
(ArrayList<AnimatorListener>) mListeners.clone();
|
|
int numListeners = tmpListeners.size();
|
|
for (int i = 0; i < numListeners; ++i) {
|
|
tmpListeners.get(i).onAnimationEnd(mAnimatorSet);
|
|
}
|
|
}
|
|
mAnimatorSet.mStarted = 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<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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<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;
|
|
|
|
/**
|
|
* 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<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);
|
|
}
|
|
|
|
@Override
|
|
public Node clone() {
|
|
try {
|
|
Node node = (Node) super.clone();
|
|
node.animation = (Animator) animation.clone();
|
|
return node;
|
|
} catch (CloneNotSupportedException e) {
|
|
throw new AssertionError();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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 <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 = 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 <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 = 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 <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 = 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 <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;
|
|
}
|
|
|
|
}
|
|
|
|
}
|