• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.R;
4 import static com.google.common.base.Preconditions.checkState;
5 import static org.robolectric.shadows.ShadowLooper.looperMode;
6 import static org.robolectric.util.reflector.Reflector.reflector;
7 
8 import android.view.Choreographer;
9 import android.view.Choreographer.FrameCallback;
10 import java.time.Duration;
11 import org.robolectric.annotation.Implementation;
12 import org.robolectric.annotation.Implements;
13 import org.robolectric.annotation.LooperMode;
14 import org.robolectric.annotation.LooperMode.Mode;
15 import org.robolectric.annotation.RealObject;
16 import org.robolectric.annotation.Resetter;
17 import org.robolectric.util.PerfStatsCollector;
18 import org.robolectric.util.reflector.Accessor;
19 import org.robolectric.util.reflector.Direct;
20 import org.robolectric.util.reflector.ForType;
21 import org.robolectric.util.reflector.Static;
22 
23 /**
24  * The shadow API for {@link android.view.Choreographer}.
25  *
26  * <p>Different shadow implementations will be used depending on the current {@link LooperMode}. See
27  * {@link ShadowLegacyChoreographer} and {@link ShadowPausedChoreographer} for details.
28  */
29 @Implements(value = Choreographer.class, shadowPicker = ShadowChoreographer.Picker.class)
30 public abstract class ShadowChoreographer {
31 
32   @RealObject Choreographer realObject;
33   private ChoreographerReflector reflector;
34 
35   private static volatile boolean isPaused = false;
36   private static volatile Duration frameDelay = Duration.ofMillis(1);
37 
38   public static class Picker extends LooperShadowPicker<ShadowChoreographer> {
39 
Picker()40     public Picker() {
41       super(ShadowLegacyChoreographer.class, ShadowPausedChoreographer.class);
42     }
43   }
44 
45   /**
46    * Sets the delay between each frame. Note that the frames use the {@link ShadowSystemClock} and
47    * so have the same fidelity, when using the paused looper mode (which is the only mode supported
48    * by {@code ShadowDisplayEventReceiver}) the clock has millisecond fidelity.
49    *
50    * <p>Reasonable delays may be 15ms (approximating 60fps ~16.6ms), 10ms (approximating 90fps
51    * ~11.1ms), and 30ms (approximating 30fps ~33.3ms). Choosing too small of a frame delay may
52    * increase runtime as animation frames will have more steps.
53    *
54    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
55    */
setFrameDelay(Duration delay)56   public static void setFrameDelay(Duration delay) {
57     checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
58     frameDelay = delay;
59   }
60 
61   /** See {@link #setFrameDelay(Duration)}. */
getFrameDelay()62   public static Duration getFrameDelay() {
63     checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
64     return frameDelay;
65   }
66 
67   /**
68    * Sets whether posting a frame should auto advance the clock or not. When paused the clock is not
69    * auto advanced, when unpaused the clock is advanced by the frame delay every time a frame
70    * callback is added. The default is not paused.
71    *
72    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
73    */
setPaused(boolean paused)74   public static void setPaused(boolean paused) {
75     checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
76     isPaused = paused;
77   }
78 
79   /** See {@link #setPaused(boolean)}. */
isPaused()80   public static boolean isPaused() {
81     checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
82     return isPaused;
83   }
84 
85   /**
86    * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable,
87    * Object)} is invoked. The default delay value is 0. This can be used to avoid infinite animation
88    * tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in {@link
89    * org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
90    *
91    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
92    *
93    * @deprecated Use the {@link Mode#PAUSED} looper instead.
94    */
95   @Deprecated
setPostCallbackDelay(int delayMillis)96   public static void setPostCallbackDelay(int delayMillis) {
97     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
98     ShadowLegacyChoreographer.setPostCallbackDelay(delayMillis);
99   }
100 
101   /**
102    * Allows application to specify a fixed amount of delay when {@link
103    * #postFrameCallback(FrameCallback)} is invoked. The default delay value is 0. This can be used
104    * to avoid infinite animation tasks to be spawned when in LooperMode PAUSED or {@link
105    * org.robolectric.util.Scheduler.IdleState#PAUSED} and displaying an animation.
106    *
107    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setPaused(boolean)} and {@link
108    *     #setFrameDelay(Duration)} to configure the vsync event behavior.
109    */
110   @Deprecated
setPostFrameCallbackDelay(int delayMillis)111   public static void setPostFrameCallbackDelay(int delayMillis) {
112     if (looperMode() == Mode.PAUSED) {
113       setPaused(delayMillis != 0);
114       setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
115     } else {
116       ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
117     }
118   }
119 
120   /**
121    * Return the current inter-frame interval.
122    *
123    * <p>Can only be used in {@link LooperMode.Mode#LEGACY}
124    *
125    * @return Inter-frame interval.
126    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #getFrameDelay()} to configure the
127    *     frame delay.
128    */
129   @Deprecated
getFrameInterval()130   public static long getFrameInterval() {
131     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
132     return ShadowLegacyChoreographer.getFrameInterval();
133   }
134 
135   /**
136    * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
137    *
138    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
139    *
140    * @param frameInterval Inter-frame interval.
141    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setFrameDelay(Duration)} to
142    *     configure the frame delay.
143    */
144   @Deprecated
setFrameInterval(long frameInterval)145   public static void setFrameInterval(long frameInterval) {
146     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
147     ShadowLegacyChoreographer.setFrameInterval(frameInterval);
148   }
149 
150   @Implementation(maxSdk = R)
doFrame(long frameTimeNanos, int frame)151   protected void doFrame(long frameTimeNanos, int frame) {
152     if (reflector == null) {
153       reflector = reflector(ChoreographerReflector.class, realObject);
154     }
155     PerfStatsCollector.getInstance()
156         .measure("doFrame", () -> reflector.doFrame(frameTimeNanos, frame));
157   }
158 
159   @Resetter
reset()160   public static void reset() {
161     isPaused = false;
162     frameDelay = Duration.ofMillis(1);
163   }
164 
165   /** Accessor interface for {@link Choreographer}'s internals */
166   @ForType(Choreographer.class)
167   protected interface ChoreographerReflector {
168     @Accessor("sThreadInstance")
169     @Static
getThreadInstance()170     ThreadLocal<Choreographer> getThreadInstance();
171 
172     @Direct
doFrame(long frameTimeNanos, int frame)173     void doFrame(long frameTimeNanos, int frame);
174   }
175 }
176