summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorJeff Brown <jeffbrown@google.com>2012-04-26 17:38:21 -0700
committerJeff Brown <jeffbrown@google.com>2012-04-26 20:12:43 -0700
commit20c4f87b2916d05e860d11568d7db6b2d340e909 (patch)
tree76e0921a2daaa5f2cae88c9563e4ff9f4bb5a1aa /core/java
parent97d5c418730946a0332f601cd140ed0b12ea19c1 (diff)
downloadframeworks_base-20c4f87b2916d05e860d11568d7db6b2d340e909.zip
frameworks_base-20c4f87b2916d05e860d11568d7db6b2d340e909.tar.gz
frameworks_base-20c4f87b2916d05e860d11568d7db6b2d340e909.tar.bz2
Use choreographer frame time to schedule animations.
Instead of using the current uptime millis, which can exhibit substantial jitter depending on when the code runs, use the current frame's vsync time when performing animations. The frame time provides a more consistent pulse. Bug: 6375101 Change-Id: Icf307cd8524246607db7496c6fef9a5eeb7c0439
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/animation/TimeAnimator.java10
-rwxr-xr-xcore/java/android/animation/ValueAnimator.java96
-rw-r--r--core/java/android/view/Choreographer.java80
3 files changed, 111 insertions, 75 deletions
diff --git a/core/java/android/animation/TimeAnimator.java b/core/java/android/animation/TimeAnimator.java
index a79f2a3..088d20d 100644
--- a/core/java/android/animation/TimeAnimator.java
+++ b/core/java/android/animation/TimeAnimator.java
@@ -14,16 +14,6 @@ public class TimeAnimator extends ValueAnimator {
@Override
boolean animationFrame(long currentTime) {
- if (mPlayingState == STOPPED) {
- mPlayingState = RUNNING;
- if (mSeekTime < 0) {
- mStartTime = currentTime;
- } else {
- mStartTime = currentTime - mSeekTime;
- // Now that we're playing, reset the seek time
- mSeekTime = -1;
- }
- }
if (mListener != null) {
long totalTime = currentTime - mStartTime;
long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime);
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 2154b14..326f27c 100755
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -56,11 +56,6 @@ public class ValueAnimator extends Animator {
private static float sDurationScale = 1.0f;
/**
- * Messages sent to timing handler: START is sent when an animation first begins.
- */
- static final int ANIMATION_START = 0;
-
- /**
* Values used with internal variable mPlayingState to indicate the current state of an
* animation.
*/
@@ -504,7 +499,7 @@ public class ValueAnimator extends Animator {
mPlayingState = SEEKED;
}
mStartTime = currentTime - playTime;
- animationFrame(currentTime);
+ doAnimationFrame(currentTime);
}
/**
@@ -528,8 +523,9 @@ public class ValueAnimator extends Animator {
* the same times for calculating their values, which makes synchronizing
* animations possible.
*
+ * The handler uses the Choreographer for executing periodic callbacks.
*/
- private static class AnimationHandler extends Handler implements Runnable {
+ private static class AnimationHandler implements Runnable {
// The per-thread list of all active animations
private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
@@ -552,34 +548,13 @@ public class ValueAnimator extends Animator {
}
/**
- * 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.
+ * Start animating on the next frame.
*/
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case ANIMATION_START:
- // If there are already active animations, or if another ANIMATION_START
- // message was processed during this frame, then the pending list may already
- // have been cleared. If that's the case, we've already processed the
- // active animations for this frame - don't do it again.
- if (mPendingAnimations.size() > 0) {
- doAnimationFrame();
- }
- break;
- }
+ public void start() {
+ scheduleAnimation();
}
- private void doAnimationFrame() {
- // currentTime holds the common time for all animations processed
- // during this frame
- long currentTime = AnimationUtils.currentAnimationTimeMillis();
-
+ private void doAnimationFrame(long frameTime) {
// 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
@@ -605,7 +580,7 @@ public class ValueAnimator extends Animator {
int numDelayedAnims = mDelayedAnims.size();
for (int i = 0; i < numDelayedAnims; ++i) {
ValueAnimator anim = mDelayedAnims.get(i);
- if (anim.delayedAnimationFrame(currentTime)) {
+ if (anim.delayedAnimationFrame(frameTime)) {
mReadyAnims.add(anim);
}
}
@@ -626,7 +601,7 @@ public class ValueAnimator extends Animator {
int i = 0;
while (i < numAnims) {
ValueAnimator anim = mAnimations.get(i);
- if (anim.animationFrame(currentTime)) {
+ if (anim.doAnimationFrame(frameTime)) {
mEndingAnims.add(anim);
}
if (mAnimations.size() == numAnims) {
@@ -652,10 +627,8 @@ public class ValueAnimator extends Animator {
// If there are still active or delayed animations, schedule a future call to
// onAnimate to process the next frame of the animations.
- if (!mAnimationScheduled
- && (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty())) {
- mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
- mAnimationScheduled = true;
+ if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
+ scheduleAnimation();
}
}
@@ -663,7 +636,14 @@ public class ValueAnimator extends Animator {
@Override
public void run() {
mAnimationScheduled = false;
- doAnimationFrame();
+ doAnimationFrame(mChoreographer.getFrameTime());
+ }
+
+ private void scheduleAnimation() {
+ if (!mAnimationScheduled) {
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
+ mAnimationScheduled = true;
+ }
}
}
@@ -935,7 +915,7 @@ public class ValueAnimator extends Animator {
mRunning = true;
notifyStartListeners();
}
- animationHandler.sendEmptyMessage(ANIMATION_START);
+ animationHandler.start();
}
@Override
@@ -1098,17 +1078,6 @@ public class ValueAnimator extends Animator {
*/
boolean animationFrame(long currentTime) {
boolean done = false;
-
- if (mPlayingState == STOPPED) {
- mPlayingState = RUNNING;
- if (mSeekTime < 0) {
- mStartTime = currentTime;
- } else {
- mStartTime = currentTime - mSeekTime;
- // Now that we're playing, reset the seek time
- mSeekTime = -1;
- }
- }
switch (mPlayingState) {
case RUNNING:
case SEEKED:
@@ -1144,6 +1113,31 @@ public class ValueAnimator extends Animator {
}
/**
+ * Processes a frame of the animation, adjusting the start time if needed.
+ *
+ * @param frameTime The frame time.
+ * @return true if the animation has ended.
+ */
+ final boolean doAnimationFrame(long frameTime) {
+ if (mPlayingState == STOPPED) {
+ mPlayingState = RUNNING;
+ if (mSeekTime < 0) {
+ mStartTime = frameTime;
+ } else {
+ mStartTime = frameTime - mSeekTime;
+ // Now that we're playing, reset the seek time
+ mSeekTime = -1;
+ }
+ }
+ // The frame time might be before the start time during the first frame of
+ // an animation. The "current time" must always be on or after the start
+ // time to avoid animating frames at negative time intervals. In practice, this
+ // is very rare and only happens when seeking backwards.
+ final long currentTime = Math.max(frameTime, mStartTime);
+ return animationFrame(currentTime);
+ }
+
+ /**
* Returns the current animation fraction, which is the elapsed/interpolated fraction used in
* the most recent frame update on the animation.
*
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 4866889..b319cd5 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -69,6 +69,12 @@ public final class Choreographer {
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
+ // Enable/disable using the frame time instead of returning now.
+ private static final boolean USE_FRAME_TIME = SystemProperties.getBoolean(
+ "debug.choreographer.frametime", true);
+
+ private static final long NANOS_PER_MS = 1000000;
+
private static final int MSG_DO_FRAME = 0;
private static final int MSG_DO_SCHEDULE_VSYNC = 1;
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;
@@ -84,7 +90,8 @@ public final class Choreographer {
private final CallbackQueue[] mCallbackQueues;
private boolean mFrameScheduled;
- private long mLastFrameTime;
+ private boolean mCallbacksRunning;
+ private long mLastFrameTimeNanos;
/**
* Callback type: Input callback. Runs first.
@@ -108,7 +115,7 @@ public final class Choreographer {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
- mLastFrameTime = Long.MIN_VALUE;
+ mLastFrameTimeNanos = Long.MIN_VALUE;
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
@@ -270,6 +277,40 @@ public final class Choreographer {
}
}
+ /**
+ * Gets the time when the current frame started. The frame time should be used
+ * instead of {@link SystemClock#uptimeMillis()} to synchronize animations.
+ * This helps to reduce inter-frame jitter because the frame time is fixed at the
+ * time the frame was scheduled to start, regardless of when the animations or
+ * drawing code actually ran.
+ *
+ * This method should only be called from within a callback.
+ *
+ * @return The frame start time, in the {@link SystemClock#uptimeMillis()} time base.
+ *
+ * @throws IllegalStateException if no frame is in progress.
+ */
+ public long getFrameTime() {
+ return getFrameTimeNanos() / NANOS_PER_MS;
+ }
+
+ /**
+ * Same as {@link #getFrameTime()} but with nanosecond precision.
+ *
+ * @return The frame start time, in the {@link System#nanoTime()} time base.
+ *
+ * @throws IllegalStateException if no frame is in progress.
+ */
+ public long getFrameTimeNanos() {
+ synchronized (mLock) {
+ if (!mCallbacksRunning) {
+ throw new IllegalStateException("This method must only be called as "
+ + "part of a callback while a frame is in progress.");
+ }
+ return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();
+ }
+ }
+
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
@@ -289,7 +330,8 @@ public final class Choreographer {
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
- final long nextFrameTime = Math.max(mLastFrameTime + sFrameDelay, now);
+ final long nextFrameTime = Math.max(
+ mLastFrameTimeNanos / NANOS_PER_MS + sFrameDelay, now);
if (DEBUG) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
@@ -300,13 +342,18 @@ public final class Choreographer {
}
}
- void doFrame(int frame) {
+ void doFrame(long timestampNanos, int frame) {
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
mFrameScheduled = false;
- mLastFrameTime = SystemClock.uptimeMillis();
+ mLastFrameTimeNanos = timestampNanos;
+ }
+
+ final long startNanos;
+ if (DEBUG) {
+ startNanos = System.nanoTime();
}
doCallbacks(Choreographer.CALLBACK_INPUT);
@@ -314,20 +361,24 @@ public final class Choreographer {
doCallbacks(Choreographer.CALLBACK_TRAVERSAL);
if (DEBUG) {
+ final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
- + (SystemClock.uptimeMillis() - mLastFrameTime) + " ms.");
+ + (endNanos - startNanos) * 0.000001f + " ms, latency "
+ + (startNanos - timestampNanos) * 0.000001f + " ms.");
}
}
void doCallbacks(int callbackType) {
- final long start;
Callback callbacks;
synchronized (mLock) {
- start = SystemClock.uptimeMillis();
- callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(start);
+ final long now = SystemClock.uptimeMillis();
+ callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now);
+ if (callbacks == null) {
+ return;
+ }
+ mCallbacksRunning = true;
}
-
- if (callbacks != null) {
+ try {
for (Callback c = callbacks; c != null; c = c.next) {
if (DEBUG) {
Log.d(TAG, "RunCallback: type=" + callbackType
@@ -336,8 +387,9 @@ public final class Choreographer {
}
c.action.run();
}
-
+ } finally {
synchronized (mLock) {
+ mCallbacksRunning = false;
do {
final Callback next = callbacks.next;
recycleCallbackLocked(callbacks);
@@ -404,7 +456,7 @@ public final class Choreographer {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
- doFrame(0);
+ doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
@@ -423,7 +475,7 @@ public final class Choreographer {
@Override
public void onVsync(long timestampNanos, int frame) {
- doFrame(frame);
+ doFrame(timestampNanos, frame);
}
}