AnimationHandler.java

/*
 * Copyright (C) 2018 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 androidx.core.animation;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.Choreographer;

import androidx.annotation.RequiresApi;
import androidx.collection.SimpleArrayMap;

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.
 */
class AnimationHandler {
    /**
     * Callbacks that receives notifications for animation timing
     */
    interface AnimationFrameCallback {
        /**
         * Run animation based on the frame time.
         * @param frameTime The frame start time
         */
        boolean doAnimationFrame(long frameTime);
    }

    /**
     * This method notifies all the on-going animations of the new frame, so that
     * they can update animation values as needed.
     */
    void onAnimationFrame(long frameTime) {
        AnimationHandler.this.doAnimationFrame(frameTime);
        if (getAnimationCallbacks().size() > 0) {
            mProvider.postFrameCallback();
        }
    }
    /**
     * Internal per-thread collections used to avoid set collisions as animations start and end
     * while being processed.
     */

    static class AnimationCallbackData {
        final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
                new SimpleArrayMap<>();
        final ArrayList<AnimationFrameCallback> mAnimationCallbacks = new ArrayList<>();
        boolean mListDirty = false;
    }

    public static AnimationHandler sAnimationHandler = null;
    private static AnimationHandler sTestHandler = null;
    private static final ThreadLocal<AnimationCallbackData> sAnimationCallbackData =
            new ThreadLocal<>();
    private final AnimationFrameCallbackProvider mProvider;

