diff options
6 files changed, 115 insertions, 3 deletions
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index d84a4c12a2cd..d5b2f980e1a6 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -384,6 +384,12 @@ public class AnimationHandler { }); } + void removePendingEndAnimationCallback(Runnable notifyEndAnimation) { + if (mPendingEndAnimationListeners != null) { + mPendingEndAnimationListeners.remove(notifyEndAnimation); + } + } + private void doAnimationFrame(long frameTime) { long currentTime = SystemClock.uptimeMillis(); final int size = mAnimationCallbacks.size(); diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 4bf87f91cb2f..e62cd556af9a 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -82,6 +82,12 @@ public abstract class Animator implements Cloneable { static boolean sPostNotifyEndListenerEnabled; /** + * If {@link #sPostNotifyEndListenerEnabled} is enabled, it will be set when the end callback + * is scheduled. It is cleared when it runs or finishes immediately, e.g. cancel. + */ + private Runnable mPendingEndCallback; + + /** * 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. @@ -660,10 +666,33 @@ public abstract class Animator implements Cloneable { } } + /** + * This is called when the animator needs to finish immediately. This is usually no-op unless + * {@link #sPostNotifyEndListenerEnabled} is enabled and a finish request calls around the last + * animation frame. + * + * @param notifyListeners Whether to invoke {@link AnimatorListener#onAnimationEnd}. + * @return {@code true} if the pending listeners are removed. + */ + boolean consumePendingEndListeners(boolean notifyListeners) { + if (mPendingEndCallback == null) { + return false; + } + AnimationHandler.getInstance().removePendingEndAnimationCallback(mPendingEndCallback); + mPendingEndCallback = null; + if (notifyListeners) { + notifyEndListeners(false /* isReversing */); + } + return true; + } + void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) { if (postNotifyEndListener) { - AnimationHandler.getInstance().postEndAnimationCallback( - () -> completeEndAnimation(isReversing, "postNotifyAnimEnd")); + mPendingEndCallback = () -> { + completeEndAnimation(isReversing, "postNotifyAnimEnd"); + mPendingEndCallback = null; + }; + AnimationHandler.getInstance().postEndAnimationCallback(mPendingEndCallback); } else { completeEndAnimation(isReversing, "notifyAnimEnd"); } diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 78566d2fe98d..4a07de0410ae 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -423,6 +423,13 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim notifyListeners(AnimatorCaller.ON_CANCEL, false); callOnPlayingSet(Animator::cancel); mPlayingSet.clear(); + // If the end callback is pending, invoke the end callbacks of the animator nodes before + // ending this set. Pass notifyListeners=false because this endAnimation will do that. + if (consumePendingEndListeners(false /* notifyListeners */)) { + for (int i = mNodeMap.size() - 1; i >= 0; i--) { + mNodeMap.keyAt(i).consumePendingEndListeners(true /* notifyListeners */); + } + } endAnimation(); } } diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 492c2ffc561f..fbcc73ea59e7 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1182,6 +1182,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio // If end has already been requested, through a previous end() or cancel() call, no-op // until animation starts again. if (mAnimationEndRequested) { + consumePendingEndListeners(true /* notifyListeners */); return; } diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java index 33a46d0fde17..6d86bd209a3d 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.os.Handler; +import android.os.Looper; import android.util.PollingCheck; import android.view.View; @@ -486,6 +488,42 @@ public class AnimatorSetCallsTest { }); } + @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[2]; + final AnimatorSet set = new AnimatorSet(); + final ValueAnimatorTests.MyListener asListener = new ValueAnimatorTests.MyListener(); + final ValueAnimatorTests.MyListener vaListener = new ValueAnimatorTests.MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + set.cancel(); + endCalledRightAfterCancel[0] = vaListener.endCalled; + endCalledRightAfterCancel[1] = asListener.endCalled; + endLatch.countDown(); + }); + } + }); + set.addListener(asListener); + va.addListener(vaListener); + set.play(va); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(set::start); + assertTrue(endLatch.await(1, TimeUnit.SECONDS)); + assertTrue(endCalledRightAfterCancel[0]); + assertTrue(endCalledRightAfterCancel[1]); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) { final boolean[] value = new boolean[1]; PollingCheck.waitFor(() -> { diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java index 04698465e971..a55909f0c193 100644 --- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java +++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java @@ -922,6 +922,36 @@ public class ValueAnimatorTests { } @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[1]; + final MyListener listener = new MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + va.cancel(); + endCalledRightAfterCancel[0] = listener.endCalled; + endLatch.countDown(); + }); + } + }); + va.addListener(listener); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(va::start); + assertThat(endLatch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(endCalledRightAfterCancel[0]).isTrue(); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + + @Test public void testZeroDuration() throws Throwable { // Run two animators with zero duration, with one running forward and the other one // backward. Check that the animations start and finish with the correct end fractions. @@ -1182,6 +1212,7 @@ public class ValueAnimatorTests { assertEquals(A1_START_VALUE, a1.getAnimatedValue()); }); } + class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener { boolean wasRunning = false; long firstRunningFrameTime = -1; @@ -1207,7 +1238,7 @@ public class ValueAnimatorTests { } } - class MyListener implements Animator.AnimatorListener { + static class MyListener implements Animator.AnimatorListener { boolean startCalled = false; boolean cancelCalled = false; boolean endCalled = false; |