diff options
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. |