Skip to content

Commit 20c4f87

Browse files
author
Jeff Brown
committed
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
1 parent 97d5c41 commit 20c4f87

File tree

3 files changed

+111
-75
lines changed

3 files changed

+111
-75
lines changed

core/java/android/animation/TimeAnimator.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,6 @@ public class TimeAnimator extends ValueAnimator {
1414

1515
@Override
1616
boolean animationFrame(long currentTime) {
17-
if (mPlayingState == STOPPED) {
18-
mPlayingState = RUNNING;
19-
if (mSeekTime < 0) {
20-
mStartTime = currentTime;
21-
} else {
22-
mStartTime = currentTime - mSeekTime;
23-
// Now that we're playing, reset the seek time
24-
mSeekTime = -1;
25-
}
26-
}
2717
if (mListener != null) {
2818
long totalTime = currentTime - mStartTime;
2919
long deltaTime = (mPreviousTime < 0) ? 0 : (currentTime - mPreviousTime);

core/java/android/animation/ValueAnimator.java

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ public class ValueAnimator extends Animator {
5555
*/
5656
private static float sDurationScale = 1.0f;
5757

58-
/**
59-
* Messages sent to timing handler: START is sent when an animation first begins.
60-
*/
61-
static final int ANIMATION_START = 0;
62-
6358
/**
6459
* Values used with internal variable mPlayingState to indicate the current state of an
6560
* animation.
@@ -504,7 +499,7 @@ public void setCurrentPlayTime(long playTime) {
504499
mPlayingState = SEEKED;
505500
}
506501
mStartTime = currentTime - playTime;
507-
animationFrame(currentTime);
502+
doAnimationFrame(currentTime);
508503
}
509504

510505
/**
@@ -528,8 +523,9 @@ public long getCurrentPlayTime() {
528523
* the same times for calculating their values, which makes synchronizing
529524
* animations possible.
530525
*
526+
* The handler uses the Choreographer for executing periodic callbacks.
531527
*/
532-
private static class AnimationHandler extends Handler implements Runnable {
528+
private static class AnimationHandler implements Runnable {
533529
// The per-thread list of all active animations
534530
private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
535531

@@ -552,34 +548,13 @@ private AnimationHandler() {
552548
}
553549

554550
/**
555-
* The START message is sent when an animation's start() method is called.
556-
* It cannot start synchronously when start() is called
557-
* because the call may be on the wrong thread, and it would also not be
558-
* synchronized with other animations because it would not start on a common
559-
* timing pulse. So each animation sends a START message to the handler, which
560-
* causes the handler to place the animation on the active animations queue and
561-
* start processing frames for that animation.
551+
* Start animating on the next frame.
562552
*/
563-
@Override
564-
public void handleMessage(Message msg) {
565-
switch (msg.what) {
566-
case ANIMATION_START:
567-
// If there are already active animations, or if another ANIMATION_START
568-
// message was processed during this frame, then the pending list may already
569-
// have been cleared. If that's the case, we've already processed the
570-
// active animations for this frame - don't do it again.
571-
if (mPendingAnimations.size() > 0) {
572-
doAnimationFrame();
573-
}
574-
break;
575-
}
553+
public void start() {
554+
scheduleAnimation();
576555
}
577556

578-
private void doAnimationFrame() {
579-
// currentTime holds the common time for all animations processed
580-
// during this frame
581-
long currentTime = AnimationUtils.currentAnimationTimeMillis();
582-
557+
private void doAnimationFrame(long frameTime) {
583558
// mPendingAnimations holds any animations that have requested to be started
584559
// We're going to clear mPendingAnimations, but starting animation may
585560
// cause more to be added to the pending list (for example, if one animation
@@ -605,7 +580,7 @@ private void doAnimationFrame() {
605580
int numDelayedAnims = mDelayedAnims.size();
606581
for (int i = 0; i < numDelayedAnims; ++i) {
607582
ValueAnimator anim = mDelayedAnims.get(i);
608-
if (anim.delayedAnimationFrame(currentTime)) {
583+
if (anim.delayedAnimationFrame(frameTime)) {
609584
mReadyAnims.add(anim);
610585
}
611586
}
@@ -626,7 +601,7 @@ private void doAnimationFrame() {
626601
int i = 0;
627602
while (i < numAnims) {
628603
ValueAnimator anim = mAnimations.get(i);
629-
if (anim.animationFrame(currentTime)) {
604+
if (anim.doAnimationFrame(frameTime)) {
630605
mEndingAnims.add(anim);
631606
}
632607
if (mAnimations.size() == numAnims) {
@@ -652,18 +627,23 @@ private void doAnimationFrame() {
652627

653628
// If there are still active or delayed animations, schedule a future call to
654629
// onAnimate to process the next frame of the animations.
655-
if (!mAnimationScheduled
656-
&& (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty())) {
657-
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
658-
mAnimationScheduled = true;
630+
if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
631+
scheduleAnimation();
659632
}
660633
}
661634

662635
// Called by the Choreographer.
663636
@Override
664637
public void run() {
665638
mAnimationScheduled = false;
666-
doAnimationFrame();
639+
doAnimationFrame(mChoreographer.getFrameTime());
640+
}
641+
642+
private void scheduleAnimation() {
643+
if (!mAnimationScheduled) {
644+
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
645+
mAnimationScheduled = true;
646+
}
667647
}
668648
}
669649

@@ -935,7 +915,7 @@ private void start(boolean playBackwards) {
935915
mRunning = true;
936916
notifyStartListeners();
937917
}
938-
animationHandler.sendEmptyMessage(ANIMATION_START);
918+
animationHandler.start();
939919
}
940920

941921
@Override
@@ -1098,17 +1078,6 @@ private boolean delayedAnimationFrame(long currentTime) {
10981078
*/
10991079
boolean animationFrame(long currentTime) {
11001080
boolean done = false;
1101-
1102-
if (mPlayingState == STOPPED) {
1103-
mPlayingState = RUNNING;
1104-
if (mSeekTime < 0) {
1105-
mStartTime = currentTime;
1106-
} else {
1107-
mStartTime = currentTime - mSeekTime;
1108-
// Now that we're playing, reset the seek time
1109-
mSeekTime = -1;
1110-
}
1111-
}
11121081
switch (mPlayingState) {
11131082
case RUNNING:
11141083
case SEEKED:
@@ -1143,6 +1112,31 @@ boolean animationFrame(long currentTime) {
11431112
return done;
11441113
}
11451114

1115+
/**
1116+
* Processes a frame of the animation, adjusting the start time if needed.
1117+
*
1118+
* @param frameTime The frame time.
1119+
* @return true if the animation has ended.
1120+
*/
1121+
final boolean doAnimationFrame(long frameTime) {
1122+
if (mPlayingState == STOPPED) {
1123+
mPlayingState = RUNNING;
1124+
if (mSeekTime < 0) {
1125+
mStartTime = frameTime;
1126+
} else {
1127+
mStartTime = frameTime - mSeekTime;
1128+
// Now that we're playing, reset the seek time
1129+
mSeekTime = -1;
1130+
}
1131+
}
1132+
// The frame time might be before the start time during the first frame of
1133+
// an animation. The "current time" must always be on or after the start
1134+
// time to avoid animating frames at negative time intervals. In practice, this
1135+
// is very rare and only happens when seeking backwards.
1136+
final long currentTime = Math.max(frameTime, mStartTime);
1137+
return animationFrame(currentTime);
1138+
}
1139+
11461140
/**
11471141
* Returns the current animation fraction, which is the elapsed/interpolated fraction used in
11481142
* the most recent frame update on the animation.

core/java/android/view/Choreographer.java

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ protected Choreographer initialValue() {
6969
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
7070
"debug.choreographer.vsync", true);
7171

72+
// Enable/disable using the frame time instead of returning now.
73+
private static final boolean USE_FRAME_TIME = SystemProperties.getBoolean(
74+
"debug.choreographer.frametime", true);
75+
76+
private static final long NANOS_PER_MS = 1000000;
77+
7278
private static final int MSG_DO_FRAME = 0;
7379
private static final int MSG_DO_SCHEDULE_VSYNC = 1;
7480
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;
@@ -84,7 +90,8 @@ protected Choreographer initialValue() {
8490
private final CallbackQueue[] mCallbackQueues;
8591

8692
private boolean mFrameScheduled;
87-
private long mLastFrameTime;
93+
private boolean mCallbacksRunning;
94+
private long mLastFrameTimeNanos;
8895

8996
/**
9097
* Callback type: Input callback. Runs first.
@@ -108,7 +115,7 @@ private Choreographer(Looper looper) {
108115
mLooper = looper;
109116
mHandler = new FrameHandler(looper);
110117
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
111-
mLastFrameTime = Long.MIN_VALUE;
118+
mLastFrameTimeNanos = Long.MIN_VALUE;
112119

113120
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
114121
for (int i = 0; i <= CALLBACK_LAST; i++) {
@@ -270,6 +277,40 @@ public void removeCallbacks(int callbackType, Runnable action, Object token) {
270277
}
271278
}
272279

280+
/**
281+
* Gets the time when the current frame started. The frame time should be used
282+
* instead of {@link SystemClock#uptimeMillis()} to synchronize animations.
283+
* This helps to reduce inter-frame jitter because the frame time is fixed at the
284+
* time the frame was scheduled to start, regardless of when the animations or
285+
* drawing code actually ran.
286+
*
287+
* This method should only be called from within a callback.
288+
*
289+
* @return The frame start time, in the {@link SystemClock#uptimeMillis()} time base.
290+
*
291+
* @throws IllegalStateException if no frame is in progress.
292+
*/
293+
public long getFrameTime() {
294+
return getFrameTimeNanos() / NANOS_PER_MS;
295+
}
296+
297+
/**
298+
* Same as {@link #getFrameTime()} but with nanosecond precision.
299+
*
300+
* @return The frame start time, in the {@link System#nanoTime()} time base.
301+
*
302+
* @throws IllegalStateException if no frame is in progress.
303+
*/
304+
public long getFrameTimeNanos() {
305+
synchronized (mLock) {
306+
if (!mCallbacksRunning) {
307+
throw new IllegalStateException("This method must only be called as "
308+
+ "part of a callback while a frame is in progress.");
309+
}
310+
return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();
311+
}
312+
}
313+
273314
private void scheduleFrameLocked(long now) {
274315
if (!mFrameScheduled) {
275316
mFrameScheduled = true;
@@ -289,7 +330,8 @@ private void scheduleFrameLocked(long now) {
289330
mHandler.sendMessageAtFrontOfQueue(msg);
290331
}
291332
} else {
292-
final long nextFrameTime = Math.max(mLastFrameTime + sFrameDelay, now);
333+
final long nextFrameTime = Math.max(
334+
mLastFrameTimeNanos / NANOS_PER_MS + sFrameDelay, now);
293335
if (DEBUG) {
294336
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
295337
}
@@ -300,34 +342,43 @@ private void scheduleFrameLocked(long now) {
300342
}
301343
}
302344

303-
void doFrame(int frame) {
345+
void doFrame(long timestampNanos, int frame) {
304346
synchronized (mLock) {
305347
if (!mFrameScheduled) {
306348
return; // no work to do
307349
}
308350
mFrameScheduled = false;
309-
mLastFrameTime = SystemClock.uptimeMillis();
351+
mLastFrameTimeNanos = timestampNanos;
352+
}
353+
354+
final long startNanos;
355+
if (DEBUG) {
356+
startNanos = System.nanoTime();
310357
}
311358

312359
doCallbacks(Choreographer.CALLBACK_INPUT);
313360
doCallbacks(Choreographer.CALLBACK_ANIMATION);
314361
doCallbacks(Choreographer.CALLBACK_TRAVERSAL);
315362

316363
if (DEBUG) {
364+
final long endNanos = System.nanoTime();
317365
Log.d(TAG, "Frame " + frame + ": Finished, took "
318-
+ (SystemClock.uptimeMillis() - mLastFrameTime) + " ms.");
366+
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
367+
+ (startNanos - timestampNanos) * 0.000001f + " ms.");
319368
}
320369
}
321370

322371
void doCallbacks(int callbackType) {
323-
final long start;
324372
Callback callbacks;
325373
synchronized (mLock) {
326-
start = SystemClock.uptimeMillis();
327-
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(start);
374+
final long now = SystemClock.uptimeMillis();
375+
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now);
376+
if (callbacks == null) {
377+
return;
378+
}
379+
mCallbacksRunning = true;
328380
}
329-
330-
if (callbacks != null) {
381+
try {
331382
for (Callback c = callbacks; c != null; c = c.next) {
332383
if (DEBUG) {
333384
Log.d(TAG, "RunCallback: type=" + callbackType
@@ -336,8 +387,9 @@ void doCallbacks(int callbackType) {
336387
}
337388
c.action.run();
338389
}
339-
390+
} finally {
340391
synchronized (mLock) {
392+
mCallbacksRunning = false;
341393
do {
342394
final Callback next = callbacks.next;
343395
recycleCallbackLocked(callbacks);
@@ -404,7 +456,7 @@ public FrameHandler(Looper looper) {
404456
public void handleMessage(Message msg) {
405457
switch (msg.what) {
406458
case MSG_DO_FRAME:
407-
doFrame(0);
459+
doFrame(System.nanoTime(), 0);
408460
break;
409461
case MSG_DO_SCHEDULE_VSYNC:
410462
doScheduleVsync();
@@ -423,7 +475,7 @@ public FrameDisplayEventReceiver(Looper looper) {
423475

424476
@Override
425477
public void onVsync(long timestampNanos, int frame) {
426-
doFrame(frame);
478+
doFrame(timestampNanos, frame);
427479
}
428480
}
429481

0 commit comments

Comments
 (0)