Home | History | Annotate | Download | only in animation
      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 
     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