1 /*
2  * Copyright (C) 2015 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 android.animation;
18 
19 import android.os.SystemClock;
20 import android.util.ArrayMap;
21 import android.view.Choreographer;
22 
23 import java.util.ArrayList;
24 
25 /**
26  * This custom, static handler handles the timing pulse that is shared by all active
27  * ValueAnimators. This approach ensures that the setting of animation values will happen on the
28  * same thread that animations start on, and that all animations will share the same times for
29  * calculating their values, which makes synchronizing animations possible.
30  *
31  * The handler uses the Choreographer by default for doing periodic callbacks. A custom
32  * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
33  * may be independent of UI frame update. This could be useful in testing.
34  *
35  * @hide
36  */
37 public class AnimationHandler {
38     /**
39      * Internal per-thread collections used to avoid set collisions as animations start and end
40      * while being processed.
41      * @hide
42      */
43     private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
44             new ArrayMap<>();
45     private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
46             new ArrayList<>();
47     private final ArrayList<AnimationFrameCallback> mCommitCallbacks =
48             new ArrayList<>();
49     private AnimationFrameCallbackProvider mProvider;
50 
51     private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
52         @Override
53         public void doFrame(long frameTimeNanos) {
54             doAnimationFrame(getProvider().getFrameTime());
55             if (mAnimationCallbacks.size() > 0) {
56                 getProvider().postFrameCallback(this);
57             }
58         }
59     };
60 
61     public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
62     private boolean mListDirty = false;
63 
getInstance()64     public static AnimationHandler getInstance() {
65         if (sAnimatorHandler.get() == null) {
66             sAnimatorHandler.set(new AnimationHandler());
67         }
68         return sAnimatorHandler.get();
69     }
70 
71     /**
72      * By default, the Choreographer is used to provide timing for frame callbacks. A custom
73      * provider can be used here to provide different timing pulse.
74      */
setProvider(AnimationFrameCallbackProvider provider)75     public void setProvider(AnimationFrameCallbackProvider provider) {
76         if (provider == null) {
77             mProvider = new MyFrameCallbackProvider();
78         } else {
79             mProvider = provider;
80         }
81     }
82 
getProvider()83     private AnimationFrameCallbackProvider getProvider() {
84         if (mProvider == null) {
85             mProvider = new MyFrameCallbackProvider();
86         }
87         return mProvider;
88     }
89 
90     /**
91      * Register to get a callback on the next frame after the delay.
92      */
addAnimationFrameCallback(final AnimationFrameCallback callback, long delay)93     public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
94         if (mAnimationCallbacks.size() == 0) {
95             getProvider().postFrameCallback(mFrameCallback);
96         }
97         if (!mAnimationCallbacks.contains(callback)) {
98             mAnimationCallbacks.add(callback);
99         }
100 
101         if (delay > 0) {
102             mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
103         }
104     }
105 
106     /**
107      * Register to get a one shot callback for frame commit timing. Frame commit timing is the
108      * time *after* traversals are done, as opposed to the animation frame timing, which is
109      * before any traversals. This timing can be used to adjust the start time of an animation
110      * when expensive traversals create big delta between the animation frame timing and the time
111      * that animation is first shown on screen.
112      *
113      * Note this should only be called when the animation has already registered to receive
114      * animation frame callbacks. This callback will be guaranteed to happen *after* the next
115      * animation frame callback.
116      */
addOneShotCommitCallback(final AnimationFrameCallback callback)117     public void addOneShotCommitCallback(final AnimationFrameCallback callback) {
118         if (!mCommitCallbacks.contains(callback)) {
119             mCommitCallbacks.add(callback);
120         }
121     }
122 
123     /**
124      * Removes the given callback from the list, so it will no longer be called for frame related
125      * timing.
126      */
removeCallback(AnimationFrameCallback callback)127     public void removeCallback(AnimationFrameCallback callback) {
128         mCommitCallbacks.remove(callback);
129         mDelayedCallbackStartTime.remove(callback);
130         int id = mAnimationCallbacks.indexOf(callback);
131         if (id >= 0) {
132             mAnimationCallbacks.set(id, null);
133             mListDirty = true;
134         }
135     }
136 
doAnimationFrame(long frameTime)137     private void doAnimationFrame(long frameTime) {
138         int size = mAnimationCallbacks.size();
139         long currentTime = SystemClock.uptimeMillis();
140         for (int i = 0; i < size; i++) {
141             final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
142             if (callback == null) {
143                 continue;
144             }
145             if (isCallbackDue(callback, currentTime)) {
146                 callback.doAnimationFrame(frameTime);
147                 if (mCommitCallbacks.contains(callback)) {
148                     getProvider().postCommitCallback(new Runnable() {
149                         @Override
150                         public void run() {
151                             commitAnimationFrame(callback, getProvider().getFrameTime());
152                         }
153                     });
154                 }
155             }
156         }
157         cleanUpList();
158     }
159 
commitAnimationFrame(AnimationFrameCallback callback, long frameTime)160     private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
161         if (!mDelayedCallbackStartTime.containsKey(callback) &&
162                 mCommitCallbacks.contains(callback)) {
163             callback.commitAnimationFrame(frameTime);
164             mCommitCallbacks.remove(callback);
165         }
166     }
167 
168     /**
169      * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
170      * so that they can start getting frame callbacks.
171      *
172      * @return true if they have passed the initial delay or have no delay, false otherwise.
173      */
isCallbackDue(AnimationFrameCallback callback, long currentTime)174     private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
175         Long startTime = mDelayedCallbackStartTime.get(callback);
176         if (startTime == null) {
177             return true;
178         }
179         if (startTime < currentTime) {
180             mDelayedCallbackStartTime.remove(callback);
181             return true;
182         }
183         return false;
184     }
185 
186     /**
187      * Return the number of callbacks that have registered for frame callbacks.
188      */
getAnimationCount()189     public static int getAnimationCount() {
190         AnimationHandler handler = sAnimatorHandler.get();
191         if (handler == null) {
192             return 0;
193         }
194         return handler.getCallbackSize();
195     }
196 
setFrameDelay(long delay)197     public static void setFrameDelay(long delay) {
198         getInstance().getProvider().setFrameDelay(delay);
199     }
200 
getFrameDelay()201     public static long getFrameDelay() {
202         return getInstance().getProvider().getFrameDelay();
203     }
204 
autoCancelBasedOn(ObjectAnimator objectAnimator)205     void autoCancelBasedOn(ObjectAnimator objectAnimator) {
206         for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
207             AnimationFrameCallback cb = mAnimationCallbacks.get(i);
208             if (cb == null) {
209                 continue;
210             }
211             if (objectAnimator.shouldAutoCancel(cb)) {
212                 ((Animator) mAnimationCallbacks.get(i)).cancel();
213             }
214         }
215     }
216 
cleanUpList()217     private void cleanUpList() {
218         if (mListDirty) {
219             for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
220                 if (mAnimationCallbacks.get(i) == null) {
221                     mAnimationCallbacks.remove(i);
222                 }
223             }
224             mListDirty = false;
225         }
226     }
227 
getCallbackSize()228     private int getCallbackSize() {
229         int count = 0;
230         int size = mAnimationCallbacks.size();
231         for (int i = size - 1; i >= 0; i--) {
232             if (mAnimationCallbacks.get(i) != null) {
233                 count++;
234             }
235         }
236         return count;
237     }
238 
239     /**
240      * Default provider of timing pulse that uses Choreographer for frame callbacks.
241      */
242     private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
243 
244         final Choreographer mChoreographer = Choreographer.getInstance();
245 
246         @Override
postFrameCallback(Choreographer.FrameCallback callback)247         public void postFrameCallback(Choreographer.FrameCallback callback) {
248             mChoreographer.postFrameCallback(callback);
249         }
250 
251         @Override
postCommitCallback(Runnable runnable)252         public void postCommitCallback(Runnable runnable) {
253             mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
254         }
255 
256         @Override
getFrameTime()257         public long getFrameTime() {
258             return mChoreographer.getFrameTime();
259         }
260 
261         @Override
getFrameDelay()262         public long getFrameDelay() {
263             return Choreographer.getFrameDelay();
264         }
265 
266         @Override
setFrameDelay(long delay)267         public void setFrameDelay(long delay) {
268             Choreographer.setFrameDelay(delay);
269         }
270     }
271 
272     /**
273      * Callbacks that receives notifications for animation timing and frame commit timing.
274      */
275     interface AnimationFrameCallback {
276         /**
277          * Run animation based on the frame time.
278          * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
279          *                  base.
280          */
doAnimationFrame(long frameTime)281         void doAnimationFrame(long frameTime);
282 
283         /**
284          * This notifies the callback of frame commit time. Frame commit time is the time after
285          * traversals happen, as opposed to the normal animation frame time that is before
286          * traversals. This is used to compensate expensive traversals that happen as the
287          * animation starts. When traversals take a long time to complete, the rendering of the
288          * initial frame will be delayed (by a long time). But since the startTime of the
289          * animation is set before the traversal, by the time of next frame, a lot of time would
290          * have passed since startTime was set, the animation will consequently skip a few frames
291          * to respect the new frameTime. By having the commit time, we can adjust the start time to
292          * when the first frame was drawn (after any expensive traversals) so that no frames
293          * will be skipped.
294          *
295          * @param frameTime The frame time after traversals happen, if any, in the
296          *                  {@link SystemClock#uptimeMillis()} time base.
297          */
commitAnimationFrame(long frameTime)298         void commitAnimationFrame(long frameTime);
299     }
300 
301     /**
302      * The intention for having this interface is to increase the testability of ValueAnimator.
303      * Specifically, we can have a custom implementation of the interface below and provide
304      * timing pulse without using Choreographer. That way we could use any arbitrary interval for
305      * our timing pulse in the tests.
306      *
307      * @hide
308      */
309     public interface AnimationFrameCallbackProvider {
postFrameCallback(Choreographer.FrameCallback callback)310         void postFrameCallback(Choreographer.FrameCallback callback);
postCommitCallback(Runnable runnable)311         void postCommitCallback(Runnable runnable);
getFrameTime()312         long getFrameTime();
getFrameDelay()313         long getFrameDelay();
setFrameDelay(long delay)314         void setFrameDelay(long delay);
315     }
316 }
317