aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
blob: cff9382ea1a9efb65da7e614dd3a371297c64be9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.R;
import static com.google.common.base.Preconditions.checkState;
import static org.robolectric.shadows.ShadowLooper.looperMode;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
import android.view.DisplayEventReceiver;
import java.time.Duration;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;

/**
 * The shadow API for {@link android.view.Choreographer}.
 *
 * <p>Different shadow implementations will be used depending on the current {@link LooperMode}. See
 * {@link ShadowLegacyChoreographer} and {@link ShadowPausedChoreographer} for details.
 */
@Implements(value = Choreographer.class, shadowPicker = ShadowChoreographer.Picker.class)
public abstract class ShadowChoreographer {

  @RealObject Choreographer realObject;
  private ChoreographerReflector reflector;

  private static volatile boolean isPaused = false;
  private static volatile Duration frameDelay = Duration.ofMillis(1);

  public static class Picker extends LooperShadowPicker<ShadowChoreographer> {

    public Picker() {
      super(ShadowLegacyChoreographer.class, ShadowPausedChoreographer.class);
    }
  }

  /**
   * Sets the delay between each frame. Note that the frames use the {@link ShadowSystemClock} and
   * so have the same fidelity, when using the paused looper mode (which is the only mode supported
   * by {@code ShadowDisplayEventReceiver}) the clock has millisecond fidelity.
   *
   * <p>Reasonable delays may be 15ms (approximating 60fps ~16.6ms), 10ms (approximating 90fps
   * ~11.1ms), and 30ms (approximating 30fps ~33.3ms). Choosing too small of a frame delay may
   * increase runtime as animation frames will have more steps.
   *
   * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
   */
  public static void setFrameDelay(Duration delay) {
    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
    frameDelay = delay;
  }

  /** See {@link #setFrameDelay(Duration)}. */
  public static Duration getFrameDelay() {
    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
    return frameDelay;
  }

  /**
   * Sets whether posting a frame should auto advance the clock or not. When paused the clock is not
   * auto advanced, when unpaused the clock is advanced by the frame delay every time a frame
   * callback is added. The default is not paused.
   *
   * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
   */
  public static void setPaused(boolean paused) {
    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
    isPaused = paused;
  }

  /** See {@link #setPaused(boolean)}. */
  public static boolean isPaused() {
    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
    return isPaused;
  }

  /**
   * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable,
   * Object)} is invoked. The default delay value is 0. This can be used to avoid infinite animation
   * tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in {@link
   * org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
   *
   * <p>Only supported in {@link LooperMode.Mode#LEGACY}
   *
   * @deprecated Use the {@link Mode#PAUSED} looper instead.
   */
  @Deprecated
  public static void setPostCallbackDelay(int delayMillis) {
    checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
    ShadowLegacyChoreographer.setPostCallbackDelay(delayMillis);
  }

  /**
   * Allows application to specify a fixed amount of delay when {@link
   * #postFrameCallback(FrameCallback)} is invoked. The default delay value is 0. This can be used
   * to avoid infinite animation tasks to be spawned when in LooperMode PAUSED or {@link
   * org.robolectric.util.Scheduler.IdleState#PAUSED} and displaying an animation.
   *
   * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setPaused(boolean)} and {@link
   *     #setFrameDelay(Duration)} to configure the vsync event behavior.
   */
  @Deprecated
  public static void setPostFrameCallbackDelay(int delayMillis) {
    if (looperMode() == Mode.LEGACY) {
      ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
    } else {
      setPaused(delayMillis != 0);
      setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
    }
  }

  /**
   * Return the current inter-frame interval.
   *
   * <p>Can only be used in {@link LooperMode.Mode#LEGACY}
   *
   * @return Inter-frame interval.
   * @deprecated Use the {@link Mode#PAUSED} looper and {@link #getFrameDelay()} to configure the
   *     frame delay.
   */
  @Deprecated
  public static long getFrameInterval() {
    checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
    return ShadowLegacyChoreographer.getFrameInterval();
  }

  /**
   * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
   *
   * <p>Only supported in {@link LooperMode.Mode#LEGACY}
   *
   * @param frameInterval Inter-frame interval.
   * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setFrameDelay(Duration)} to
   *     configure the frame delay.
   */
  @Deprecated
  public static void setFrameInterval(long frameInterval) {
    checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
    ShadowLegacyChoreographer.setFrameInterval(frameInterval);
  }

  @Implementation(maxSdk = R)
  protected void doFrame(long frameTimeNanos, int frame) {
    if (reflector == null) {
      reflector = reflector(ChoreographerReflector.class, realObject);
    }
    PerfStatsCollector.getInstance()
        .measure("doFrame", () -> reflector.doFrame(frameTimeNanos, frame));
  }

  @Resetter
  public static void reset() {
    isPaused = false;
    frameDelay = Duration.ofMillis(1);
  }

  /** Accessor interface for {@link Choreographer}'s internals */
  @ForType(Choreographer.class)
  protected interface ChoreographerReflector {
    @Accessor("sThreadInstance")
    @Static
    ThreadLocal<Choreographer> getThreadInstance();

    @Direct
    void doFrame(long frameTimeNanos, int frame);

    @Accessor("mDisplayEventReceiver")
    DisplayEventReceiver getReceiver();
  }
}