1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 5 import static org.robolectric.RuntimeEnvironment.getApiLevel; 6 import static org.robolectric.shadow.api.Shadow.directlyOn; 7 8 import android.view.Choreographer; 9 import android.view.Choreographer.FrameCallback; 10 import android.view.RenderNodeAnimator; 11 import org.robolectric.annotation.Implementation; 12 import org.robolectric.annotation.Implements; 13 import org.robolectric.annotation.RealObject; 14 import org.robolectric.annotation.Resetter; 15 import org.robolectric.util.ReflectionHelpers; 16 17 @Implements(value = RenderNodeAnimator.class, isInAndroidSdk = false, minSdk = LOLLIPOP) 18 public class ShadowRenderNodeAnimator { 19 private static final int STATE_FINISHED = 3; 20 21 @RealObject RenderNodeAnimator realObject; 22 private Choreographer choreographer = Choreographer.getInstance(); 23 private boolean scheduled = false; 24 private long startTime = -1; 25 private boolean isEnding = false; 26 27 @Resetter 28 public static void reset() { 29 // sAnimationHelper is a static field used for processing delayed animations. Since it registers 30 // callbacks on the Choreographer, this is a problem if not reset between tests (as once the 31 // test is complete, its scheduled callbacks would be removed, but the static object would still 32 // believe it was registered and not re-register for the next test). 33 ReflectionHelpers.setStaticField( 34 RenderNodeAnimator.class, "sAnimationHelper", new ThreadLocal<>()); 35 } 36 37 @Implementation(minSdk = LOLLIPOP_MR1) 38 public void moveToRunningState() { 39 directlyOn(realObject, RenderNodeAnimator.class, "moveToRunningState"); 40 if (!isEnding) { 41 // Only schedule if this wasn't called during an end() call, as Robolectric will run any 42 // Choreographer callbacks synchronously when unpaused (and thus end up running the full 43 // animation even though RenderNodeAnimator just wanted to kick it into STATE_STARTED). 44 schedule(); 45 } 46 } 47 48 @Implementation 49 public void doStart() { 50 directlyOn(realObject, RenderNodeAnimator.class, "doStart"); 51 if (getApiLevel() <= LOLLIPOP) { 52 schedule(); 53 } 54 } 55 56 @Implementation 57 public void cancel() { 58 directlyOn(realObject, RenderNodeAnimator.class).cancel(); 59 60 if (getApiLevel() <= LOLLIPOP) { 61 int state = ReflectionHelpers.getField(realObject, "mState"); 62 if (state != STATE_FINISHED) { 63 // In 21, RenderNodeAnimator only calls nEnd, it doesn't call the Java end method. Thus, it 64 // expects the native code will end up calling onFinished, so we do that here. 65 directlyOn(realObject, RenderNodeAnimator.class, "onFinished"); 66 } 67 } 68 } 69 70 @Implementation 71 public void end() { 72 // Set this to true to prevent us from scheduling and running the full animation on the end() 73 // call. This can happen if the animation had not been started yet. 74 isEnding = true; 75 directlyOn(realObject, RenderNodeAnimator.class).end(); 76 isEnding = false; 77 unschedule(); 78 79 int state = ReflectionHelpers.getField(realObject, "mState"); 80 if (state != STATE_FINISHED) { 81 // This means that the RenderNodeAnimator called out to native code to finish the animation, 82 // expecting that it would end up calling onFinished. Since that won't happen in Robolectric, 83 // we call onFinished ourselves. 84 directlyOn(realObject, RenderNodeAnimator.class, "onFinished"); 85 } 86 } 87 88 private void schedule() { 89 if (!scheduled) { 90 scheduled = true; 91 choreographer.postFrameCallback(frameCallback); 92 } 93 } 94 95 private void unschedule() { 96 if (scheduled) { 97 choreographer.removeFrameCallback(frameCallback); 98 scheduled = false; 99 } 100 } 101 102 private FrameCallback frameCallback = new FrameCallback() { 103 @Override 104 public void doFrame(long frameTimeNanos) { 105 scheduled = false; 106 if (startTime == -1) { 107 startTime = frameTimeNanos; 108 } 109 110 long duration = realObject.getDuration(); 111 long curTime = frameTimeNanos - startTime; 112 if (curTime >= duration) { 113 directlyOn(realObject, RenderNodeAnimator.class, "onFinished"); 114 } else { 115 schedule(); 116 } 117 } 118 }; 119 }