blob: 3ee275cb63994f118a2a8bccbe0fce56d6af9457 [file] [log] [blame]
/*
* 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;
/**
* Coordinates animations and drawing for UI on a particular thread.
*
* This object is thread-safe. Other threads can add and remove listeners
* or schedule work to occur at a later time on the UI 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 volatile 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.
// Temporarily disabled by default because postponing performTraversals() violates
// assumptions about traversals happening in-order relative to other posted messages.
// Bug: 5721047
private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean(
"debug.choreographer.animdraw", false);
private static final int MSG_DO_ANIMATION = 0;
private static final int MSG_DO_DRAW = 1;
private final Object mLock = new Object();
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 the calling thread. Must be called from
* a thread that already has a {@link android.os.Looper} associated with it.
*
* @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.
*/
public void scheduleAnimation() {
synchronized (mLock) {
scheduleAnimationLocked();
}
}
private void scheduleAnimationLocked() {
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);
}
}
}
/**
* Returns true if {@link #scheduleAnimation()} has been called but
* {@link OnAnimateListener#onAnimate() OnAnimateListener.onAnimate()} has
* not yet been called.
*/
public boolean isAnimationScheduled() {
synchronized (mLock) {
return mAnimationScheduled;
}
}
/**
* Schedules drawing to occur on the next frame synchronization boundary.
* Must be called on the UI thread.
*/
public void scheduleDraw() {
synchronized (mLock) {
if (!mDrawScheduled) {
mDrawScheduled = true;
if (USE_ANIMATION_TIMER_FOR_DRAW) {
scheduleAnimationLocked();
} else {
if (DEBUG) {
Log.d(TAG, "Scheduling draw immediately.");
}
sendEmptyMessage(MSG_DO_DRAW);
}
}
}
}
/**
* Returns true if {@link #scheduleDraw()} has been called but
* {@link OnDrawListener#onDraw() OnDrawListener.onDraw()} has
* not yet been called.
*/
public boolean isDrawScheduled() {
synchronized (mLock) {
return mDrawScheduled;
}
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_ANIMATION:
doAnimation();
break;
case MSG_DO_DRAW:
doDraw();
break;
}
}
private void doAnimation() {
doAnimationInner();
if (USE_ANIMATION_TIMER_FOR_DRAW) {
doDraw();
}
}
private void doAnimationInner() {
final long start;
final OnAnimateListener[] listeners;
synchronized (mLock) {
if (!mAnimationScheduled) {
return; // no work to do
}
mAnimationScheduled = false;
start = SystemClock.uptimeMillis();
if (DEBUG) {
Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime)
+ " ms have elapsed since previous animation.");
}
mLastAnimationTime = start;
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.");
}
}
private void doDraw() {
final long start;
final OnDrawListener[] listeners;
synchronized (mLock) {
if (!mDrawScheduled) {
return; // no work to do
}
mDrawScheduled = false;
start = SystemClock.uptimeMillis();
if (DEBUG) {
Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime)
+ " ms have elapsed since previous draw.");
}
mLastDrawTime = start;
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.
*
* @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);
}
synchronized (mLock) {
mOnAnimateListeners = ArrayUtils.appendElement(OnAnimateListener.class,
mOnAnimateListeners, listener);
}
}
/**
* Removes an animation listener.
*
* @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);
}
synchronized (mLock) {
mOnAnimateListeners = ArrayUtils.removeElement(OnAnimateListener.class,
mOnAnimateListeners, listener);
stopTimingLoopIfNoListenersLocked();
}
}
/**
* Adds a draw listener.
*
* @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);
}
synchronized (mLock) {
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);
}
synchronized (mLock) {
mOnDrawListeners = ArrayUtils.removeElement(OnDrawListener.class,
mOnDrawListeners, listener);
stopTimingLoopIfNoListenersLocked();
}
}
private void stopTimingLoopIfNoListenersLocked() {
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();
}
}
}