Merge "Call pause() and resume() on child animators."

This commit is contained in:
George Mount
2023-02-17 18:56:10 +00:00
committed by Android (Google) Code Review
4 changed files with 378 additions and 82 deletions

View File

@@ -71,6 +71,13 @@ public abstract class Animator implements Cloneable {
*/
private static long sBackgroundPauseDelay = 10000;
/**
* 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;
/**
* Sets the duration for delaying pausing animators when apps go into the background.
* Used by AnimationHandler when requested to pause animators.
@@ -160,14 +167,7 @@ public abstract class Animator implements Cloneable {
public void pause() {
if (isStarted() && !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 +184,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 +443,7 @@ public abstract class Animator implements Cloneable {
if (mPauseListeners != null) {
anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(mPauseListeners);
}
anim.mCachedList = null;
return anim;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
@@ -590,6 +584,70 @@ 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);
}
/**
* 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
@@ -748,4 +806,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);
}
}

View File

@@ -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
@@ -424,24 +425,28 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
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();
}
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) {
@@ -662,6 +667,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
super.pause();
if (!previouslyPaused && mPaused) {
mPauseTime = -1;
callOnPlayingSet(Animator::pause);
}
}
@@ -676,6 +682,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
if (mPauseTime >= 0) {
addAnimationCallback(0);
}
callOnPlayingSet(Animator::resume);
}
}
@@ -751,26 +758,14 @@ 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);
}
notifyListeners(AnimatorCaller.ON_START, 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);
}
notifyListeners(AnimatorCaller.ON_END, inReverse);
}
mStartListenersCalled = false;
}

View File

@@ -1110,24 +1110,14 @@ 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);
}
notifyListeners(AnimatorCaller.ON_START, 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);
}
notifyListeners(AnimatorCaller.ON_END, isReversing);
}
mStartListenersCalled = false;
}
@@ -1224,15 +1214,7 @@ 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);
}
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();
@@ -1435,12 +1417,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;
}
@@ -1494,12 +1471,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
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);
}
}
notifyListeners(AnimatorCaller.ON_REPEAT, false);
}
}
@@ -1697,12 +1669,7 @@ 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);
}
}
callOnList(mUpdateListeners, AnimatorCaller.ON_UPDATE, this, false);
}
@Override

View File

@@ -0,0 +1,251 @@
/*
* 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 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;
@MediumTest
public class AnimatorSetCallsTest {
@Rule
public final ActivityScenarioRule<AnimatorSetActivity> mRule =
new ActivityScenarioRule<>(AnimatorSetActivity.class);
private AnimatorSetActivity mActivity;
private AnimatorSet mSet1;
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);
AnimatorSet set2 = new AnimatorSet();
mListener2 = new CountListener();
set2.addListener(mListener2);
set2.addPauseListener(mListener2);
ObjectAnimator anim = ObjectAnimator.ofFloat(square, "translationX", 0f, 100f);
mListener3 = new CountListener();
anim.addListener(mListener3);
anim.addPauseListener(mListener3);
anim.setDuration(1);
set2.play(anim);
mSet1.play(set2);
});
}
@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
);
}
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(0, startNoParam);
assertEquals(0, endNoParam);
assertEquals(startForward, this.startForward);
assertEquals(startReverse, this.startReverse);
assertEquals(endForward, this.endForward);
assertEquals(endReverse, this.endReverse);
assertEquals(cancel, this.cancel);
assertEquals(repeat, this.repeat);
assertEquals(pause, this.pause);
assertEquals(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++;
}
}
}