summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/animation/Animator.java143
-rw-r--r--core/java/android/animation/AnimatorSet.java96
-rw-r--r--core/java/android/animation/ValueAnimator.java76
-rw-r--r--core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java8
-rw-r--r--core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java526
-rw-r--r--core/tests/coretests/src/android/animation/ValueAnimatorTests.java60
-rw-r--r--core/tests/coretests/src/android/animation/ViewPropertyAnimatorTest.java6
7 files changed, 778 insertions, 137 deletions
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index d0ce70133414..12026aa3f72a 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -72,6 +72,20 @@ public abstract class Animator implements Cloneable {
private static long sBackgroundPauseDelay = 1000;
/**
+ * A cache of the values in a list. Used so that when calling the list, we have a copy
+ * of it in case the list is modified while iterating. The array can be reused to avoid
+ * allocation on every notification.
+ */
+ private Object[] mCachedList;
+
+ /**
+ * Tracks whether we've notified listeners of the onAnimationStart() event. This can be
+ * complex to keep track of since we notify listeners at different times depending on
+ * startDelay and whether start() was called before end().
+ */
+ boolean mStartListenersCalled = false;
+
+ /**
* Sets the duration for delaying pausing animators when apps go into the background.
* Used by AnimationHandler when requested to pause animators.
*
@@ -158,16 +172,11 @@ public abstract class Animator implements Cloneable {
* @see AnimatorPauseListener
*/
public void pause() {
- if (isStarted() && !mPaused) {
+ // We only want to pause started Animators or animators that setCurrentPlayTime()
+ // have been called on. mStartListenerCalled will be true if seek has happened.
+ if ((isStarted() || mStartListenersCalled) && !mPaused) {
mPaused = true;
- if (mPauseListeners != null) {
- ArrayList<AnimatorPauseListener> tmpListeners =
- (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationPause(this);
- }
- }
+ notifyPauseListeners(AnimatorCaller.ON_PAUSE);
}
}
@@ -184,14 +193,7 @@ public abstract class Animator implements Cloneable {
public void resume() {
if (mPaused) {
mPaused = false;
- if (mPauseListeners != null) {
- ArrayList<AnimatorPauseListener> tmpListeners =
- (ArrayList<AnimatorPauseListener>) mPauseListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationResume(this);
- }
- }
+ notifyPauseListeners(AnimatorCaller.ON_RESUME);
}
}
@@ -450,6 +452,8 @@ public abstract class Animator implements Cloneable {
if (mPauseListeners != null) {
anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(mPauseListeners);
}
+ anim.mCachedList = null;
+ anim.mStartListenersCalled = false;
return anim;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
@@ -591,6 +595,86 @@ public abstract class Animator implements Cloneable {
}
/**
+ * Calls notification for each AnimatorListener.
+ *
+ * @param notification The notification method to call on each listener.
+ * @param isReverse When this is used with start/end, this is the isReverse parameter. For
+ * other calls, this is ignored.
+ */
+ void notifyListeners(
+ AnimatorCaller<AnimatorListener, Animator> notification,
+ boolean isReverse
+ ) {
+ callOnList(mListeners, notification, this, isReverse);
+ }
+
+ /**
+ * Call pause/resume on each AnimatorPauseListener.
+ *
+ * @param notification Either ON_PAUSE or ON_RESUME to call onPause or onResume on each
+ * listener.
+ */
+ void notifyPauseListeners(AnimatorCaller<AnimatorPauseListener, Animator> notification) {
+ callOnList(mPauseListeners, notification, this, false);
+ }
+
+ void notifyStartListeners(boolean isReversing) {
+ boolean startListenersCalled = mStartListenersCalled;
+ mStartListenersCalled = true;
+ if (mListeners != null && !startListenersCalled) {
+ notifyListeners(AnimatorCaller.ON_START, isReversing);
+ }
+ }
+
+ void notifyEndListeners(boolean isReversing) {
+ boolean startListenersCalled = mStartListenersCalled;
+ mStartListenersCalled = false;
+ if (mListeners != null && startListenersCalled) {
+ notifyListeners(AnimatorCaller.ON_END, isReversing);
+ }
+ }
+
+ /**
+ * Calls <code>call</code> for every item in <code>list</code> with <code>animator</code> and
+ * <code>isReverse</code> as parameters.
+ *
+ * @param list The list of items to make calls on.
+ * @param call The method to call for each item in list.
+ * @param animator The animator parameter of call.
+ * @param isReverse The isReverse parameter of call.
+ * @param <T> The item type of list
+ * @param <A> The Animator type of animator.
+ */
+ <T, A> void callOnList(
+ ArrayList<T> list,
+ AnimatorCaller<T, A> call,
+ A animator,
+ boolean isReverse
+ ) {
+ int size = list == null ? 0 : list.size();
+ if (size > 0) {
+ // Try to reuse mCacheList to store the items of list.
+ Object[] array;
+ if (mCachedList == null || mCachedList.length < size) {
+ array = new Object[size];
+ } else {
+ array = mCachedList;
+ // Clear it in case there is some reentrancy
+ mCachedList = null;
+ }
+ list.toArray(array);
+ for (int i = 0; i < size; i++) {
+ //noinspection unchecked
+ T item = (T) array[i];
+ call.call(item, animator, isReverse);
+ array[i] = null;
+ }
+ // Store it for the next call so we can reuse this array, if needed.
+ mCachedList = array;
+ }
+ }
+
+ /**
* <p>An animation listener receives notifications from an animation.
* Notifications indicate animation related events, such as the end or the
* repetition of the animation.</p>
@@ -748,4 +832,29 @@ public abstract class Animator implements Cloneable {
return clone;
}
}
+
+ /**
+ * Internally used by {@link #callOnList(ArrayList, AnimatorCaller, Object, boolean)} to
+ * make a call on all children of a list. This can be for start, stop, pause, cancel, update,
+ * etc notifications.
+ *
+ * @param <T> The type of listener to make the call on
+ * @param <A> The type of animator that is passed as a parameter
+ */
+ interface AnimatorCaller<T, A> {
+ void call(T listener, A animator, boolean isReverse);
+
+ AnimatorCaller<AnimatorListener, Animator> ON_START = AnimatorListener::onAnimationStart;
+ AnimatorCaller<AnimatorListener, Animator> ON_END = AnimatorListener::onAnimationEnd;
+ AnimatorCaller<AnimatorListener, Animator> ON_CANCEL =
+ (listener, animator, isReverse) -> listener.onAnimationCancel(animator);
+ AnimatorCaller<AnimatorListener, Animator> ON_REPEAT =
+ (listener, animator, isReverse) -> listener.onAnimationRepeat(animator);
+ AnimatorCaller<AnimatorPauseListener, Animator> ON_PAUSE =
+ (listener, animator, isReverse) -> listener.onAnimationPause(animator);
+ AnimatorCaller<AnimatorPauseListener, Animator> ON_RESUME =
+ (listener, animator, isReverse) -> listener.onAnimationResume(animator);
+ AnimatorCaller<ValueAnimator.AnimatorUpdateListener, ValueAnimator> ON_UPDATE =
+ (listener, animator, isReverse) -> listener.onAnimationUpdate(animator);
+ }
}
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 257adfe390d6..60659dc12342 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -32,6 +32,7 @@ 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
@@ -188,11 +189,6 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
*/
private long[] mChildStartAndStopTimes;
- /**
- * Tracks whether we've notified listeners of the onAnimationStart() event.
- */
- private boolean mStartListenersCalled;
-
// This is to work around a bug in b/34736819. This needs to be removed once app team
// fixes their side.
private AnimatorListenerAdapter mAnimationEndListener = new AnimatorListenerAdapter() {
@@ -423,25 +419,29 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
- if (isStarted()) {
- ArrayList<AnimatorListener> tmpListeners = null;
- if (mListeners != null) {
- tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone();
- int size = tmpListeners.size();
- for (int i = 0; i < size; i++) {
- tmpListeners.get(i).onAnimationCancel(this);
- }
- }
- ArrayList<Node> playingSet = new ArrayList<>(mPlayingSet);
- int setSize = playingSet.size();
- for (int i = 0; i < setSize; i++) {
- playingSet.get(i).mAnimation.cancel();
- }
+ 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(Consumer<Animator> consumer) {
+ final ArrayList<Node> list = mPlayingSet;
+ final int size = list.size();
+ //noinspection ForLoopReplaceableByForEach
+ for (int i = 0; i < size; i++) {
+ final Animator animator = list.get(i).mAnimation;
+ consumer.accept(animator);
+ }
+ }
+
// Force all the animations to end when the duration scale is 0.
private void forceToEnd() {
if (mEndCanBeCalled) {
@@ -481,13 +481,13 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
return;
}
if (isStarted()) {
+ mStarted = false; // don't allow reentrancy
// Iterate the animations that haven't finished or haven't started, and end them.
if (mReversing) {
// Between start() and first frame, mLastEventId would be unset (i.e. -1)
mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId;
- while (mLastEventId > 0) {
- mLastEventId = mLastEventId - 1;
- AnimationEvent event = mEvents.get(mLastEventId);
+ for (int eventId = mLastEventId - 1; eventId >= 0; eventId--) {
+ AnimationEvent event = mEvents.get(eventId);
Animator anim = event.mNode.mAnimation;
if (mNodeMap.get(anim).mEnded) {
continue;
@@ -503,11 +503,10 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
}
}
} else {
- while (mLastEventId < mEvents.size() - 1) {
+ for (int eventId = mLastEventId + 1; eventId < mEvents.size(); eventId++) {
// Avoid potential reentrant loop caused by child animators manipulating
// AnimatorSet's lifecycle (i.e. not a recommended approach).
- mLastEventId = mLastEventId + 1;
- AnimationEvent event = mEvents.get(mLastEventId);
+ AnimationEvent event = mEvents.get(eventId);
Animator anim = event.mNode.mAnimation;
if (mNodeMap.get(anim).mEnded) {
continue;
@@ -522,7 +521,6 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
}
}
}
- mPlayingSet.clear();
}
endAnimation();
}
@@ -662,6 +660,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
super.pause();
if (!previouslyPaused && mPaused) {
mPauseTime = -1;
+ callOnPlayingSet(Animator::pause);
}
}
@@ -676,6 +675,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
if (mPauseTime >= 0) {
addAnimationCallback(0);
}
+ callOnPlayingSet(Animator::resume);
}
}
@@ -716,6 +716,10 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
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;
@@ -749,32 +753,6 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
}
}
- private void notifyStartListeners(boolean inReverse) {
- if (mListeners != null && !mStartListenersCalled) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- AnimatorListener listener = tmpListeners.get(i);
- listener.onAnimationStart(this, inReverse);
- }
- }
- mStartListenersCalled = true;
- }
-
- private void notifyEndListeners(boolean inReverse) {
- if (mListeners != null && mStartListenersCalled) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- AnimatorListener listener = tmpListeners.get(i);
- listener.onAnimationEnd(this, inReverse);
- }
- }
- mStartListenersCalled = false;
- }
-
// 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) {
@@ -941,12 +919,18 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
lastPlayTime - node.mStartTime,
notify
);
+ if (notify) {
+ mPlayingSet.remove(node);
+ }
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
lastPlayTime - node.mStartTime,
notify
);
+ if (notify && !mPlayingSet.contains(node)) {
+ mPlayingSet.add(node);
+ }
}
}
}
@@ -974,12 +958,18 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
lastPlayTime - node.mStartTime,
notify
);
+ if (notify) {
+ mPlayingSet.remove(node);
+ }
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
lastPlayTime - node.mStartTime,
notify
);
+ if (notify && !mPlayingSet.contains(node)) {
+ mPlayingSet.add(node);
+ }
}
}
}
@@ -1120,8 +1110,8 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
mSeekState.setPlayTime(0, mReversing);
}
}
- animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true);
mSeekState.setPlayTime(playTime, mReversing);
+ animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true);
}
/**
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 7009725aa32b..5d69f8b80799 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -199,13 +199,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
private boolean mStarted = false;
/**
- * Tracks whether we've notified listeners of the onAnimationStart() event. This can be
- * complex to keep track of since we notify listeners at different times depending on
- * startDelay and whether start() was called before end().
- */
- private boolean mStartListenersCalled = false;
-
- /**
* Flag that denotes whether the animation is set up and ready to go. Used to
* set up animation that has not yet been started.
*/
@@ -1108,30 +1101,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
}
}
- private void notifyStartListeners(boolean isReversing) {
- if (mListeners != null && !mStartListenersCalled) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this, isReversing);
- }
- }
- mStartListenersCalled = true;
- }
-
- private void notifyEndListeners(boolean isReversing) {
- if (mListeners != null && mStartListenersCalled) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this, isReversing);
- }
- }
- mStartListenersCalled = false;
- }
-
/**
* Start the animation playing. This version of start() takes a boolean flag that indicates
* whether the animation should play in reverse. The flag is usually false, but may be set
@@ -1149,6 +1118,10 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
+ if (playBackwards == mResumed && mSelfPulse == !mSuppressSelfPulseRequested && mStarted) {
+ // already started
+ return;
+ }
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
@@ -1219,23 +1192,14 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
// Only cancel if the animation is actually running or has been started and is about
// to run
// Only notify listeners if the animator has actually started
- if ((mStarted || mRunning) && mListeners != null) {
+ if ((mStarted || mRunning || mStartListenersCalled) && mListeners != null) {
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
notifyStartListeners(mReversing);
}
- int listenersSize = mListeners.size();
- if (listenersSize > 0) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- for (int i = 0; i < listenersSize; i++) {
- AnimatorListener listener = tmpListeners.get(i);
- listener.onAnimationCancel(this);
- }
- }
+ notifyListeners(AnimatorCaller.ON_CANCEL, false);
}
endAnimation();
-
}
@Override
@@ -1338,11 +1302,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
// If it's not yet running, then start listeners weren't called. Call them now.
notifyStartListeners(mReversing);
}
- mRunning = false;
- mStarted = false;
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
+ mRunning = false;
+ mStarted = false;
notifyEndListeners(mReversing);
// mReversing needs to be reset *after* notifying the listeners for the end callbacks.
mReversing = false;
@@ -1435,12 +1399,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
- if (mListeners != null) {
- int numListeners = mListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- mListeners.get(i).onAnimationRepeat(this);
- }
- }
+ notifyListeners(AnimatorCaller.ON_REPEAT, false);
} else if (lastIterationFinished) {
done = true;
}
@@ -1493,13 +1452,8 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
iteration = Math.min(iteration, mRepeatCount);
lastIteration = Math.min(lastIteration, mRepeatCount);
- if (iteration != lastIteration) {
- if (mListeners != null) {
- int numListeners = mListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- mListeners.get(i).onAnimationRepeat(this);
- }
- }
+ if (notify && iteration != lastIteration) {
+ notifyListeners(AnimatorCaller.ON_REPEAT, false);
}
}
@@ -1697,11 +1651,8 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
- if (mUpdateListeners != null) {
- int numListeners = mUpdateListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- mUpdateListeners.get(i).onAnimationUpdate(this);
- }
+ if (mSeekFraction >= 0 || mStartListenersCalled) {
+ callOnList(mUpdateListeners, AnimatorCaller.ON_UPDATE, this, false);
}
}
@@ -1718,7 +1669,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
anim.mRunning = false;
anim.mPaused = false;
anim.mResumed = false;
- anim.mStartListenersCalled = false;
anim.mStartTime = -1;
anim.mStartTimeCommitted = false;
anim.mAnimationEndRequested = false;
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
index 7a1de0c5d4fe..a7538701807a 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
@@ -435,9 +435,11 @@ public class AnimatorSetActivityTest {
mActivityRule.runOnUiThread(s::start);
while (!listener.endIsCalled) {
- boolean passedStartDelay = a1.isStarted() || a2.isStarted() || a3.isStarted() ||
- a4.isStarted() || a5.isStarted();
- assertEquals(passedStartDelay, s.isRunning());
+ mActivityRule.runOnUiThread(() -> {
+ boolean passedStartDelay = a1.isStarted() || a2.isStarted() || a3.isStarted()
+ || a4.isStarted() || a5.isStarted();
+ assertEquals(passedStartDelay, s.isRunning());
+ });
Thread.sleep(50);
}
assertFalse(s.isRunning());
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
new file mode 100644
index 000000000000..43266a51502b
--- /dev/null
+++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2023 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.PollingCheck;
+import android.view.View;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.MediumTest;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+public class AnimatorSetCallsTest {
+ @Rule
+ public final ActivityScenarioRule<AnimatorSetActivity> mRule =
+ new ActivityScenarioRule<>(AnimatorSetActivity.class);
+
+ private AnimatorSetActivity mActivity;
+ private AnimatorSet mSet1;
+ private AnimatorSet mSet2;
+ private ObjectAnimator mAnimator;
+ private CountListener mListener1;
+ private CountListener mListener2;
+ private CountListener mListener3;
+
+ @Before
+ public void setUp() throws Exception {
+ mRule.getScenario().onActivity((activity) -> {
+ mActivity = activity;
+ View square = mActivity.findViewById(R.id.square1);
+
+ mSet1 = new AnimatorSet();
+ mListener1 = new CountListener();
+ mSet1.addListener(mListener1);
+ mSet1.addPauseListener(mListener1);
+
+ mSet2 = new AnimatorSet();
+ mListener2 = new CountListener();
+ mSet2.addListener(mListener2);
+ mSet2.addPauseListener(mListener2);
+
+ mAnimator = ObjectAnimator.ofFloat(square, "translationX", 0f, 100f);
+ mListener3 = new CountListener();
+ mAnimator.addListener(mListener3);
+ mAnimator.addPauseListener(mListener3);
+ mAnimator.setDuration(1);
+
+ mSet2.play(mAnimator);
+ mSet1.play(mSet2);
+ });
+ }
+
+ @Test
+ public void startEndCalledOnChildren() {
+ mRule.getScenario().onActivity((a) -> mSet1.start());
+ waitForOnUiThread(() -> mListener1.endForward > 0);
+
+ // only startForward and endForward should have been called once
+ mListener1.assertValues(
+ 1, 0, 1, 0, 0, 0, 0, 0
+ );
+ mListener2.assertValues(
+ 1, 0, 1, 0, 0, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 1, 0, 1, 0, 0, 0, 0, 0
+ );
+ }
+
+ @Test
+ public void cancelCalledOnChildren() {
+ mRule.getScenario().onActivity((a) -> {
+ mSet1.start();
+ mSet1.cancel();
+ });
+ waitForOnUiThread(() -> mListener1.endForward > 0);
+
+ // only startForward and endForward should have been called once
+ mListener1.assertValues(
+ 1, 0, 1, 0, 1, 0, 0, 0
+ );
+ mListener2.assertValues(
+ 1, 0, 1, 0, 1, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 1, 0, 1, 0, 1, 0, 0, 0
+ );
+ }
+
+ @Test
+ public void startEndReversedCalledOnChildren() {
+ mRule.getScenario().onActivity((a) -> mSet1.reverse());
+ waitForOnUiThread(() -> mListener1.endReverse > 0);
+
+ // only startForward and endForward should have been called once
+ mListener1.assertValues(
+ 0, 1, 0, 1, 0, 0, 0, 0
+ );
+ mListener2.assertValues(
+ 0, 1, 0, 1, 0, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 0, 1, 0, 1, 0, 0, 0, 0
+ );
+ }
+
+ @Test
+ public void pauseResumeCalledOnChildren() {
+ mRule.getScenario().onActivity((a) -> {
+ mSet1.start();
+ mSet1.pause();
+ });
+ waitForOnUiThread(() -> mListener1.pause > 0);
+
+ // only startForward and pause should have been called once
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+ mListener3.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+
+ mRule.getScenario().onActivity((a) -> mSet1.resume());
+ waitForOnUiThread(() -> mListener1.endForward > 0);
+
+ // resume and endForward should have been called once
+ mListener1.assertValues(
+ 1, 0, 1, 0, 0, 0, 1, 1
+ );
+ mListener2.assertValues(
+ 1, 0, 1, 0, 0, 0, 1, 1
+ );
+ mListener3.assertValues(
+ 1, 0, 1, 0, 0, 0, 1, 1
+ );
+ }
+
+ @Test
+ public void updateOnlyWhileChangingValues() {
+ ArrayList<Float> updateValues = new ArrayList<>();
+ mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ updateValues.add((Float) animation.getAnimatedValue());
+ }
+ });
+
+ mSet1.setCurrentPlayTime(0);
+
+ assertEquals(1, updateValues.size());
+ assertEquals(0f, updateValues.get(0), 0f);
+ }
+
+ @Test
+ public void updateOnlyWhileRunning() {
+ ArrayList<Float> updateValues = new ArrayList<>();
+ mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ updateValues.add((Float) animation.getAnimatedValue());
+ }
+ });
+
+ mRule.getScenario().onActivity((a) -> {
+ mSet1.start();
+ });
+
+ waitForOnUiThread(() -> mListener1.endForward > 0);
+
+ // the duration is only 1ms, so there should only be two values, 0 and 100.
+ assertEquals(0f, updateValues.get(0), 0f);
+ assertEquals(100f, updateValues.get(updateValues.size() - 1), 0f);
+
+ // now check all the values in the middle, which can never go from 100->0.
+ boolean isAtEnd = false;
+ for (int i = 1; i < updateValues.size() - 1; i++) {
+ float actual = updateValues.get(i);
+ if (actual == 100f) {
+ isAtEnd = true;
+ }
+ float expected = isAtEnd ? 100f : 0f;
+ assertEquals(expected, actual, 0f);
+ }
+ }
+
+ @Test
+ public void pauseResumeSeekingAnimators() {
+ ValueAnimator animator2 = ValueAnimator.ofFloat(0f, 1f);
+ mSet2.play(animator2).after(mAnimator);
+ mSet2.setStartDelay(100);
+ mSet1.setStartDelay(100);
+ mAnimator.setDuration(100);
+
+ mActivity.runOnUiThread(() -> {
+ mSet1.setCurrentPlayTime(0);
+ mSet1.pause();
+
+ // only startForward and pause should have been called once
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+ mListener2.assertValues(
+ 0, 0, 0, 0, 0, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 0, 0, 0, 0, 0, 0, 0, 0
+ );
+
+ mSet1.resume();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 1
+ );
+ mListener2.assertValues(
+ 0, 0, 0, 0, 0, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 0, 0, 0, 0, 0, 0, 0, 0
+ );
+
+ mSet1.setCurrentPlayTime(200);
+
+ // resume and endForward should have been called once
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 1
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 0, 0
+ );
+ mListener3.assertValues(
+ 1, 0, 0, 0, 0, 0, 0, 0
+ );
+
+ mSet1.pause();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 2, 1
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+ mListener3.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 0
+ );
+ mSet1.resume();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 2, 2
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 1
+ );
+ mListener3.assertValues(
+ 1, 0, 0, 0, 0, 0, 1, 1
+ );
+
+ // now go to animator2
+ mSet1.setCurrentPlayTime(400);
+ mSet1.pause();
+ mSet1.resume();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 3, 3
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 2, 2
+ );
+ mListener3.assertValues(
+ 1, 0, 1, 0, 0, 0, 1, 1
+ );
+
+ // now go back to mAnimator
+ mSet1.setCurrentPlayTime(250);
+ mSet1.pause();
+ mSet1.resume();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 4, 4
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 0, 0, 0, 3, 3
+ );
+ mListener3.assertValues(
+ 1, 1, 1, 0, 0, 0, 2, 2
+ );
+
+ // now go back to before mSet2 was being run
+ mSet1.setCurrentPlayTime(1);
+ mSet1.pause();
+ mSet1.resume();
+ mListener1.assertValues(
+ 1, 0, 0, 0, 0, 0, 5, 5
+ );
+ mListener2.assertValues(
+ 1, 0, 0, 1, 0, 0, 3, 3
+ );
+ mListener3.assertValues(
+ 1, 1, 1, 1, 0, 0, 2, 2
+ );
+ });
+ }
+
+ @Test
+ public void endInCancel() throws Throwable {
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mSet1.end();
+ }
+ };
+ mSet1.addListener(listener);
+ mActivity.runOnUiThread(() -> {
+ mSet1.start();
+ mSet1.cancel();
+ // Should go to the end value
+ View square = mActivity.findViewById(R.id.square1);
+ assertEquals(100f, square.getTranslationX(), 0.001f);
+ });
+ }
+
+ @Test
+ public void reentrantStart() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(3);
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation, boolean isReverse) {
+ mSet1.start();
+ latch.countDown();
+ }
+ };
+ mSet1.addListener(listener);
+ mSet2.addListener(listener);
+ mAnimator.addListener(listener);
+ mActivity.runOnUiThread(() -> mSet1.start());
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+ mActivity.runOnUiThread(() -> {});
+ }
+
+ @Test
+ public void reentrantEnd() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(3);
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ mSet1.end();
+ latch.countDown();
+ }
+ };
+ mSet1.addListener(listener);
+ mSet2.addListener(listener);
+ mAnimator.addListener(listener);
+ mActivity.runOnUiThread(() -> {
+ mSet1.start();
+ mSet1.end();
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+ mActivity.runOnUiThread(() -> {});
+ }
+
+ @Test
+ public void reentrantPause() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(3);
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationPause(Animator animation) {
+ mSet1.pause();
+ latch.countDown();
+ }
+ };
+ mSet1.addPauseListener(listener);
+ mSet2.addPauseListener(listener);
+ mAnimator.addPauseListener(listener);
+ mActivity.runOnUiThread(() -> {
+ mSet1.start();
+ mSet1.pause();
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+ mActivity.runOnUiThread(() -> {});
+ }
+
+ @Test
+ public void reentrantResume() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(3);
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationResume(Animator animation) {
+ mSet1.resume();
+ latch.countDown();
+ }
+ };
+ mSet1.addPauseListener(listener);
+ mSet2.addPauseListener(listener);
+ mAnimator.addPauseListener(listener);
+ mActivity.runOnUiThread(() -> {
+ mSet1.start();
+ mSet1.pause();
+ mSet1.resume();
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+ mActivity.runOnUiThread(() -> {});
+ }
+
+ private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) {
+ final boolean[] value = new boolean[1];
+ PollingCheck.waitFor(() -> {
+ mActivity.runOnUiThread(() -> value[0] = condition.canProceed());
+ return value[0];
+ });
+ }
+
+ private static class CountListener implements Animator.AnimatorListener,
+ Animator.AnimatorPauseListener {
+ public int startNoParam;
+ public int endNoParam;
+ public int startReverse;
+ public int startForward;
+ public int endForward;
+ public int endReverse;
+ public int cancel;
+ public int repeat;
+ public int pause;
+ public int resume;
+
+ public void assertValues(
+ int startForward,
+ int startReverse,
+ int endForward,
+ int endReverse,
+ int cancel,
+ int repeat,
+ int pause,
+ int resume
+ ) {
+ assertEquals("onAnimationStart() without direction", 0, startNoParam);
+ assertEquals("onAnimationEnd() without direction", 0, endNoParam);
+ assertEquals("onAnimationStart(forward)", startForward, this.startForward);
+ assertEquals("onAnimationStart(reverse)", startReverse, this.startReverse);
+ assertEquals("onAnimationEnd(forward)", endForward, this.endForward);
+ assertEquals("onAnimationEnd(reverse)", endReverse, this.endReverse);
+ assertEquals("onAnimationCancel()", cancel, this.cancel);
+ assertEquals("onAnimationRepeat()", repeat, this.repeat);
+ assertEquals("onAnimationPause()", pause, this.pause);
+ assertEquals("onAnimationResume()", resume, this.resume);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation, boolean isReverse) {
+ if (isReverse) {
+ startReverse++;
+ } else {
+ startForward++;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ if (isReverse) {
+ endReverse++;
+ } else {
+ endForward++;
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ startNoParam++;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ endNoParam++;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ cancel++;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ repeat++;
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ pause++;
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ resume++;
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index dee0a3ecdbe0..a53d57f0383c 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -40,6 +40,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -1067,6 +1069,64 @@ public class ValueAnimatorTests {
});
}
+ @Test
+ public void reentrantStart() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(1);
+ a1.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation, boolean isReverse) {
+ a1.start();
+ latch.countDown();
+ }
+ });
+ mActivityRule.runOnUiThread(() -> a1.start());
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread isn't blocked by an infinite loop:
+ mActivityRule.runOnUiThread(() -> {});
+ }
+
+ @Test
+ public void reentrantPause() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(1);
+ a1.addPauseListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationPause(Animator animation) {
+ a1.pause();
+ latch.countDown();
+ }
+ });
+ mActivityRule.runOnUiThread(() -> {
+ a1.start();
+ a1.pause();
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread isn't blocked by an infinite loop:
+ mActivityRule.runOnUiThread(() -> {});
+ }
+
+ @Test
+ public void reentrantResume() throws Throwable {
+ CountDownLatch latch = new CountDownLatch(1);
+ a1.addPauseListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationResume(Animator animation) {
+ a1.resume();
+ latch.countDown();
+ }
+ });
+ mActivityRule.runOnUiThread(() -> {
+ a1.start();
+ a1.pause();
+ a1.resume();
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+ // Make sure that the UI thread isn't blocked by an infinite loop:
+ mActivityRule.runOnUiThread(() -> {});
+ }
+
class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener {
boolean wasRunning = false;
long firstRunningFrameTime = -1;
diff --git a/core/tests/coretests/src/android/animation/ViewPropertyAnimatorTest.java b/core/tests/coretests/src/android/animation/ViewPropertyAnimatorTest.java
index 81cd4da4f425..8cc88ea230a1 100644
--- a/core/tests/coretests/src/android/animation/ViewPropertyAnimatorTest.java
+++ b/core/tests/coretests/src/android/animation/ViewPropertyAnimatorTest.java
@@ -135,11 +135,15 @@ public class ViewPropertyAnimatorTest {
* @throws Exception
*/
@Before
- public void setUp() throws Exception {
+ public void setUp() throws Throwable {
final BasicAnimatorActivity activity = mActivityRule.getActivity();
Button button = activity.findViewById(R.id.animatingButton);
mAnimator = button.animate().x(100).y(100);
+ mActivityRule.runOnUiThread(() -> {
+ mAnimator.start();
+ mAnimator.cancel();
+ });
// mListener is the main testing mechanism of this file. The asserts of each test
// are embedded in the listener callbacks that it implements.