/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.animation; import android.app.ActivityThread; import android.app.Application; import android.os.Build; import android.os.Looper; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.Log; import android.view.animation.Animation; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; /** * This class plays a set of {@link Animator} objects in the specified order. Animations * can be set up to play together, in sequence, or after a specified delay. * *
There are two different approaches to adding animations to a AnimatorSet
:
* 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.
It is possible to set up a AnimatorSet
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.
*
*
For more information about animating with {@code AnimatorSet}, read the * Property * Animation developer guide.
*Builder
object, which is used to
* set up playing constraints. This initial play()
method
* tells the Builder
the animation that is the dependency for
* the succeeding commands to the Builder
. For example,
* calling play(a1).with(a2)
sets up the AnimatorSet to play
* a1
and a2
at the same time,
* play(a1).before(a2)
sets up the AnimatorSet to play
* a1
first, followed by a2
, and
* play(a1).after(a2)
sets up the AnimatorSet to play
* a2
first, followed by a1
.
*
* Note that play()
is the only way to tell the
* Builder
the animation upon which the dependency is created,
* so successive calls to the various functions in Builder
* will all refer to the initial parameter supplied in play()
* as the dependency of the other animations. For example, calling
* play(a1).before(a2).before(a3)
will play both a2
* and a3
when a1 ends; it does not set up a dependency between
* a2
and a3
.
Builder
object. A null parameter will result
* in a null Builder
return value.
* @return Builder The object that constructs the AnimatorSet based on the dependencies
* outlined in the calls to play
and the other methods in the
* Builder
Note that canceling a AnimatorSet
also cancels all of the animations that it
* is responsible for.
*/
@SuppressWarnings("unchecked")
@Override
public void cancel() {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
if (isStarted()) {
ArrayListNote that ending a AnimatorSet
also ends all of the animations that it is
* responsible for.
Starting this For ongoing animations, this method returns the current progress of the animation in
* terms of play time. For an animation that has not yet been started: if the animation has been
* seeked to a certain time via {@link #setCurrentPlayTime(long)}, the seeked play time will
* be returned; otherwise, this method will return 0.
*
* @return the current position in time of the animation in milliseconds
*/
public long getCurrentPlayTime() {
if (mSeekState.isActive()) {
return mSeekState.getPlayTime();
}
if (mLastFrameTime == -1) {
// Not yet started or during start delay
return 0;
}
float durationScale = ValueAnimator.getDurationScale();
durationScale = durationScale == 0 ? 1 : durationScale;
if (mReversing) {
return (long) ((mLastFrameTime - mFirstFrame) / durationScale);
} else {
return (long) ((mLastFrameTime - mFirstFrame - mStartDelay) / durationScale);
}
}
private void initChildren() {
if (!isInitialized()) {
mChildrenInitialized = true;
// Forcefully initialize all children based on their end time, so that if the start
// value of a child is dependent on a previous animation, the animation will be
// initialized after the the previous animations have been advanced to the end.
skipToEndValue(false);
}
}
/**
* @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
* base.
* @return
* @hide
*/
@Override
public boolean doAnimationFrame(long frameTime) {
float durationScale = ValueAnimator.getDurationScale();
if (durationScale == 0f) {
// Duration scale is 0, end the animation right away.
forceToEnd();
return true;
}
// After the first frame comes in, we need to wait for start delay to pass before updating
// any animation values.
if (mFirstFrame < 0) {
mFirstFrame = frameTime;
}
// Handle pause/resume
if (mPaused) {
// Note: Child animations don't receive pause events. Since it's never a contract that
// the child animators will be paused when set is paused, this is unlikely to be an
// issue.
mPauseTime = frameTime;
removeAnimationCallback();
return false;
} else if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mFirstFrame += (frameTime - mPauseTime);
mPauseTime = -1;
}
// Continue at seeked position
if (mSeekState.isActive()) {
mSeekState.updateSeekDirection(mReversing);
if (mReversing) {
mFirstFrame = (long) (frameTime - mSeekState.getPlayTime() * durationScale);
} else {
mFirstFrame = (long) (frameTime - (mSeekState.getPlayTime() + mStartDelay)
* durationScale);
}
mSeekState.reset();
}
if (!mReversing && frameTime < mFirstFrame + mStartDelay * durationScale) {
// Still during start delay in a forward playing case.
return false;
}
// From here on, we always use unscaled play time. Note this unscaled playtime includes
// the start delay.
long unscaledPlayTime = (long) ((frameTime - mFirstFrame) / durationScale);
mLastFrameTime = frameTime;
// 1. Pulse the animators that will start or end in this frame
// 2. Pulse the animators that will finish in a later frame
int latestId = findLatestEventIdForTime(unscaledPlayTime);
int startId = mLastEventId;
handleAnimationEvents(startId, latestId, unscaledPlayTime);
mLastEventId = latestId;
// Pump a frame to the on-going animators
for (int i = 0; i < mPlayingSet.size(); i++) {
Node node = mPlayingSet.get(i);
if (!node.mEnded) {
pulseFrame(node, getPlayTimeForNode(unscaledPlayTime, node));
}
}
// Remove all the finished anims
for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
if (mPlayingSet.get(i).mEnded) {
mPlayingSet.remove(i);
}
}
boolean finished = false;
if (mReversing) {
if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) {
// The only animation that is running is the delay animation.
finished = true;
} else if (mPlayingSet.isEmpty() && mLastEventId < 3) {
// The only remaining animation is the delay animation
finished = true;
}
} else {
finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1;
}
if (finished) {
endAnimation();
return true;
}
return false;
}
/**
* @hide
*/
@Override
public void commitAnimationFrame(long frameTime) {
// No op.
}
@Override
boolean pulseAnimationFrame(long frameTime) {
return doAnimationFrame(frameTime);
}
/**
* When playing forward, we call start() at the animation's scheduled start time, and make sure
* to pump a frame at the animation's scheduled end time.
*
* When playing in reverse, we should reverse the animation when we hit animation's end event,
* and expect the animation to end at the its delay ended event, rather than start event.
*/
private void handleAnimationEvents(int startId, int latestId, long playTime) {
if (mReversing) {
startId = startId == -1 ? mEvents.size() : startId;
for (int i = startId - 1; i >= latestId; i--) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_END) {
if (node.mAnimation.isStarted()) {
// If the animation has already been started before its due time (i.e.
// the child animator is being manipulated outside of the AnimatorSet), we
// need to cancel the animation to reset the internal state (e.g. frame
// time tracking) and remove the self pulsing callbacks
node.mAnimation.cancel();
}
node.mEnded = false;
mPlayingSet.add(event.mNode);
node.mAnimation.startWithoutPulsing(true);
pulseFrame(node, 0);
} else if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED && !node.mEnded) {
// end event:
pulseFrame(node, getPlayTimeForNode(playTime, node));
}
}
} else {
for (int i = startId + 1; i <= latestId; i++) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_START) {
mPlayingSet.add(event.mNode);
if (node.mAnimation.isStarted()) {
// If the animation has already been started before its due time (i.e.
// the child animator is being manipulated outside of the AnimatorSet), we
// need to cancel the animation to reset the internal state (e.g. frame
// time tracking) and remove the self pulsing callbacks
node.mAnimation.cancel();
}
node.mEnded = false;
node.mAnimation.startWithoutPulsing(false);
pulseFrame(node, 0);
} else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) {
// start event:
pulseFrame(node, getPlayTimeForNode(playTime, node));
}
}
}
}
/**
* This method pulses frames into child animations. It scales the input animation play time
* with the duration scale and pass that to the child animation via pulseAnimationFrame(long).
*
* @param node child animator node
* @param animPlayTime unscaled play time (including start delay) for the child animator
*/
private void pulseFrame(Node node, long animPlayTime) {
if (!node.mEnded) {
float durationScale = ValueAnimator.getDurationScale();
durationScale = durationScale == 0 ? 1 : durationScale;
node.mEnded = node.mAnimation.pulseAnimationFrame(
(long) (animPlayTime * durationScale));
}
}
private long getPlayTimeForNode(long overallPlayTime, Node node) {
return getPlayTimeForNode(overallPlayTime, node, mReversing);
}
private long getPlayTimeForNode(long overallPlayTime, Node node, boolean inReverse) {
if (inReverse) {
overallPlayTime = getTotalDuration() - overallPlayTime;
return node.mEndTime - overallPlayTime;
} else {
return overallPlayTime - node.mStartTime;
}
}
private void startAnimation() {
addDummyListener();
// Register animation callback
addAnimationCallback(0);
if (mSeekState.getPlayTimeNormalized() == 0 && mReversing) {
// Maintain old behavior, if seeked to 0 then call reverse, we'll treat the case
// the same as no seeking at all.
mSeekState.reset();
}
// Set the child animators to the right end:
if (mShouldResetValuesAtStart) {
if (isInitialized()) {
skipToEndValue(!mReversing);
} else if (mReversing) {
// Reversing but haven't initialized all the children yet.
initChildren();
skipToEndValue(!mReversing);
} else {
// If not all children are initialized and play direction is forward
for (int i = mEvents.size() - 1; i >= 0; i--) {
if (mEvents.get(i).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
Animator anim = mEvents.get(i).mNode.mAnimation;
// Only reset the animations that have been initialized to start value,
// so that if they are defined without a start value, they will get the
// values set at the right time (i.e. the next animation run)
if (anim.isInitialized()) {
anim.skipToEndValue(true);
}
}
}
}
}
if (mReversing || mStartDelay == 0 || mSeekState.isActive()) {
long playTime;
// If no delay, we need to call start on the first animations to be consistent with old
// behavior.
if (mSeekState.isActive()) {
mSeekState.updateSeekDirection(mReversing);
playTime = mSeekState.getPlayTime();
} else {
playTime = 0;
}
int toId = findLatestEventIdForTime(playTime);
handleAnimationEvents(-1, toId, playTime);
for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
if (mPlayingSet.get(i).mEnded) {
mPlayingSet.remove(i);
}
}
mLastEventId = toId;
}
}
// This is to work around the issue in b/34736819, as the old behavior in AnimatorSet had
// masked a real bug in play movies. TODO: remove this and below once the root cause is fixed.
private void addDummyListener() {
for (int i = 1; i < mNodes.size(); i++) {
mNodes.get(i).mAnimation.addListener(mDummyListener);
}
}
private void removeDummyListener() {
for (int i = 1; i < mNodes.size(); i++) {
mNodes.get(i).mAnimation.removeListener(mDummyListener);
}
}
private int findLatestEventIdForTime(long currentPlayTime) {
int size = mEvents.size();
int latestId = mLastEventId;
// Call start on the first animations now to be consistent with the old behavior
if (mReversing) {
currentPlayTime = getTotalDuration() - currentPlayTime;
mLastEventId = mLastEventId == -1 ? size : mLastEventId;
for (int j = mLastEventId - 1; j >= 0; j--) {
AnimationEvent event = mEvents.get(j);
if (event.getTime() >= currentPlayTime) {
latestId = j;
}
}
} else {
for (int i = mLastEventId + 1; i < size; i++) {
AnimationEvent event = mEvents.get(i);
// TODO: need a function that accounts for infinite duration to compare time
if (event.getTime() != DURATION_INFINITE && event.getTime() <= currentPlayTime) {
latestId = i;
}
}
}
return latestId;
}
private void endAnimation() {
mStarted = false;
mLastFrameTime = -1;
mFirstFrame = -1;
mLastEventId = -1;
mPaused = false;
mPauseTime = -1;
mSeekState.reset();
mPlayingSet.clear();
// No longer receive callbacks
removeAnimationCallback();
// Call end listener
if (mListeners != null) {
ArrayList
* Note: reverse is not supported for infinite AnimatorSet.
*/
@Override
public void reverse() {
start(true, true);
}
@Override
public String toString() {
String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{";
int size = mNodes.size();
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
returnVal += "\n " + node.mAnimation.toString();
}
return returnVal + "\n}";
}
private void printChildCount() {
// Print out the child count through a level traverse.
ArrayList The 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: 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 Note that it is possible to express relationships that cannot be resolved and will not
* result in sensible results. For example, 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.
*
* Note: Manipulating AnimatorSet's lifecycle in the child animators' listener callbacks
* will lead to undefined behaviors. Also, AnimatorSet will ignore any seeking in the child
* animators once {@link #start()} is called.
*/
@SuppressWarnings("unchecked")
@Override
public void start() {
start(false, true);
}
@Override
void startWithoutPulsing(boolean inReverse) {
start(inReverse, false);
}
private void initAnimation() {
if (mInterpolator != null) {
for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
node.mAnimation.setInterpolator(mInterpolator);
}
}
updateAnimatorsDuration();
createDependencyGraph();
}
private void start(boolean inReverse, boolean selfPulse) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mStarted = true;
mSelfPulse = selfPulse;
mPaused = false;
mPauseTime = -1;
int size = mNodes.size();
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
node.mEnded = false;
node.mAnimation.setAllowRunningAsynchronously(false);
}
initAnimation();
if (inReverse && !canReverse()) {
throw new UnsupportedOperationException("Cannot reverse infinite AnimatorSet");
}
mReversing = inReverse;
// Now that all dependencies are set up, start the animations that should be started.
boolean isEmptySet = isEmptySet(this);
if (!isEmptySet) {
startAnimation();
}
if (mListeners != null) {
ArrayListBuilder
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.
*
* Builder
object cannot be constructed directly, but is rather constructed
* internally via a call to {@link AnimatorSet#play(Animator)}.
* AnimatorSet s = new AnimatorSet();
* s.play(anim1).with(anim2);
* s.play(anim2).before(anim3);
* s.play(anim4).after(anim3);
*
*
* 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);
*
*
* 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.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 = getNodeForAnimation(anim);
mCurrentNode.addSibling(node);
return this;
}
/**
* Sets up the given animation to play when the animation supplied in the
* {@link AnimatorSet#play(Animator)} call that created this 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) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
}
/**
* Sets up the given animation to play when the animation supplied in the
* {@link AnimatorSet#play(Animator)} call that created this 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) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
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;
}
}
}