    AnimationHandler(AnimationFrameCallbackProvider provider) {
        if (provider == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                mProvider = new FrameCallbackProvider16();
            } else {
                mProvider = new FrameCallbackProvider14(this);
            }
        } else {
            mProvider = provider;
        }
    }

    public static AnimationHandler getInstance() {
        if (sTestHandler != null) {
            return sTestHandler;
        }
        if (sAnimationHandler == null) {
            sAnimationHandler = new AnimationHandler(null);
        }
        return sAnimationHandler;
    }

    static void setTestHandler(AnimationHandler handler) {
        sTestHandler = handler;
    }

    void setFrameDelay(long frameDelay) {
        mProvider.setFrameDelay(frameDelay);
    }

    long getFrameDelay() {
        return mProvider.getFrameDelay();
    }

    private SimpleArrayMap<AnimationFrameCallback, Long> getDelayedCallbackStartTime() {
        AnimationCallbackData data = sAnimationCallbackData.get();
        if (data == null) {
            data = new AnimationCallbackData();
            sAnimationCallbackData.set(data);
        }

        return data.mDelayedCallbackStartTime;
    }

    private ArrayList<AnimationFrameCallback> getAnimationCallbacks() {
        AnimationCallbackData data = sAnimationCallbackData.get();
        if (data == null) {
            data = new AnimationCallbackData();
            sAnimationCallbackData.set(data);
        }

        return data.mAnimationCallbacks;
    }

    private boolean isListDirty() {
        AnimationCallbackData data = sAnimationCallbackData.get();
        if (data == null) {
            data = new AnimationCallbackData();
            sAnimationCallbackData.set(data);
        }

        return data.mListDirty;
    }

    private void setListDirty(boolean dirty) {
        AnimationCallbackData data = sAnimationCallbackData.get();
        if (data == null) {
            data = new AnimationCallbackData();
            sAnimationCallbackData.set(data);
        }

        data.mListDirty = dirty;
    }

    /**
     * Register to get a callback on the next frame after the delay.
     */
    void addAnimationFrameCallback(final AnimationFrameCallback callback) {
        if (getAnimationCallbacks().size() == 0) {
            mProvider.postFrameCallback();
        }
        if (!getAnimationCallbacks().contains(callback)) {
            getAnimationCallbacks().add(callback);
        }
        mProvider.onNewCallbackAdded(callback);
    }

    /**
     * Removes the given callback from the list, so it will no longer be called for frame related
     * timing.
     */
    public void removeCallback(AnimationFrameCallback callback) {
        getDelayedCallbackStartTime().remove(callback);
        int id = getAnimationCallbacks().indexOf(callback);
        if (id >= 0) {
            getAnimationCallbacks().set(id, null);
            setListDirty(true);
        }
    }

    void autoCancelBasedOn(ObjectAnimator objectAnimator) {
        for (int i = getAnimationCallbacks().size() - 1; i >= 0; i--) {
            AnimationFrameCallback cb = getAnimationCallbacks().get(i);
            if (cb == null) {
                continue;
            }
            if (objectAnimator.shouldAutoCancel(cb)) {
                ((Animator) getAnimationCallbacks().get(i)).cancel();
            }
        }
    }

    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        for (int i = 0; i < getAnimationCallbacks().size(); i++) {
            final AnimationFrameCallback callback = getAnimationCallbacks().get(i);
            if (callback == null) {
                continue;
            }
            if (isCallbackDue(callback, currentTime)) {
                callback.doAnimationFrame(frameTime);
            }
        }
        cleanUpList();
    }

    /**
     * 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 = getDelayedCallbackStartTime().get(callback);
        if (startTime == null) {
            return true;
        }
        if (startTime < currentTime) {
            getDelayedCallbackStartTime().remove(callback);
            return true;
        }
        return false;
    }

    private void cleanUpList() {
        if (isListDirty()) {
            for (int i = getAnimationCallbacks().size() - 1; i >= 0; i--) {
                if (getAnimationCallbacks().get(i) == null) {
                    getAnimationCallbacks().remove(i);
                }
            }
            setListDirty(false);
        }
    }

    private int getCallbackSize() {
        int count = 0;
        int size = getAnimationCallbacks().size();
        for (int i = size - 1; i >= 0; i--) {
            if (getAnimationCallbacks().get(i) != null) {
                count++;
            }
        }
        return count;
    }

    /**
     * Return the number of callbacks that have registered for frame callbacks.
     */
    public static int getAnimationCount() {
        AnimationHandler handler = AnimationHandler.getInstance();
        if (handler == null) {
            return 0;
        }
        return handler.getCallbackSize();
    }

    /**
     * Default provider of timing pulse that uses Choreographer for frame callbacks.
     */
    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
    private class FrameCallbackProvider16 implements AnimationFrameCallbackProvider,
            Choreographer.FrameCallback {

        FrameCallbackProvider16() {
        }

        @Override
        public void doFrame(long frameTimeNanos) {
            onAnimationFrame(frameTimeNanos / 1000000);
        }

        @Override
        public void onNewCallbackAdded(AnimationFrameCallback callback) {}

        @Override
        public void postFrameCallback() {
            Choreographer.getInstance().postFrameCallback(this);
        }

        @Override
        public void setFrameDelay(long delay) {
            android.animation.ValueAnimator.setFrameDelay(delay);
        }

        @Override
        public long getFrameDelay() {
            return android.animation.ValueAnimator.getFrameDelay();
        }
    }

    /**
     * Frame provider for ICS and ICS-MR1 releases. The frame callback is achieved via posting
     * a Runnable to the main thread Handler with a delay.
     */
    private static class FrameCallbackProvider14 implements AnimationFrameCallbackProvider,
            Runnable {

        private static final ThreadLocal<Handler> sHandler = new ThreadLocal<>();
        private long mLastFrameTime = -1;
        private long mFrameDelay = 16;
        AnimationHandler mAnimationHandler;

        FrameCallbackProvider14(AnimationHandler animationHandler) {
            mAnimationHandler = animationHandler;
        }

        Handler getHandler() {
            if (sHandler.get() == null) {
                sHandler.set(new Handler(Looper.myLooper()));
            }
            return sHandler.get();
        }

        @Override
        public void run() {
            mLastFrameTime = SystemClock.uptimeMillis();
            mAnimationHandler.onAnimationFrame(mLastFrameTime);
        }

        @Override
        public void onNewCallbackAdded(AnimationFrameCallback callback) {}

        @Override
        public void postFrameCallback() {
            long delay = mFrameDelay - (SystemClock.uptimeMillis()
                    - mLastFrameTime);
            delay = Math.max(delay, 0);
            getHandler().postDelayed(this, delay);
        }

        // TODO: consider removing frame delay setter/getter from ValueAnimator
        @Override
        public void setFrameDelay(long delay) {
            mFrameDelay = delay > 0 ? delay : 0;
        }

        @Override
        public long getFrameDelay() {
            return mFrameDelay;
        }
    }

    /**
     * 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.
     */
    interface AnimationFrameCallbackProvider {

        void onNewCallbackAdded(AnimationFrameCallback callback);

        void postFrameCallback();

        void setFrameDelay(long delay);

        long getFrameDelay();
    }
}