Java tutorial
/* 50AH-code ========= 50 Android Hacks (http://manning.com/sessa/) book source code Copyright (c) 2012 Manning Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * 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.actionbarsherlock.internal.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. */ @SuppressWarnings("unchecked") 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> */ @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. */ @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); } } } } } 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 = 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; } } }