Loading core/java/android/animation/AnimationHandler.java +6 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading core/java/android/animation/Animator.java +31 −2 Original line number Diff line number Diff line Loading @@ -81,6 +81,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 Loading Loading @@ -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"); } Loading core/java/android/animation/AnimatorSet.java +7 −0 Original line number Diff line number Diff line Loading @@ -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(); } } Loading core/java/android/animation/ValueAnimator.java +1 −0 Original line number Diff line number Diff line Loading @@ -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; } Loading core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +38 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(() -> { Loading Loading
core/java/android/animation/AnimationHandler.java +6 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading
core/java/android/animation/Animator.java +31 −2 Original line number Diff line number Diff line Loading @@ -81,6 +81,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 Loading Loading @@ -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"); } Loading
core/java/android/animation/AnimatorSet.java +7 −0 Original line number Diff line number Diff line Loading @@ -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(); } } Loading
core/java/android/animation/ValueAnimator.java +1 −0 Original line number Diff line number Diff line Loading @@ -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; } Loading
core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +38 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(() -> { Loading