Home | History | Annotate | Download | only in shadows
      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 }