Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 96e942da authored by Jeff Brown's avatar Jeff Brown
Browse files

Use a Choreographer to schedule animation and drawing.

Both animations and drawing need to march to the beat of
the same drum, but the animation system doesn't know
abgout the view system and vice-versa so neither one
can drive the other.

We introduce the Choreographer as a drummer to keep
everyone in time and ensure a magnificent performance.

This patch enabled VSync based animations and drawing by
default.  Two system properties are provided for testing
purposes to control the behavior.

"debug.choreographer.vsync": Enables vsync based animation
timing.  Defaults to true.  When false, animations are
timed by posting delayed messages to a message queue in
the same way they used to be before this patch.

"debug.choreographer.animdraw": Enables the use of the animation
timer to drive drawing such that drawing is synchronized with
animations (in other words, with vsync or the timing loop).
Defaults to true.  When false, layout traversals and drawing
are posted to the message queue for execution without any delay or
synchronization in the same way they used to be before this patch.

Stubbed out part of the layoutlib animation code because it
depends on the old timing loop (opened bug 5712395)

Change-Id: I186d9518648e89bc3e809e393e9a9148bbbecc4d
parent 0a0a1248
Loading
Loading
Loading
Loading
+120 −110
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AndroidRuntimeException;
import android.view.Choreographer;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
@@ -45,17 +46,10 @@ public class ValueAnimator extends Animator {
     * Internal constants
     */

    /*
     * The default amount of time in ms between animation frames
     */
    private static final long DEFAULT_FRAME_DELAY = 10;

    /**
     * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent
     * by the handler to itself to process the next animation frame
     * Messages sent to timing handler: START is sent when an animation first begins.
     */
    static final int ANIMATION_START = 0;
    static final int ANIMATION_FRAME = 1;

    /**
     * Values used with internal variable mPlayingState to indicate the current state of an
@@ -162,9 +156,6 @@ public class ValueAnimator extends Animator {
    // The amount of time in ms to delay starting the animation after start() is called
    private long mStartDelay = 0;

    // The number of milliseconds between animation frames
    private static long sFrameDelay = DEFAULT_FRAME_DELAY;

    // The number of times the animation will repeat. The default is 0, which means the animation
    // will play only once
    private int mRepeatCount = 0;
@@ -511,7 +502,8 @@ public class ValueAnimator extends Animator {
     * animations possible.
     *
     */
    private static class AnimationHandler extends Handler {
    private static class AnimationHandler extends Handler
            implements Choreographer.OnAnimateListener {
        // The per-thread list of all active animations
        private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();

@@ -526,40 +518,41 @@ public class ValueAnimator extends Animator {
        private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
        private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();

        private final Choreographer mChoreographer;
        private boolean mIsChoreographed;

        private AnimationHandler() {
            mChoreographer = Choreographer.getInstance();
        }

        /**
         * There are only two messages that we care about: ANIMATION_START and
         * ANIMATION_FRAME. The START message is sent when an animation's start()
         * method is called. It cannot start synchronously when start() is called
         * The START message is sent when an animation's start()  method is called.
         * It cannot start synchronously when start() is called
         * because the call may be on the wrong thread, and it would also not be
         * synchronized with other animations because it would not start on a common
         * timing pulse. So each animation sends a START message to the handler, which
         * causes the handler to place the animation on the active animations queue and
         * start processing frames for that animation.
         * The FRAME message is the one that is sent over and over while there are any
         * active animations to process.
         */
        @Override
        public void handleMessage(Message msg) {
            boolean callAgain = true;
            ArrayList<ValueAnimator> animations = mAnimations;
            ArrayList<ValueAnimator> delayedAnims = mDelayedAnims;
            switch (msg.what) {
                // TODO: should we avoid sending frame message when starting if we
                // were already running?
                case ANIMATION_START:
                    ArrayList<ValueAnimator> pendingAnimations = mPendingAnimations;
                    if (animations.size() > 0 || delayedAnims.size() > 0) {
                        callAgain = false;
                    doAnimationStart();
                    break;
            }
                    // pendingAnims holds any animations that have requested to be started
                    // We're going to clear sPendingAnimations, but starting animation may
        }

        private void doAnimationStart() {
            // mPendingAnimations holds any animations that have requested to be started
            // We're going to clear mPendingAnimations, but starting animation may
            // cause more to be added to the pending list (for example, if one animation
                    // starting triggers another starting). So we loop until sPendingAnimations
            // starting triggers another starting). So we loop until mPendingAnimations
            // is empty.
                    while (pendingAnimations.size() > 0) {
            while (mPendingAnimations.size() > 0) {
                ArrayList<ValueAnimator> pendingCopy =
                                (ArrayList<ValueAnimator>) pendingAnimations.clone();
                        pendingAnimations.clear();
                        (ArrayList<ValueAnimator>) mPendingAnimations.clone();
                mPendingAnimations.clear();
                int count = pendingCopy.size();
                for (int i = 0; i < count; ++i) {
                    ValueAnimator anim = pendingCopy.get(i);
@@ -567,48 +560,48 @@ public class ValueAnimator extends Animator {
                    if (anim.mStartDelay == 0) {
                        anim.startAnimation(this);
                    } else {
                                delayedAnims.add(anim);
                        mDelayedAnims.add(anim);
                    }
                }
            }
                    // fall through to process first frame of new animations
                case ANIMATION_FRAME:
            doAnimationFrame();
        }

        private void doAnimationFrame() {
            // currentTime holds the common time for all animations processed
            // during this frame
            long currentTime = AnimationUtils.currentAnimationTimeMillis();
                    ArrayList<ValueAnimator> readyAnims = mReadyAnims;
                    ArrayList<ValueAnimator> endingAnims = mEndingAnims;

            // First, process animations currently sitting on the delayed queue, adding
            // them to the active animations if they are ready
                    int numDelayedAnims = delayedAnims.size();
            int numDelayedAnims = mDelayedAnims.size();
            for (int i = 0; i < numDelayedAnims; ++i) {
                        ValueAnimator anim = delayedAnims.get(i);
                ValueAnimator anim = mDelayedAnims.get(i);
                if (anim.delayedAnimationFrame(currentTime)) {
                            readyAnims.add(anim);
                    mReadyAnims.add(anim);
                }
            }
                    int numReadyAnims = readyAnims.size();
            int numReadyAnims = mReadyAnims.size();
            if (numReadyAnims > 0) {
                for (int i = 0; i < numReadyAnims; ++i) {
                            ValueAnimator anim = readyAnims.get(i);
                    ValueAnimator anim = mReadyAnims.get(i);
                    anim.startAnimation(this);
                    anim.mRunning = true;
                            delayedAnims.remove(anim);
                    mDelayedAnims.remove(anim);
                }
                        readyAnims.clear();
                mReadyAnims.clear();
            }

            // Now process all active animations. The return value from animationFrame()
            // tells the handler whether it should now be ended
                    int numAnims = animations.size();
            int numAnims = mAnimations.size();
            int i = 0;
            while (i < numAnims) {
                        ValueAnimator anim = animations.get(i);
                ValueAnimator anim = mAnimations.get(i);
                if (anim.animationFrame(currentTime)) {
                            endingAnims.add(anim);
                    mEndingAnims.add(anim);
                }
                        if (animations.size() == numAnims) {
                if (mAnimations.size() == numAnims) {
                    ++i;
                } else {
                    // An animation might be canceled or ended by client code
@@ -619,25 +612,36 @@ public class ValueAnimator extends Animator {
                    // but that entails garbage and processing overhead that would
                    // be nice to avoid.
                    --numAnims;
                            endingAnims.remove(anim);
                    mEndingAnims.remove(anim);
                }
            }
                    if (endingAnims.size() > 0) {
                        for (i = 0; i < endingAnims.size(); ++i) {
                            endingAnims.get(i).endAnimation(this);
            if (mEndingAnims.size() > 0) {
                for (i = 0; i < mEndingAnims.size(); ++i) {
                    mEndingAnims.get(i).endAnimation(this);
                }
                        endingAnims.clear();
                mEndingAnims.clear();
            }

                    // If there are still active or delayed animations, call the handler again
                    // after the frameDelay
                    if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) {
                        sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -
                            (AnimationUtils.currentAnimationTimeMillis() - currentTime)));
            // If there are still active or delayed animations, schedule a future call to
            // onAnimate to process the next frame of the animations.
            if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
                if (!mIsChoreographed) {
                    mIsChoreographed = true;
                    mChoreographer.addOnAnimateListener(this);
                }
                mChoreographer.scheduleAnimation();
            } else {
                if (mIsChoreographed) {
                    mIsChoreographed = false;
                    mChoreographer.removeOnAnimateListener(this);
                }
                    break;
            }
        }

        @Override
        public void onAnimate() {
            doAnimationFrame();
        }
    }

    /**
@@ -667,10 +671,13 @@ public class ValueAnimator extends Animator {
     * function because the same delay will be applied to all animations, since they are all
     * run off of a single timing loop.
     *
     * The frame delay may be ignored when the animation system uses an external timing
     * source, such as the display refresh rate (vsync), to govern animations.
     *
     * @return the requested time between frames, in milliseconds
     */
    public static long getFrameDelay() {
        return sFrameDelay;
        return Choreographer.getFrameDelay();
    }

    /**
@@ -680,10 +687,13 @@ public class ValueAnimator extends Animator {
     * function because the same delay will be applied to all animations, since they are all
     * run off of a single timing loop.
     *
     * The frame delay may be ignored when the animation system uses an external timing
     * source, such as the display refresh rate (vsync), to govern animations.
     *
     * @param frameDelay the requested time between frames, in milliseconds
     */
    public static void setFrameDelay(long frameDelay) {
        sFrameDelay = frameDelay;
        Choreographer.setFrameDelay(frameDelay);
    }

    /**
+380 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 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 android.view;

import com.android.internal.util.ArrayUtils;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;

/**
 * Coodinates animations and drawing for UI on a particular thread.
 * @hide
 */
public final class Choreographer extends Handler {
    private static final String TAG = "Choreographer";
    private static final boolean DEBUG = false;

    // The default amount of time in ms between animation frames.
    // When vsync is not enabled, we want to have some idea of how long we should
    // wait before posting the next animation message.  It is important that the
    // default value be less than the true inter-frame delay on all devices to avoid
    // situations where we might skip frames by waiting too long (we must compensate
    // for jitter and hardware variations).  Regardless of this value, the animation
    // and display loop is ultimately rate-limited by how fast new graphics buffers can
    // be dequeued.
    private static final long DEFAULT_FRAME_DELAY = 10;

    // The number of milliseconds between animation frames.
    private static long sFrameDelay = DEFAULT_FRAME_DELAY;

    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            return new Choreographer(looper);
        }
    };

    // System property to enable/disable vsync for animations and drawing.
    // Enabled by default.
    private static final boolean USE_VSYNC = SystemProperties.getBoolean(
            "debug.choreographer.vsync", true);

    // System property to enable/disable the use of the vsync / animation timer
    // for drawing rather than drawing immediately.
    // Enabled by default.
    private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean(
            "debug.choreographer.animdraw", true);

    private static final int MSG_DO_ANIMATION = 0;
    private static final int MSG_DO_DRAW = 1;

    private final Looper mLooper;

    private OnAnimateListener[] mOnAnimateListeners;
    private OnDrawListener[] mOnDrawListeners;

    private boolean mAnimationScheduled;
    private boolean mDrawScheduled;
    private FrameDisplayEventReceiver mFrameDisplayEventReceiver;
    private long mLastAnimationTime;
    private long mLastDrawTime;

    private Choreographer(Looper looper) {
        super(looper);
        mLooper = looper;
        mLastAnimationTime = Long.MIN_VALUE;
        mLastDrawTime = Long.MIN_VALUE;
    }

    /**
     * Gets the choreographer for this thread.
     * Must be called on the UI thread.
     *
     * @return The choreographer for this thread.
     * @throws IllegalStateException if the thread does not have a looper.
     */
    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }

    /**
     * The amount of time, in milliseconds, between each frame of the animation. This is a
     * requested time that the animation will attempt to honor, but the actual delay between
     * frames may be different, depending on system load and capabilities. This is a static
     * function because the same delay will be applied to all animations, since they are all
     * run off of a single timing loop.
     *
     * The frame delay may be ignored when the animation system uses an external timing
     * source, such as the display refresh rate (vsync), to govern animations.
     *
     * @return the requested time between frames, in milliseconds
     */
    public static long getFrameDelay() {
        return sFrameDelay;
    }

    /**
     * The amount of time, in milliseconds, between each frame of the animation. This is a
     * requested time that the animation will attempt to honor, but the actual delay between
     * frames may be different, depending on system load and capabilities. This is a static
     * function because the same delay will be applied to all animations, since they are all
     * run off of a single timing loop.
     *
     * The frame delay may be ignored when the animation system uses an external timing
     * source, such as the display refresh rate (vsync), to govern animations.
     *
     * @param frameDelay the requested time between frames, in milliseconds
     */
    public static void setFrameDelay(long frameDelay) {
        sFrameDelay = frameDelay;
    }

    /**
     * Schedules animation (and drawing) to occur on the next frame synchronization boundary.
     * Must be called on the UI thread.
     */
    public void scheduleAnimation() {
        if (!mAnimationScheduled) {
            mAnimationScheduled = true;
            if (USE_VSYNC) {
                if (DEBUG) {
                    Log.d(TAG, "Scheduling vsync for animation.");
                }
                if (mFrameDisplayEventReceiver == null) {
                    mFrameDisplayEventReceiver = new FrameDisplayEventReceiver(mLooper);
                }
                mFrameDisplayEventReceiver.scheduleVsync();
            } else {
                final long now = SystemClock.uptimeMillis();
                final long nextAnimationTime = Math.max(mLastAnimationTime + sFrameDelay, now);
                if (DEBUG) {
                    Log.d(TAG, "Scheduling animation in " + (nextAnimationTime - now) + " ms.");
                }
                sendEmptyMessageAtTime(MSG_DO_ANIMATION, nextAnimationTime);
            }
        }
    }

    /**
     * Schedules drawing to occur on the next frame synchronization boundary.
     * Must be called on the UI thread.
     */
    public void scheduleDraw() {
        if (!mDrawScheduled) {
            mDrawScheduled = true;
            if (USE_ANIMATION_TIMER_FOR_DRAW) {
                scheduleAnimation();
            } else {
                if (DEBUG) {
                    Log.d(TAG, "Scheduling draw immediately.");
                }
                sendEmptyMessage(MSG_DO_DRAW);
            }
        }
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_ANIMATION:
                doAnimation();
                break;
            case MSG_DO_DRAW:
                doDraw();
                break;
        }
    }

    private void doAnimation() {
        if (mAnimationScheduled) {
            mAnimationScheduled = false;

            final long start = SystemClock.uptimeMillis();
            if (DEBUG) {
                Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime)
                        + " ms have elapsed since previous animation.");
            }
            mLastAnimationTime = start;

            final OnAnimateListener[] listeners = mOnAnimateListeners;
            if (listeners != null) {
                for (int i = 0; i < listeners.length; i++) {
                    listeners[i].onAnimate();
                }
            }

            if (DEBUG) {
                Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms.");
            }
        }

        if (USE_ANIMATION_TIMER_FOR_DRAW) {
            doDraw();
        }
    }

    private void doDraw() {
        if (mDrawScheduled) {
            mDrawScheduled = false;

            final long start = SystemClock.uptimeMillis();
            if (DEBUG) {
                Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime)
                        + " ms have elapsed since previous draw.");
            }
            mLastDrawTime = start;

            final OnDrawListener[] listeners = mOnDrawListeners;
            if (listeners != null) {
                for (int i = 0; i < listeners.length; i++) {
                    listeners[i].onDraw();
                }
            }

            if (DEBUG) {
                Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms.");
            }
        }
    }

    /**
     * Adds an animation listener.
     * Must be called on the UI thread.
     *
     * @param listener The listener to add.
     */
    public void addOnAnimateListener(OnAnimateListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        if (DEBUG) {
            Log.d(TAG, "Adding onAnimate listener: " + listener);
        }

        mOnAnimateListeners = ArrayUtils.appendElement(OnAnimateListener.class,
                mOnAnimateListeners, listener);
    }

    /**
     * Removes an animation listener.
     * Must be called on the UI thread.
     *
     * @param listener The listener to remove.
     */
    public void removeOnAnimateListener(OnAnimateListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        if (DEBUG) {
            Log.d(TAG, "Removing onAnimate listener: " + listener);
        }

        mOnAnimateListeners = ArrayUtils.removeElement(OnAnimateListener.class,
                mOnAnimateListeners, listener);
        stopTimingLoopIfNoListeners();
    }

    /**
     * Adds a draw listener.
     * Must be called on the UI thread.
     *
     * @param listener The listener to add.
     */
    public void addOnDrawListener(OnDrawListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        if (DEBUG) {
            Log.d(TAG, "Adding onDraw listener: " + listener);
        }

        mOnDrawListeners = ArrayUtils.appendElement(OnDrawListener.class,
                mOnDrawListeners, listener);
    }

    /**
     * Removes a draw listener.
     * Must be called on the UI thread.
     *
     * @param listener The listener to remove.
     */
    public void removeOnDrawListener(OnDrawListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        if (DEBUG) {
            Log.d(TAG, "Removing onDraw listener: " + listener);
        }

        mOnDrawListeners = ArrayUtils.removeElement(OnDrawListener.class,
                mOnDrawListeners, listener);
        stopTimingLoopIfNoListeners();
    }

    private void stopTimingLoopIfNoListeners() {
        if (mOnDrawListeners == null && mOnAnimateListeners == null) {
            if (DEBUG) {
                Log.d(TAG, "Stopping timing loop.");
            }

            if (mAnimationScheduled) {
                mAnimationScheduled = false;
                if (!USE_VSYNC) {
                    removeMessages(MSG_DO_ANIMATION);
                }
            }

            if (mDrawScheduled) {
                mDrawScheduled = false;
                if (!USE_ANIMATION_TIMER_FOR_DRAW) {
                    removeMessages(MSG_DO_DRAW);
                }
            }

            if (mFrameDisplayEventReceiver != null) {
                mFrameDisplayEventReceiver.dispose();
                mFrameDisplayEventReceiver = null;
            }
        }
    }

    /**
     * Listens for animation frame timing events.
     */
    public static interface OnAnimateListener {
        /**
         * Called to animate properties before drawing the frame.
         */
        public void onAnimate();
    }

    /**
     * Listens for draw frame timing events.
     */
    public static interface OnDrawListener {
        /**
         * Called to draw the frame.
         */
        public void onDraw();
    }

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver {
        public FrameDisplayEventReceiver(Looper looper) {
            super(looper);
        }

        @Override
        public void onVsync(long timestampNanos, int frame) {
            doAnimation();
        }
    }
}
+197 −200

