Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 25515df0 authored by George Mount's avatar George Mount Committed by Automerger Merge Worker
Browse files

Call pause() and resume() on child animators. am: fd41f0f6

parents badd2a9a fd41f0f6
Loading
Loading
Loading
Loading
+99 −16
Original line number Diff line number Diff line
@@ -71,6 +71,13 @@ 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;

    /**
     * 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);
    }
}
+22 −27
Original line number Diff line number Diff line
@@ -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;
    }
+6 −39
Original line number Diff line number Diff line
@@ -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
+251 −0
Original line number Diff line number Diff line
/*
 * 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++;
        }
    }
}