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
17package android.animation;
18
19import android.os.SystemClock;
20import android.util.ArrayMap;
21import android.view.Choreographer;
22
23import 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 */
37public 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
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     */
75    public void setProvider(AnimationFrameCallbackProvider provider) {
76        if (provider == null) {
77            mProvider = new MyFrameCallbackProvider();
78        } else {
79            mProvider = provider;
80        }
81    }
82
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     */
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     */
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     */
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
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
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     */
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     */
189    public static int getAnimationCount() {
190        AnimationHandler handler = sAnimatorHandler.get();
191        if (handler == null) {
192            return 0;
193        }
194        return handler.getCallbackSize();
195    }
196
197    public static void setFrameDelay(long delay) {
198        getInstance().getProvider().setFrameDelay(delay);
199    }
200
201    public static long getFrameDelay() {
202        return getInstance().getProvider().getFrameDelay();
203    }
204
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
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
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
247        public void postFrameCallback(Choreographer.FrameCallback callback) {
248            mChoreographer.postFrameCallback(callback);
249        }
250
251        @Override
252        public void postCommitCallback(Runnable runnable) {
253            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
254        }
255
256        @Override
257        public long getFrameTime() {
258            return mChoreographer.getFrameTime();
259        }
260
261        @Override
262        public long getFrameDelay() {
263            return Choreographer.getFrameDelay();
264        }
265
266        @Override
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         */
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         */
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 {
310        void postFrameCallback(Choreographer.FrameCallback callback);
311        void postCommitCallback(Runnable runnable);
312        long getFrameTime();
313        long getFrameDelay();
314        void setFrameDelay(long delay);
315    }
316}
317