File changed.

Preview size limit exceeded, changes collapsed.

+52 −1
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.internal.util;

import java.lang.reflect.Array;
import java.util.Collection;

// XXX these should be changed to reflect the actual memory allocator we use.
// it looks like right now objects want to be powers of 2 minus 8
@@ -142,4 +141,56 @@ public class ArrayUtils
        }
        return false;
    }

    /**
     * Appends an element to a copy of the array and returns the copy.
     * @param array The original array, or null to represent an empty array.
     * @param element The element to add.
     * @return A new array that contains all of the elements of the original array
     * with the specified element added at the end.
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] appendElement(Class<T> kind, T[] array, T element) {
        final T[] result;
        final int end;
        if (array != null) {
            end = array.length;
            result = (T[])Array.newInstance(kind, end + 1);
            System.arraycopy(array, 0, result, 0, end);
        } else {
            end = 0;
            result = (T[])Array.newInstance(kind, 1);
        }
        result[end] = element;
        return result;
    }

    /**
     * Removes an element from a copy of the array and returns the copy.
     * If the element is not present, then the original array is returned unmodified.
     * @param array The original array, or null to represent an empty array.
     * @param element The element to remove.
     * @return A new array that contains all of the elements of the original array
     * except the first copy of the specified element removed.  If the specified element
     * was not present, then returns the original array.  Returns null if the result
     * would be an empty array.
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] removeElement(Class<T> kind, T[] array, T element) {
        if (array != null) {
            final int length = array.length;
            for (int i = 0; i < length; i++) {
                if (array[i] == element) {
                    if (length == 1) {
                        return null;
                    }
                    T[] result = (T[])Array.newInstance(kind, length - 1);
                    System.arraycopy(array, 0, result, 0, i);
                    System.arraycopy(array, i + 1, result, i, length - i - 1);
                    return result;
                }
            }
        }
        return array;
    }
}
+5 −2
Original line number Diff line number Diff line
@@ -86,8 +86,11 @@ public abstract class AnimationThread extends Thread {
        try {
            Handler_Delegate.setCallback(new IHandlerCallback() {
                public void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) {
                    if (msg.what == ValueAnimator.ANIMATION_START ||
                            msg.what == ValueAnimator.ANIMATION_FRAME) {
                    if (msg.what == ValueAnimator.ANIMATION_START /*||
                            FIXME: The ANIMATION_FRAME message no longer exists.  Instead,
                            the animation timing loop is based on a Choreographer object
                            that schedules animation and drawing frames.
                            msg.what == ValueAnimator.ANIMATION_FRAME*/) {
                        mQueue.add(new MessageBundle(handler, msg, uptimeMillis));
                    } else {
                        // just ignore.