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