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