1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.animation;
18 
19 import android.view.Choreographer;
20 
21 import java.util.ArrayList;
22 
23 /**
24  * This custom, static handler handles the timing pulse that is shared by all active
25  * ValueAnimators. This approach ensures that the setting of animation values will happen on the
26  * same thread that animations start on, and that all animations will share the same times for
27  * calculating their values, which makes synchronizing animations possible.
28  *
29  * The handler uses the Choreographer by default for doing periodic callbacks. A custom
30  * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
31  * may be independent of UI frame update. This could be useful in testing.
32  */
33 class AnimationHandler {
34     /**
35      * Callbacks that receives notifications for animation timing
36      */
37     interface AnimationFrameCallback {
38         /**
39          * Run animation based on the frame time.
40          * @param frameTime The frame start time
41          */
doAnimationFrame(long frameTime)42         boolean doAnimationFrame(long frameTime);
43     }
44 
45     /**
46      * This method notifies all the on-going animations of the new frame, so that
47      * they can update animation values as needed.
48      */
onAnimationFrame(long frameTime)49     void onAnimationFrame(long frameTime) {
50         AnimationHandler.this.doAnimationFrame(frameTime);
51         if (mAnimationCallbacks.size() > 0) {
52             mProvider.postFrameCallback();
53         }
54     }
55     /**
56      * Internal per-thread collections used to avoid set collisions as animations start and end
57      * while being processed.
58      */
59 
60     public static final ThreadLocal<AnimationHandler> sAnimationHandler = new ThreadLocal<>();
61     private static AnimationHandler sTestHandler = null;
62     private final AnimationFrameCallbackProvider mProvider;
63     private final ArrayList<AnimationFrameCallback> mAnimationCallbacks = new ArrayList<>();
64     boolean mListDirty = false;
65 
AnimationHandler(AnimationFrameCallbackProvider provider)66     AnimationHandler(AnimationFrameCallbackProvider provider) {
67         if (provider == null) {
68             mProvider = new FrameCallbackProvider16();
69         } else {
70             mProvider = provider;
71         }
72     }
73 
getInstance()74     public static AnimationHandler getInstance() {
75         if (sTestHandler != null) {
76             return sTestHandler;
77         }
78         if (sAnimationHandler.get() == null) {
79             sAnimationHandler.set(new AnimationHandler(null));
80         }
81         return sAnimationHandler.get();
82     }
83 
setTestHandler(AnimationHandler handler)84     static void setTestHandler(AnimationHandler handler) {
85         sTestHandler = handler;
86     }
87 
setFrameDelay(long frameDelay)88     void setFrameDelay(long frameDelay) {
89         mProvider.setFrameDelay(frameDelay);
90     }
91 
getFrameDelay()92     long getFrameDelay() {
93         return mProvider.getFrameDelay();
94     }
95 
96     /**
97      * Register to get a callback on the next frame after the delay.
98      */
addAnimationFrameCallback(final AnimationFrameCallback callback)99     void addAnimationFrameCallback(final AnimationFrameCallback callback) {
100         if (mAnimationCallbacks.size() == 0) {
101             mProvider.postFrameCallback();
102         }
103         if (!mAnimationCallbacks.contains(callback)) {
104             mAnimationCallbacks.add(callback);
105         }
106         mProvider.onNewCallbackAdded(callback);
107     }
108 
109     /**
110      * Removes the given callback from the list, so it will no longer be called for frame related
111      * timing.
112      */
removeCallback(AnimationFrameCallback callback)113     public void removeCallback(AnimationFrameCallback callback) {
114         int id = mAnimationCallbacks.indexOf(callback);
115         if (id >= 0) {
116             mAnimationCallbacks.set(id, null);
117             mListDirty = true;
118         }
119     }
120 
autoCancelBasedOn(ObjectAnimator objectAnimator)121     void autoCancelBasedOn(ObjectAnimator objectAnimator) {
122         for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
123             AnimationFrameCallback cb = mAnimationCallbacks.get(i);
124             if (cb == null) {
125                 continue;
126             }
127             if (objectAnimator.shouldAutoCancel(cb)) {
128                 ((Animator) mAnimationCallbacks.get(i)).cancel();
129             }
130         }
131     }
132 
doAnimationFrame(long frameTime)133     private void doAnimationFrame(long frameTime) {
134         for (int i = 0; i < mAnimationCallbacks.size(); i++) {
135             final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
136             if (callback == null) {
137                 continue;
138             }
139             callback.doAnimationFrame(frameTime);
140         }
141         cleanUpList();
142     }
143 
cleanUpList()144     private void cleanUpList() {
145         if (mListDirty) {
146             for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
147                 if (mAnimationCallbacks.get(i) == null) {
148                     mAnimationCallbacks.remove(i);
149                 }
150             }
151             mListDirty = false;
152         }
153     }
154 
getCallbackSize()155     private int getCallbackSize() {
156         int count = 0;
157         int size = mAnimationCallbacks.size();
158         for (int i = size - 1; i >= 0; i--) {
159             if (mAnimationCallbacks.get(i) != null) {
160                 count++;
161             }
162         }
163         return count;
164     }
165 
166     /**
167      * Return the number of callbacks that have registered for frame callbacks.
168      */
getAnimationCount()169     public static int getAnimationCount() {
170         AnimationHandler handler = AnimationHandler.getInstance();
171         if (handler == null) {
172             return 0;
173         }
174         return handler.getCallbackSize();
175     }
176 
177     /**
178      * Default provider of timing pulse that uses Choreographer for frame callbacks.
179      */
180     private class FrameCallbackProvider16 implements AnimationFrameCallbackProvider,
181             Choreographer.FrameCallback {
182 
FrameCallbackProvider16()183         FrameCallbackProvider16() {
184         }
185 
186         @Override
doFrame(long frameTimeNanos)187         public void doFrame(long frameTimeNanos) {
188             onAnimationFrame(frameTimeNanos / 1000000);
189         }
190 
191         @Override
onNewCallbackAdded(AnimationFrameCallback callback)192         public void onNewCallbackAdded(AnimationFrameCallback callback) {}
193 
194         @Override
postFrameCallback()195         public void postFrameCallback() {
196             Choreographer.getInstance().postFrameCallback(this);
197         }
198 
199         @Override
setFrameDelay(long delay)200         public void setFrameDelay(long delay) {
201             android.animation.ValueAnimator.setFrameDelay(delay);
202         }
203 
204         @Override
getFrameDelay()205         public long getFrameDelay() {
206             return android.animation.ValueAnimator.getFrameDelay();
207         }
208     }
209 
210     /**
211      * The intention for having this interface is to increase the testability of ValueAnimator.
212      * Specifically, we can have a custom implementation of the interface below and provide
213      * timing pulse without using Choreographer. That way we could use any arbitrary interval for
214      * our timing pulse in the tests.
215      */
216     interface AnimationFrameCallbackProvider {
217 
onNewCallbackAdded(AnimationFrameCallback callback)218         void onNewCallbackAdded(AnimationFrameCallback callback);
219 
postFrameCallback()220         void postFrameCallback();
221 
setFrameDelay(long delay)222         void setFrameDelay(long delay);
223 
getFrameDelay()224         long getFrameDelay();
225     }
226 }
227