/* * 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.util.LongArray; import android.view.animation.Animation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; /** * 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() || mStartListenersCalled) {
notifyListeners(AnimatorCaller.ON_CANCEL, false);
callOnPlayingSet(Animator::cancel);
mPlayingSet.clear();
endAnimation();
}
}
/**
* Calls consumer on every Animator of mPlayingSet.
*
* @param consumer The method to call on every Animator of mPlayingSet.
*/
private void callOnPlayingSet(ConsumerNote that ending a AnimatorSet
also ends all of the animations that it is
* responsible for.
Starting this AnimatorSet
will, in turn, start the animations for which
* it is responsible. The details of when exactly those animations are started depends on
* the dependency relationships that have been set up between the animations.
*
* 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");
}
if (inReverse == mReversing && selfPulse == mSelfPulse && mStarted) {
// It is already started
return;
}
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();
}
notifyStartListeners(inReverse);
if (isEmptySet) {
// In the case of empty AnimatorSet, or 0 duration scale, we will trigger the
// onAnimationEnd() right away.
end();
}
}
// Returns true if set is empty or contains nothing but animator sets with no start delay.
private static boolean isEmptySet(AnimatorSet set) {
if (set.getStartDelay() > 0) {
return false;
}
for (int i = 0; i < set.getChildAnimations().size(); i++) {
Animator anim = set.getChildAnimations().get(i);
if (!(anim instanceof AnimatorSet)) {
// Contains non-AnimatorSet, not empty.
return false;
} else {
if (!isEmptySet((AnimatorSet) anim)) {
return false;
}
}
}
return true;
}
private void updateAnimatorsDuration() {
if (mDuration >= 0) {
// If the duration was set on this AnimatorSet, pass it along to all child animations
int size = mNodes.size();
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
// TODO: don't set the duration of the timing-only nodes created by AnimatorSet to
// insert "play-after" delays
node.mAnimation.setDuration(mDuration);
}
}
mDelayAnim.setDuration(mStartDelay);
}
@Override
void skipToEndValue(boolean inReverse) {
// This makes sure the animation events are sorted an up to date.
initAnimation();
initChildren();
// Calling skip to the end in the sequence that they would be called in a forward/reverse
// run, such that the sequential animations modifying the same property would have
// the right value in the end.
if (inReverse) {
for (int i = mEvents.size() - 1; i >= 0; i--) {
AnimationEvent event = mEvents.get(i);
if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
event.mNode.mAnimation.skipToEndValue(true);
}
}
} else {
for (int i = 0; i < mEvents.size(); i++) {
AnimationEvent event = mEvents.get(i);
if (event.mEvent == AnimationEvent.ANIMATION_END) {
event.mNode.mAnimation.skipToEndValue(false);
}
}
}
}
/**
* Internal only.
*
* This method sets the animation values based on the play time. It also fast forward or
* backward all the child animations progress accordingly.
*
* This method is also responsible for calling
* {@link android.view.animation.Animation.AnimationListener#onAnimationRepeat(Animation)},
* as needed, based on the last play time and current play time.
*/
private void animateBasedOnPlayTime(
long currentPlayTime,
long lastPlayTime,
boolean inReverse
) {
if (currentPlayTime < 0 || lastPlayTime < -1) {
throw new UnsupportedOperationException("Error: Play time should never be negative.");
}
// TODO: take into account repeat counts and repeat callback when repeat is implemented.
if (inReverse) {
long duration = getTotalDuration();
if (duration == DURATION_INFINITE) {
throw new UnsupportedOperationException(
"Cannot reverse AnimatorSet with infinite duration"
);
}
// Convert the play times to the forward direction.
currentPlayTime = Math.min(currentPlayTime, duration);
currentPlayTime = duration - currentPlayTime;
lastPlayTime = duration - lastPlayTime;
}
long[] startEndTimes = ensureChildStartAndEndTimes();
int index = findNextIndex(lastPlayTime, startEndTimes);
int endIndex = findNextIndex(currentPlayTime, startEndTimes);
// Change values at the start/end times so that values are set in the right order.
// We don't want an animator that would finish before another to override the value
// set by another animator that finishes earlier.
if (currentPlayTime >= lastPlayTime) {
while (index < endIndex) {
long playTime = startEndTimes[index];
if (lastPlayTime != playTime) {
animateSkipToEnds(playTime, lastPlayTime);
animateValuesInRange(playTime, lastPlayTime);
lastPlayTime = playTime;
}
index++;
}
} else {
while (index > endIndex) {
index--;
long playTime = startEndTimes[index];
if (lastPlayTime != playTime) {
animateSkipToEnds(playTime, lastPlayTime);
animateValuesInRange(playTime, lastPlayTime);
lastPlayTime = playTime;
}
}
}
if (currentPlayTime != lastPlayTime) {
animateSkipToEnds(currentPlayTime, lastPlayTime);
animateValuesInRange(currentPlayTime, lastPlayTime);
}
}
/**
* Looks through startEndTimes for playTime. If it is in startEndTimes, the index after
* is returned. Otherwise, it returns the index at which it would be placed if it were
* to be inserted.
*/
private int findNextIndex(long playTime, long[] startEndTimes) {
int index = Arrays.binarySearch(startEndTimes, playTime);
if (index < 0) {
index = -index - 1;
} else {
index++;
}
return index;
}
@Override
void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {
initAnimation();
if (lastPlayTime > currentPlayTime) {
notifyStartListeners(true);
for (int i = mEvents.size() - 1; i >= 0; i--) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_END
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
if (currentPlayTime <= start && start < lastPlayTime) {
animator.animateSkipToEnds(
0,
lastPlayTime - node.mStartTime
);
mPlayingSet.remove(node);
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
lastPlayTime - node.mStartTime
);
if (!mPlayingSet.contains(node)) {
mPlayingSet.add(node);
}
}
}
}
if (currentPlayTime <= 0) {
notifyEndListeners(true);
}
} else {
notifyStartListeners(false);
int eventsSize = mEvents.size();
for (int i = 0; i < eventsSize; i++) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
if (lastPlayTime < end && end <= currentPlayTime) {
animator.animateSkipToEnds(
end - node.mStartTime,
lastPlayTime - node.mStartTime
);
mPlayingSet.remove(node);
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
lastPlayTime - node.mStartTime
);
if (!mPlayingSet.contains(node)) {
mPlayingSet.add(node);
}
}
}
}
if (currentPlayTime >= getTotalDuration()) {
notifyEndListeners(false);
}
}
}
@Override
void animateValuesInRange(long currentPlayTime, long lastPlayTime) {
initAnimation();
if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) {
notifyStartListeners(false);
} else {
long duration = getTotalDuration();
if (duration >= 0
&& (lastPlayTime > duration || (lastPlayTime == duration
&& currentPlayTime < duration))
) {
notifyStartListeners(true);
}
}
int eventsSize = mEvents.size();
for (int i = 0; i < eventsSize; i++) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
if ((start < currentPlayTime && currentPlayTime < end)
|| (start == currentPlayTime && lastPlayTime < start)
|| (end == currentPlayTime && lastPlayTime > end)
) {
animator.animateValuesInRange(
currentPlayTime - node.mStartTime,
Math.max(-1, lastPlayTime - node.mStartTime)
);
}
}
}
}
private long[] ensureChildStartAndEndTimes() {
if (mChildStartAndStopTimes == null) {
LongArray startAndEndTimes = new LongArray();
getStartAndEndTimes(startAndEndTimes, 0);
long[] times = startAndEndTimes.toArray();
Arrays.sort(times);
mChildStartAndStopTimes = times;
}
return mChildStartAndStopTimes;
}
@Override
void getStartAndEndTimes(LongArray times, long offset) {
int eventsSize = mEvents.size();
for (int i = 0; i < eventsSize; i++) {
AnimationEvent event = mEvents.get(i);
if (event.mEvent == AnimationEvent.ANIMATION_DELAY_ENDED
&& event.mNode.mStartTime != DURATION_INFINITE
) {
event.mNode.mAnimation.getStartAndEndTimes(times, offset + event.mNode.mStartTime);
}
}
}
@Override
boolean isInitialized() {
if (mChildrenInitialized) {
return true;
}
boolean allInitialized = true;
for (int i = 0; i < mNodes.size(); i++) {
if (!mNodes.get(i).mAnimation.isInitialized()) {
allInitialized = false;
break;
}
}
mChildrenInitialized = allInitialized;
return mChildrenInitialized;
}
/**
* Sets the position of the animation to the specified point in time. This time should
* be between 0 and the total duration of the animation, including any repetition. If
* the animation has not yet been started, then it will not advance forward after it is
* set to this time; it will simply set the time to this value and perform any appropriate
* actions based on that time. If the animation is already running, then setCurrentPlayTime()
* will set the current playing time to this value and continue playing from that point.
* On {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, an AnimatorSet
* that hasn't been {@link #start()}ed, will issue
* {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator, boolean)}
* and {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator, boolean)}
* events.
*
* @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
* Unless the animation is reversing, the playtime is considered the time since
* the end of the start delay of the AnimatorSet in a forward playing direction.
*
*/
public void setCurrentPlayTime(long playTime) {
if (mReversing && getTotalDuration() == DURATION_INFINITE) {
// Should never get here
throw new UnsupportedOperationException("Error: Cannot seek in reverse in an infinite"
+ " AnimatorSet");
}
if ((getTotalDuration() != DURATION_INFINITE && playTime > getTotalDuration() - mStartDelay)
|| playTime < 0) {
throw new UnsupportedOperationException("Error: Play time should always be in between"
+ " 0 and duration.");
}
initAnimation();
long lastPlayTime = mSeekState.getPlayTime();
if (!isStarted() || isPaused()) {
if (mReversing && !isStarted()) {
throw new UnsupportedOperationException("Error: Something went wrong. mReversing"
+ " should not be set when AnimatorSet is not started.");
}
if (!mSeekState.isActive()) {
findLatestEventIdForTime(0);
initChildren();
// Set all the values to start values.
skipToEndValue(!mReversing);
mSeekState.setPlayTime(0, mReversing);
}
}
mSeekState.setPlayTime(playTime, mReversing);
animateBasedOnPlayTime(playTime, lastPlayTime, mReversing);
}
/**
* Returns the milliseconds elapsed since the start of the animation.
*
*
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;
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, getPlayTimeForNodeIncludingDelay(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, getPlayTimeForNodeIncludingDelay(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, getPlayTimeForNodeIncludingDelay(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;
if (node.mAnimation.pulseAnimationFrame((long) (animPlayTime * durationScale))) {
node.mEnded = true;
}
}
}
private long getPlayTimeForNodeIncludingDelay(long overallPlayTime, Node node) {
return getPlayTimeForNodeIncludingDelay(overallPlayTime, node, mReversing);
}
private long getPlayTimeForNodeIncludingDelay(
long overallPlayTime,
Node node,
boolean inReverse
) {
if (inReverse) {
overallPlayTime = getTotalDuration() - overallPlayTime;
return node.mEndTime - overallPlayTime;
} else {
return overallPlayTime - node.mStartTime;
}
}
private void startAnimation() {
addAnimationEndListener();
// 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 addAnimationEndListener() {
for (int i = 1; i < mNodes.size(); i++) {
mNodes.get(i).mAnimation.addListener(mAnimationEndListener);
}
}
private void removeAnimationEndListener() {
for (int i = 1; i < mNodes.size(); i++) {
mNodes.get(i).mAnimation.removeListener(mAnimationEndListener);
}
}
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();
notifyEndListeners(mReversing);
removeAnimationEndListener();
mSelfPulse = true;
mReversing = false;
}
private void removeAnimationCallback() {
if (!mSelfPulse) {
return;
}
AnimationHandler handler = AnimationHandler.getInstance();
handler.removeCallback(this);
}
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
AnimationHandler handler = AnimationHandler.getInstance();
handler.addAnimationFrameCallback(this, delay);
}
@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.
*/
final int nodeCount = mNodes.size();
anim.mStarted = false;
anim.mLastFrameTime = -1;
anim.mFirstFrame = -1;
anim.mLastEventId = -1;
anim.mPaused = false;
anim.mPauseTime = -1;
anim.mSeekState = new SeekState();
anim.mSelfPulse = true;
anim.mStartListenersCalled = false;
anim.mPlayingSet = new 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, Builder
object is a utility class to facilitate adding animations to a
* AnimatorSet
along with the relationships between the various animations. The
* intention of the Builder
methods, along with the {@link
* AnimatorSet#play(Animator) play()} method of AnimatorSet
is to make it possible
* to express the dependency relationships of animations in a natural way. Developers can also
* use the {@link AnimatorSet#playTogether(Animator[]) playTogether()} and {@link
* AnimatorSet#playSequentially(Animator[]) playSequentially()} methods if these suit the need,
* but it might be easier in some situations to express the AnimatorSet of animations in pairs.
*
* 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 a ValueAnimator just to run the clock
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(delay);
after(anim);
return this;
}
}
}