| /* |
| * Copyright (C) 2015 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 android.annotation.Nullable; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.view.Choreographer; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| |
| /** |
| * This custom, static handler handles the timing pulse that is shared by all active |
| * ValueAnimators. This approach ensures that the setting of animation values will happen on the |
| * same thread that animations start on, and that all animations will share the same times for |
| * calculating their values, which makes synchronizing animations possible. |
| * |
| * The handler uses the Choreographer by default for doing periodic callbacks. A custom |
| * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that |
| * may be independent of UI frame update. This could be useful in testing. |
| * |
| * @hide |
| */ |
| public class AnimationHandler { |
| |
| private static final String TAG = "AnimationHandler"; |
| private static final boolean LOCAL_LOGV = false; |
| |
| /** |
| * Internal per-thread collections used to avoid set collisions as animations start and end |
| * while being processed. |
| */ |
| private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime = |
| new ArrayMap<>(); |
| private final ArrayList<AnimationFrameCallback> mAnimationCallbacks = |
| new ArrayList<>(); |
| private final ArrayList<AnimationFrameCallback> mCommitCallbacks = |
| new ArrayList<>(); |
| private AnimationFrameCallbackProvider mProvider; |
| |
| // Static flag which allows the pausing behavior to be globally disabled/enabled. |
| private static boolean sAnimatorPausingEnabled = isPauseBgAnimationsEnabledInSystemProperties(); |
| |
| // Static flag which prevents the system property from overriding sAnimatorPausingEnabled field. |
| private static boolean sOverrideAnimatorPausingSystemProperty = false; |
| |
| /** |
| * This paused list is used to store animators forcibly paused when the activity |
| * went into the background (to avoid unnecessary background processing work). |
| * These animators should be resume()'d when the activity returns to the foreground. |
| */ |
| private final ArrayList<Animator> mPausedAnimators = new ArrayList<>(); |
| |
| /** |
| * This structure is used to store the currently active objects (ViewRootImpls or |
| * WallpaperService.Engines) in the process. Each of these objects sends a request to |
| * AnimationHandler when it goes into the background (request to pause) or foreground |
| * (request to resume). Because all animators are managed by AnimationHandler on the same |
| * thread, it should only ever pause animators when *all* requestors are in the background. |
| * This list tracks the background/foreground state of all requestors and only ever |
| * pauses animators when all items are in the background (false). To simplify, we only ever |
| * store visible (foreground) requestors; if the set size reaches zero, there are no |
| * objects in the foreground and it is time to pause animators. |
| */ |
| private final ArrayList<WeakReference<Object>> mAnimatorRequestors = new ArrayList<>(); |
| |
| private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { |
| @Override |
| public void doFrame(long frameTimeNanos) { |
| doAnimationFrame(getProvider().getFrameTime()); |
| if (mAnimationCallbacks.size() > 0) { |
| getProvider().postFrameCallback(this); |
| } |
| } |
| }; |
| |
| public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>(); |
| private static AnimationHandler sTestHandler = null; |
| private boolean mListDirty = false; |
| |
| public static AnimationHandler getInstance() { |
| if (sTestHandler != null) { |
| return sTestHandler; |
| } |
| if (sAnimatorHandler.get() == null) { |
| sAnimatorHandler.set(new AnimationHandler()); |
| } |
| return sAnimatorHandler.get(); |
| } |
| |
| /** |
| * Sets an instance that will be returned by {@link #getInstance()} on every thread. |
| * @return the previously active test handler, if any. |
| * @hide |
| */ |
| public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) { |
| AnimationHandler oldHandler = sTestHandler; |
| sTestHandler = handler; |
| return oldHandler; |
| } |
| |
| /** |
| * System property that controls the behavior of pausing infinite animators when an app |
| * is moved to the background. |
| * |
| * @return the value of 'framework.pause_bg_animations.enabled' system property |
| */ |
| private static boolean isPauseBgAnimationsEnabledInSystemProperties() { |
| if (sOverrideAnimatorPausingSystemProperty) return sAnimatorPausingEnabled; |
| return SystemProperties |
| .getBoolean("framework.pause_bg_animations.enabled", true); |
| } |
| |
| /** |
| * Disable the default behavior of pausing infinite animators when |
| * apps go into the background. |
| * |
| * @param enable Enable (default behavior) or disable background pausing behavior. |
| */ |
| public static void setAnimatorPausingEnabled(boolean enable) { |
| sAnimatorPausingEnabled = enable; |
| } |
| |
| /** |
| * Prevents the setAnimatorPausingEnabled behavior from being overridden |
| * by the 'framework.pause_bg_animations.enabled' system property value. |
| * |
| * This is for testing purposes only. |
| * |
| * @param enable Enable or disable (default behavior) overriding the system |
| * property. |
| */ |
| public static void setOverrideAnimatorPausingSystemProperty(boolean enable) { |
| sOverrideAnimatorPausingSystemProperty = enable; |
| } |
| |
| /** |
| * This is called when a window goes away. We should remove |
| * it from the requestors list to ensure that we are counting requests correctly and not |
| * tracking obsolete+enabled requestors. |
| */ |
| public static void removeRequestor(Object requestor) { |
| getInstance().requestAnimatorsEnabledImpl(false, requestor); |
| if (LOCAL_LOGV) { |
| Log.v(TAG, "removeRequestor for " + requestor); |
| } |
| } |
| |
| /** |
| * This method is called from ViewRootImpl or WallpaperService when either a window is no |
| * longer visible (enable == false) or when a window becomes visible (enable == true). |
| * If animators are not properly disabled when activities are backgrounded, it can lead to |
| * unnecessary processing, particularly for infinite animators, as the system will continue |
| * to pulse timing events even though the results are not visible. As a workaround, we |
| * pause all un-paused infinite animators, and resume them when any window in the process |
| * becomes visible. |
| */ |
| public static void requestAnimatorsEnabled(boolean enable, Object requestor) { |
| getInstance().requestAnimatorsEnabledImpl(enable, requestor); |
| } |
| |
| private void requestAnimatorsEnabledImpl(boolean enable, Object requestor) { |
| boolean wasEmpty = mAnimatorRequestors.isEmpty(); |
| setAnimatorPausingEnabled(isPauseBgAnimationsEnabledInSystemProperties()); |
| synchronized (mAnimatorRequestors) { |
| // Only store WeakRef objects to avoid leaks |
| if (enable) { |
| // First, check whether such a reference is already on the list |
| WeakReference<Object> weakRef = null; |
| for (int i = mAnimatorRequestors.size() - 1; i >= 0; --i) { |
| WeakReference<Object> ref = mAnimatorRequestors.get(i); |
| Object referent = ref.get(); |
| if (referent == requestor) { |
| weakRef = ref; |
| } else if (referent == null) { |
| // Remove any reference that has been cleared |
| mAnimatorRequestors.remove(i); |
| } |
| } |
| if (weakRef == null) { |
| weakRef = new WeakReference<>(requestor); |
| mAnimatorRequestors.add(weakRef); |
| } |
| } else { |
| for (int i = mAnimatorRequestors.size() - 1; i >= 0; --i) { |
| WeakReference<Object> ref = mAnimatorRequestors.get(i); |
| Object referent = ref.get(); |
| if (referent == requestor || referent == null) { |
| // remove requested item or item that has been cleared |
| mAnimatorRequestors.remove(i); |
| } |
| } |
| // If a reference to the requestor wasn't in the list, nothing to remove |
| } |
| } |
| if (!sAnimatorPausingEnabled) { |
| // Resume any animators that have been paused in the meantime, otherwise noop |
| // Leave logic above so that if pausing gets re-enabled, the state of the requestors |
| // list is valid |
| resumeAnimators(); |
| return; |
| } |
| boolean isEmpty = mAnimatorRequestors.isEmpty(); |
| if (wasEmpty != isEmpty) { |
| // only paused/resume animators if there was a visibility change |
| if (!isEmpty) { |
| // If any requestors are enabled, resume currently paused animators |
| resumeAnimators(); |
| } else { |
| // Wait before pausing to avoid thrashing animator state for temporary backgrounding |
| Choreographer.getInstance().postFrameCallbackDelayed(mPauser, |
| Animator.getBackgroundPauseDelay()); |
| } |
| } |
| if (LOCAL_LOGV) { |
| Log.v(TAG, (enable ? "enable" : "disable") + " animators for " + requestor |
| + " with pauseDelay of " + Animator.getBackgroundPauseDelay()); |
| for (int i = 0; i < mAnimatorRequestors.size(); ++i) { |
| Log.v(TAG, "animatorRequestors " + i + " = " |
| + mAnimatorRequestors.get(i) + " with referent " |
| + mAnimatorRequestors.get(i).get()); |
| } |
| } |
| } |
| |
| private void resumeAnimators() { |
| Choreographer.getInstance().removeFrameCallback(mPauser); |
| for (int i = mPausedAnimators.size() - 1; i >= 0; --i) { |
| mPausedAnimators.get(i).resume(); |
| } |
| mPausedAnimators.clear(); |
| } |
| |
| private Choreographer.FrameCallback mPauser = frameTimeNanos -> { |
| if (mAnimatorRequestors.size() > 0) { |
| // something enabled animators since this callback was scheduled - bail |
| return; |
| } |
| for (int i = 0; i < mAnimationCallbacks.size(); ++i) { |
| AnimationFrameCallback callback = mAnimationCallbacks.get(i); |
| if (callback instanceof Animator) { |
| Animator animator = ((Animator) callback); |
| if (animator.getTotalDuration() == Animator.DURATION_INFINITE |
| && !animator.isPaused()) { |
| mPausedAnimators.add(animator); |
| animator.pause(); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * By default, the Choreographer is used to provide timing for frame callbacks. A custom |
| * provider can be used here to provide different timing pulse. |
| */ |
| public void setProvider(AnimationFrameCallbackProvider provider) { |
| if (provider == null) { |
| mProvider = new MyFrameCallbackProvider(); |
| } else { |
| mProvider = provider; |
| } |
| } |
| |
| private AnimationFrameCallbackProvider getProvider() { |
| if (mProvider == null) { |
| mProvider = new MyFrameCallbackProvider(); |
| } |
| return mProvider; |
| } |
| |
| /** |
| * Register to get a callback on the next frame after the delay. |
| */ |
| public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { |
| if (mAnimationCallbacks.size() == 0) { |
| getProvider().postFrameCallback(mFrameCallback); |
| } |
| if (!mAnimationCallbacks.contains(callback)) { |
| mAnimationCallbacks.add(callback); |
| } |
| |
| if (delay > 0) { |
| mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay)); |
| } |
| } |
| |
| /** |
| * Register to get a one shot callback for frame commit timing. Frame commit timing is the |
| * time *after* traversals are done, as opposed to the animation frame timing, which is |
| * before any traversals. This timing can be used to adjust the start time of an animation |
| * when expensive traversals create big delta between the animation frame timing and the time |
| * that animation is first shown on screen. |
| * |
| * Note this should only be called when the animation has already registered to receive |
| * animation frame callbacks. This callback will be guaranteed to happen *after* the next |
| * animation frame callback. |
| */ |
| public void addOneShotCommitCallback(final AnimationFrameCallback callback) { |
| if (!mCommitCallbacks.contains(callback)) { |
| mCommitCallbacks.add(callback); |
| } |
| } |
| |
| /** |
| * Removes the given callback from the list, so it will no longer be called for frame related |
| * timing. |
| */ |
| public void removeCallback(AnimationFrameCallback callback) { |
| mCommitCallbacks.remove(callback); |
| mDelayedCallbackStartTime.remove(callback); |
| int id = mAnimationCallbacks.indexOf(callback); |
| if (id >= 0) { |
| mAnimationCallbacks.set(id, null); |
| mListDirty = true; |
| } |
| } |
| |
| private void doAnimationFrame(long frameTime) { |
| long currentTime = SystemClock.uptimeMillis(); |
| final int size = mAnimationCallbacks.size(); |
| for (int i = 0; i < size; i++) { |
| final AnimationFrameCallback callback = mAnimationCallbacks.get(i); |
| if (callback == null) { |
| continue; |
| } |
| if (isCallbackDue(callback, currentTime)) { |
| callback.doAnimationFrame(frameTime); |
| if (mCommitCallbacks.contains(callback)) { |
| getProvider().postCommitCallback(new Runnable() { |
| @Override |
| public void run() { |
| commitAnimationFrame(callback, getProvider().getFrameTime()); |
| } |
| }); |
| } |
| } |
| } |
| cleanUpList(); |
| } |
| |
| private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) { |
| if (!mDelayedCallbackStartTime.containsKey(callback) && |
| mCommitCallbacks.contains(callback)) { |
| callback.commitAnimationFrame(frameTime); |
| mCommitCallbacks.remove(callback); |
| } |
| } |
| |
| /** |
| * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay |
| * so that they can start getting frame callbacks. |
| * |
| * @return true if they have passed the initial delay or have no delay, false otherwise. |
| */ |
| private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) { |
| Long startTime = mDelayedCallbackStartTime.get(callback); |
| if (startTime == null) { |
| return true; |
| } |
| if (startTime < currentTime) { |
| mDelayedCallbackStartTime.remove(callback); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Return the number of callbacks that have registered for frame callbacks. |
| */ |
| public static int getAnimationCount() { |
| AnimationHandler handler = sTestHandler; |
| if (handler == null) { |
| handler = sAnimatorHandler.get(); |
| } |
| if (handler == null) { |
| return 0; |
| } |
| return handler.getCallbackSize(); |
| } |
| |
| public static void setFrameDelay(long delay) { |
| getInstance().getProvider().setFrameDelay(delay); |
| } |
| |
| public static long getFrameDelay() { |
| return getInstance().getProvider().getFrameDelay(); |
| } |
| |
| void autoCancelBasedOn(ObjectAnimator objectAnimator) { |
| for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) { |
| AnimationFrameCallback cb = mAnimationCallbacks.get(i); |
| if (cb == null) { |
| continue; |
| } |
| if (objectAnimator.shouldAutoCancel(cb)) { |
| ((Animator) mAnimationCallbacks.get(i)).cancel(); |
| } |
| } |
| } |
| |
| private void cleanUpList() { |
| if (mListDirty) { |
| for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) { |
| if (mAnimationCallbacks.get(i) == null) { |
| mAnimationCallbacks.remove(i); |
| } |
| } |
| mListDirty = false; |
| } |
| } |
| |
| private int getCallbackSize() { |
| int count = 0; |
| int size = mAnimationCallbacks.size(); |
| for (int i = size - 1; i >= 0; i--) { |
| if (mAnimationCallbacks.get(i) != null) { |
| count++; |
| } |
| } |
| return count; |
| } |
| |
| /** |
| * Default provider of timing pulse that uses Choreographer for frame callbacks. |
| */ |
| private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider { |
| |
| final Choreographer mChoreographer = Choreographer.getInstance(); |
| |
| @Override |
| public void postFrameCallback(Choreographer.FrameCallback callback) { |
| mChoreographer.postFrameCallback(callback); |
| } |
| |
| @Override |
| public void postCommitCallback(Runnable runnable) { |
| mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null); |
| } |
| |
| @Override |
| public long getFrameTime() { |
| return mChoreographer.getFrameTime(); |
| } |
| |
| @Override |
| public long getFrameDelay() { |
| return Choreographer.getFrameDelay(); |
| } |
| |
| @Override |
| public void setFrameDelay(long delay) { |
| Choreographer.setFrameDelay(delay); |
| } |
| } |
| |
| /** |
| * Callbacks that receives notifications for animation timing and frame commit timing. |
| * @hide |
| */ |
| public interface AnimationFrameCallback { |
| /** |
| * Run animation based on the frame time. |
| * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time |
| * base. |
| * @return if the animation has finished. |
| */ |
| boolean doAnimationFrame(long frameTime); |
| |
| /** |
| * This notifies the callback of frame commit time. Frame commit time is the time after |
| * traversals happen, as opposed to the normal animation frame time that is before |
| * traversals. This is used to compensate expensive traversals that happen as the |
| * animation starts. When traversals take a long time to complete, the rendering of the |
| * initial frame will be delayed (by a long time). But since the startTime of the |
| * animation is set before the traversal, by the time of next frame, a lot of time would |
| * have passed since startTime was set, the animation will consequently skip a few frames |
| * to respect the new frameTime. By having the commit time, we can adjust the start time to |
| * when the first frame was drawn (after any expensive traversals) so that no frames |
| * will be skipped. |
| * |
| * @param frameTime The frame time after traversals happen, if any, in the |
| * {@link SystemClock#uptimeMillis()} time base. |
| */ |
| void commitAnimationFrame(long frameTime); |
| } |
| |
| /** |
| * The intention for having this interface is to increase the testability of ValueAnimator. |
| * Specifically, we can have a custom implementation of the interface below and provide |
| * timing pulse without using Choreographer. That way we could use any arbitrary interval for |
| * our timing pulse in the tests. |
| * |
| * @hide |
| */ |
| public interface AnimationFrameCallbackProvider { |
| void postFrameCallback(Choreographer.FrameCallback callback); |
| void postCommitCallback(Runnable runnable); |
| long getFrameTime(); |
| long getFrameDelay(); |
| void setFrameDelay(long delay); |
| } |
| } |