react-native-skia 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -147,7 +147,7 @@ logger.warn("react-native-skia: buildDir: ${buildDir}")
147
147
  logger.warn("react-native-skia: node_modules: ${nodeModules}")
148
148
 
149
149
  android {
150
- namespace "com.reactnative.skia"
150
+ namespace = "com.reactnative.skia"
151
151
 
152
152
  compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
153
153
  buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
@@ -155,10 +155,10 @@ android {
155
155
  // Used to override the NDK path/version on internal CI or by allowing
156
156
  // users to customize the NDK path/version from their root project.
157
157
  if (rootProject.hasProperty("ndkPath")) {
158
- ndkPath rootProject.ext.ndkPath
158
+ ndkPath = rootProject.ext.ndkPath
159
159
  }
160
160
  if (rootProject.hasProperty("ndkVersion")) {
161
- ndkVersion rootProject.ext.ndkVersion
161
+ ndkVersion = rootProject.ext.ndkVersion
162
162
  }
163
163
 
164
164
  defaultConfig {
@@ -181,8 +181,15 @@ android {
181
181
  }
182
182
  }
183
183
  }
184
+ // The Java sources use switch expressions (Java 14+). React Native's Gradle
185
+ // plugin aligns every module on 17 from the app's plugin block, but a host
186
+ // that opts out of that alignment must still compile this library.
187
+ compileOptions {
188
+ sourceCompatibility = JavaVersion.VERSION_17
189
+ targetCompatibility = JavaVersion.VERSION_17
190
+ }
184
191
  lintOptions {
185
- abortOnError false
192
+ abortOnError = false
186
193
  }
187
194
 
188
195
  publishing {
@@ -214,12 +221,12 @@ android {
214
221
  }
215
222
 
216
223
  buildFeatures {
217
- prefab true
218
- prefabPublishing true
224
+ prefab = true
225
+ prefabPublishing = true
219
226
  }
220
227
  prefab {
221
228
  rnskia {
222
- headers "${project.buildDir}/headers/rnskia/"
229
+ headers = "${project.buildDir}/headers/rnskia/"
223
230
  }
224
231
  }
225
232
  }
@@ -238,6 +245,7 @@ repositories {
238
245
  dependencies {
239
246
  //noinspection GradleDynamicVersion
240
247
  implementation 'com.facebook.react:react-native:+' // From node_modules
248
+ testImplementation 'junit:junit:4.13.2'
241
249
  }
242
250
 
243
251
  // Package all the cpp code in a flattened directory structure
@@ -0,0 +1,24 @@
1
+ package com.reactnative.skia;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ /** The Android view a SkiaView draws into, see SkiaView.updateView(). */
6
+ enum BackingViewKind {
7
+ SURFACE_VIEW,
8
+ TEXTURE_VIEW;
9
+
10
+ /**
11
+ * The kind the androidSurfaceType prop asks for, or null for "auto", which
12
+ * leaves the choice to the opaque prop.
13
+ */
14
+ @Nullable
15
+ static BackingViewKind fromSurfaceType(@Nullable String surfaceType) {
16
+ if ("SurfaceView".equals(surfaceType)) {
17
+ return SURFACE_VIEW;
18
+ }
19
+ if ("TextureView".equals(surfaceType)) {
20
+ return TEXTURE_VIEW;
21
+ }
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,104 @@
1
+ package com.reactnative.skia;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ /**
6
+ * Decides when a SkiaView presents the recordings queued for it: a SurfaceView
7
+ * on the next vsync, a TextureView behind the draw its window has pending, so
8
+ * that the window has taken the previous frame by then. Keeps at most one
9
+ * frame callback and one posted present outstanding. Main thread.
10
+ */
11
+ final class FrameScheduler {
12
+ /** What the scheduler asks of the view. */
13
+ interface Host {
14
+ /** Calls onFrame() on the next Choreographer frame. */
15
+ void postFrameCallback();
16
+
17
+ /** Calls onPosted() from the main looper, behind any draw the window has pending. */
18
+ void postBehindPendingDraw();
19
+
20
+ /** Presents the queued recordings, returning whether any are left. */
21
+ boolean presentFrame();
22
+ }
23
+
24
+ private final Host mHost;
25
+ private boolean mFrameCallbackPosted = false;
26
+ private boolean mPresentPosted = false;
27
+
28
+ FrameScheduler(Host host) {
29
+ mHost = host;
30
+ }
31
+
32
+ /** A recording was submitted. Without a backing view there is no surface to present on yet. */
33
+ void requestFrame(@Nullable BackingViewKind kind) {
34
+ if (kind == null) {
35
+ return;
36
+ }
37
+ // A frame callback still outstanding (a retry, see presentOrRetry) presents
38
+ // the new recording along with the leftovers on that vsync.
39
+ if (mFrameCallbackPosted) {
40
+ return;
41
+ }
42
+ if (presentsOnVsync(kind)) {
43
+ postFrameCallback();
44
+ } else {
45
+ postBehindPendingDraw();
46
+ }
47
+ }
48
+
49
+ /** The Choreographer frame asked for with Host.postFrameCallback(). */
50
+ void onFrame(@Nullable BackingViewKind kind) {
51
+ mFrameCallbackPosted = false;
52
+ if (kind == null) {
53
+ return;
54
+ }
55
+ if (presentsOnVsync(kind)) {
56
+ presentOrRetry();
57
+ } else {
58
+ postBehindPendingDraw();
59
+ }
60
+ }
61
+
62
+ /** The message posted with Host.postBehindPendingDraw(). */
63
+ void onPosted() {
64
+ mPresentPosted = false;
65
+ presentOrRetry();
66
+ }
67
+
68
+ /** The view left its window, and the host removed both callbacks. */
69
+ void cancel() {
70
+ mFrameCallbackPosted = false;
71
+ mPresentPosted = false;
72
+ }
73
+
74
+ // The compositor takes a SurfaceView's buffer at the vsync. A TextureView's frame
75
+ // shows when its window draws, and its buffer queue replaces a frame not drawn yet.
76
+ private static boolean presentsOnVsync(BackingViewKind kind) {
77
+ return switch (kind) {
78
+ case SURFACE_VIEW -> true;
79
+ case TEXTURE_VIEW -> false;
80
+ };
81
+ }
82
+
83
+ // Recordings left over (the present failed, or more were submitted meanwhile)
84
+ // are tried again on the next vsync, never straight away.
85
+ private void presentOrRetry() {
86
+ if (mHost.presentFrame()) {
87
+ postFrameCallback();
88
+ }
89
+ }
90
+
91
+ private void postFrameCallback() {
92
+ if (!mFrameCallbackPosted) {
93
+ mFrameCallbackPosted = true;
94
+ mHost.postFrameCallback();
95
+ }
96
+ }
97
+
98
+ private void postBehindPendingDraw() {
99
+ if (!mPresentPosted) {
100
+ mPresentPosted = true;
101
+ mHost.postBehindPendingDraw();
102
+ }
103
+ }
104
+ }
@@ -8,6 +8,8 @@ import android.view.MotionEvent;
8
8
  import android.view.Surface;
9
9
  import android.view.View;
10
10
 
11
+ import androidx.annotation.Nullable;
12
+
11
13
  import com.facebook.jni.HybridData;
12
14
  import com.facebook.jni.annotations.DoNotStrip;
13
15
  import com.facebook.react.bridge.ReactContext;
@@ -17,14 +19,10 @@ import com.facebook.react.views.view.ReactViewGroup;
17
19
  /**
18
20
  * The Android view behind <Canvas>, <SkiaPictureView> and <SkiaGraphiteView>.
19
21
  * It is backed by a SurfaceView or a TextureView (see updateView()) that the
20
- * native side draws into with Graphite, presenting the queued recordings on
21
- * the Choreographer.
22
+ * native side draws into with Graphite, presenting the queued recordings when
23
+ * the backing view can show them (see FrameScheduler).
22
24
  */
23
25
  public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreographer.FrameCallback {
24
- // Backing view kinds, see updateView().
25
- private static final int KIND_SURFACE_VIEW = 0;
26
- private static final int KIND_TEXTURE_VIEW = 1;
27
-
28
26
  private static final String TAG = "SkiaView";
29
27
 
30
28
  @DoNotStrip
@@ -32,18 +30,36 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
32
30
 
33
31
  private View mView;
34
32
 
35
- // Props, applied together in updateView().
33
+ // Props, applied together in updateView(). A null kind is "auto".
36
34
  private boolean mOpaque = false;
37
- private String mSurfaceType = "auto";
35
+ @Nullable
36
+ private BackingViewKind mRequestedKind = null;
38
37
  private boolean mZOrderOnTop = false;
39
38
  private boolean mHighBitDepth = false;
40
39
 
41
- // What the current backing view was created with.
42
- private int mAppliedKind = -1;
40
+ // What the current backing view was created with; no kind before the first one.
41
+ @Nullable
42
+ private BackingViewKind mAppliedKind = null;
43
43
  private boolean mAppliedZOrderOnTop;
44
44
  private boolean mAppliedHighBitDepth;
45
45
 
46
- private boolean mFramePending = false;
46
+ private final Runnable mPostedPresent = this::onPostedPresent;
47
+ private final FrameScheduler mFrameScheduler = new FrameScheduler(new FrameScheduler.Host() {
48
+ @Override
49
+ public void postFrameCallback() {
50
+ Choreographer.getInstance().postFrameCallback(SkiaView.this);
51
+ }
52
+
53
+ @Override
54
+ public void postBehindPendingDraw() {
55
+ post(mPostedPresent);
56
+ }
57
+
58
+ @Override
59
+ public boolean presentFrame() {
60
+ return SkiaView.this.presentFrame();
61
+ }
62
+ });
47
63
 
48
64
  public SkiaView(Context context) {
49
65
  super(context);
@@ -65,8 +81,8 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
65
81
  mOpaque = value;
66
82
  }
67
83
 
68
- public void setSurfaceType(String value) {
69
- mSurfaceType = value == null ? "auto" : value;
84
+ public void setSurfaceType(@Nullable String value) {
85
+ mRequestedKind = BackingViewKind.fromSurfaceType(value);
70
86
  }
71
87
 
72
88
  public void setZOrderOnTop(boolean value) {
@@ -79,25 +95,22 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
79
95
 
80
96
  // Resolve the backing view from the props. "auto" picks SurfaceView for an
81
97
  // opaque canvas and TextureView for a non-opaque one.
82
- private int resolveKind() {
83
- if ("SurfaceView".equals(mSurfaceType)) {
84
- return KIND_SURFACE_VIEW;
98
+ private BackingViewKind resolveKind() {
99
+ if (mRequestedKind != null) {
100
+ return mRequestedKind;
85
101
  }
86
- if ("TextureView".equals(mSurfaceType)) {
87
- return KIND_TEXTURE_VIEW;
88
- }
89
- return mOpaque ? KIND_SURFACE_VIEW : KIND_TEXTURE_VIEW;
102
+ return mOpaque ? BackingViewKind.SURFACE_VIEW : BackingViewKind.TEXTURE_VIEW;
90
103
  }
91
104
 
92
105
  // The 10-bit buffer format only has 2 bits of alpha, which would visibly
93
106
  // break translucency, and the extra precision would be lost in the 8-bit
94
107
  // composition pass of a TextureView anyway. So the flag only applies to an
95
108
  // opaque SurfaceView.
96
- private boolean resolveHighBitDepth(int kind) {
109
+ private boolean resolveHighBitDepth(BackingViewKind kind) {
97
110
  if (!mHighBitDepth) {
98
111
  return false;
99
112
  }
100
- if (kind != KIND_SURFACE_VIEW || !mOpaque) {
113
+ if (kind != BackingViewKind.SURFACE_VIEW || !mOpaque) {
101
114
  Log.w(TAG, "highBitDepth requires an opaque SurfaceView on Android, falling back to the 8-bit format");
102
115
  return false;
103
116
  }
@@ -109,8 +122,8 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
109
122
  // it attaches (zOrderOnTop, the buffer format), replaces the child; opacity
110
123
  // is applied in place.
111
124
  void updateView() {
112
- int kind = resolveKind();
113
- boolean zOrderOnTop = kind == KIND_SURFACE_VIEW && mZOrderOnTop;
125
+ BackingViewKind kind = resolveKind();
126
+ boolean zOrderOnTop = kind == BackingViewKind.SURFACE_VIEW && mZOrderOnTop;
114
127
  boolean highBitDepth = resolveHighBitDepth(kind);
115
128
  if (mView == null
116
129
  || kind != mAppliedKind
@@ -122,9 +135,10 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
122
135
  mAppliedKind = kind;
123
136
  mAppliedZOrderOnTop = zOrderOnTop;
124
137
  mAppliedHighBitDepth = highBitDepth;
125
- mView = kind == KIND_SURFACE_VIEW
126
- ? new SkiaSurfaceView(getContext(), this, zOrderOnTop, mOpaque)
127
- : new SkiaTextureView(getContext(), this, mOpaque);
138
+ mView = switch (kind) {
139
+ case SURFACE_VIEW -> new SkiaSurfaceView(getContext(), this, zOrderOnTop, mOpaque);
140
+ case TEXTURE_VIEW -> new SkiaTextureView(getContext(), this, mOpaque);
141
+ };
128
142
  addView(mView);
129
143
  // React Native sizes native children explicitly through onLayout, so
130
144
  // the requestLayout triggered by addView is ignored; size the new
@@ -132,10 +146,11 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
132
146
  if (getWidth() > 0 || getHeight() > 0) {
133
147
  mView.layout(0, 0, getWidth(), getHeight());
134
148
  }
135
- } else if (kind == KIND_SURFACE_VIEW) {
136
- ((SkiaSurfaceView) mView).setOpaque(mOpaque);
137
149
  } else {
138
- ((SkiaTextureView) mView).setOpaque(mOpaque);
150
+ switch (kind) {
151
+ case SURFACE_VIEW -> ((SkiaSurfaceView) mView).setOpaque(mOpaque);
152
+ case TEXTURE_VIEW -> ((SkiaTextureView) mView).setOpaque(mOpaque);
153
+ }
139
154
  }
140
155
  }
141
156
 
@@ -157,31 +172,27 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
157
172
 
158
173
  // Frames --------------------------------------------------------------
159
174
 
160
- /**
161
- * Presents the queued recordings on the next vsync. Main thread; called
162
- * from native when a recording is submitted.
163
- */
175
+ /** A recording was submitted. Main thread; called from native. */
164
176
  @DoNotStrip
165
177
  public void scheduleFrame() {
166
- if (!mFramePending) {
167
- mFramePending = true;
168
- Choreographer.getInstance().postFrameCallback(this);
169
- }
178
+ mFrameScheduler.requestFrame(mAppliedKind);
170
179
  }
171
180
 
172
181
  @Override
173
182
  public void doFrame(long frameTimeNanos) {
174
- mFramePending = false;
175
- if (presentFrame()) {
176
- scheduleFrame();
177
- }
183
+ mFrameScheduler.onFrame(mAppliedKind);
184
+ }
185
+
186
+ private void onPostedPresent() {
187
+ mFrameScheduler.onPosted();
178
188
  }
179
189
 
180
190
  @Override
181
191
  protected void onDetachedFromWindow() {
182
192
  super.onDetachedFromWindow();
183
193
  Choreographer.getInstance().removeFrameCallback(this);
184
- mFramePending = false;
194
+ removeCallbacks(mPostedPresent);
195
+ mFrameScheduler.cancel();
185
196
  }
186
197
 
187
198
  @Override
@@ -242,6 +253,6 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
242
253
 
243
254
  private native void unregisterView();
244
255
 
245
- /** Choreographer tick: returns whether more recordings are waiting. */
256
+ /** Presents the queued recordings, returning whether any are left. */
246
257
  private native boolean presentFrame();
247
258
  }
@@ -0,0 +1,186 @@
1
+ package com.reactnative.skia;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+
5
+ import java.util.ArrayDeque;
6
+ import java.util.ArrayList;
7
+ import java.util.Arrays;
8
+ import java.util.Deque;
9
+ import java.util.List;
10
+
11
+ import org.junit.Test;
12
+
13
+ public class FrameSchedulerTest {
14
+ private enum Call {
15
+ POST_FRAME_CALLBACK,
16
+ POST_BEHIND_PENDING_DRAW,
17
+ PRESENT
18
+ }
19
+
20
+ private static final class RecordingHost implements FrameScheduler.Host {
21
+ final List<Call> calls = new ArrayList<>();
22
+ final Deque<Boolean> leftOverAfterPresent = new ArrayDeque<>();
23
+
24
+ @Override
25
+ public void postFrameCallback() {
26
+ calls.add(Call.POST_FRAME_CALLBACK);
27
+ }
28
+
29
+ @Override
30
+ public void postBehindPendingDraw() {
31
+ calls.add(Call.POST_BEHIND_PENDING_DRAW);
32
+ }
33
+
34
+ @Override
35
+ public boolean presentFrame() {
36
+ calls.add(Call.PRESENT);
37
+ return !leftOverAfterPresent.isEmpty() && leftOverAfterPresent.removeFirst();
38
+ }
39
+
40
+ List<Call> takeCalls() {
41
+ List<Call> taken = new ArrayList<>(calls);
42
+ calls.clear();
43
+ return taken;
44
+ }
45
+ }
46
+
47
+ private final RecordingHost mHost = new RecordingHost();
48
+ private final FrameScheduler mScheduler = new FrameScheduler(mHost);
49
+
50
+ @Test
51
+ public void surfaceViewFrameIsPresentedOnTheNextVsync() {
52
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
53
+ assertEquals(Arrays.asList(Call.POST_FRAME_CALLBACK), mHost.takeCalls());
54
+
55
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
56
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
57
+ }
58
+
59
+ @Test
60
+ public void textureViewFrameIsPresentedBehindThePendingDraw() {
61
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
62
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW), mHost.takeCalls());
63
+
64
+ mScheduler.onPosted();
65
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
66
+ }
67
+
68
+ @Test
69
+ public void textureViewIsNeverPresentedInsideAChoreographerFrame() {
70
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
71
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW), mHost.takeCalls());
72
+ }
73
+
74
+ @Test
75
+ public void textureViewLeftoverIsPresentedBehindTheDrawOfTheNextVsync() {
76
+ mHost.leftOverAfterPresent.add(true);
77
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
78
+ mScheduler.onPosted();
79
+ assertEquals(
80
+ Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT, Call.POST_FRAME_CALLBACK),
81
+ mHost.takeCalls());
82
+
83
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
84
+ mScheduler.onPosted();
85
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT), mHost.takeCalls());
86
+ }
87
+
88
+ @Test
89
+ public void textureViewRequestWhileARetryIsOutstandingWaitsForThatVsync() {
90
+ mHost.leftOverAfterPresent.add(true);
91
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
92
+ mScheduler.onPosted();
93
+ assertEquals(
94
+ Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT, Call.POST_FRAME_CALLBACK),
95
+ mHost.takeCalls());
96
+
97
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
98
+ assertEquals(Arrays.asList(), mHost.takeCalls());
99
+
100
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
101
+ mScheduler.onPosted();
102
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT), mHost.takeCalls());
103
+ }
104
+
105
+ @Test
106
+ public void surfaceViewLeftoverIsPresentedOnTheNextVsync() {
107
+ mHost.leftOverAfterPresent.add(true);
108
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
109
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
110
+ assertEquals(
111
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.PRESENT, Call.POST_FRAME_CALLBACK),
112
+ mHost.takeCalls());
113
+
114
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
115
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
116
+ }
117
+
118
+ @Test
119
+ public void requestsWaitingForTheSameFrameArePostedOnce() {
120
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
121
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
122
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
123
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
124
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
125
+ assertEquals(
126
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.PRESENT, Call.POST_BEHIND_PENDING_DRAW),
127
+ mHost.takeCalls());
128
+ }
129
+
130
+ @Test
131
+ public void requestAfterTheFrameRanIsPostedAgain() {
132
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
133
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
134
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
135
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
136
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
137
+ mScheduler.onPosted();
138
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
139
+ assertEquals(
140
+ Arrays.asList(
141
+ Call.POST_FRAME_CALLBACK,
142
+ Call.PRESENT,
143
+ Call.POST_FRAME_CALLBACK,
144
+ Call.PRESENT,
145
+ Call.POST_BEHIND_PENDING_DRAW,
146
+ Call.PRESENT,
147
+ Call.POST_BEHIND_PENDING_DRAW),
148
+ mHost.takeCalls());
149
+ }
150
+
151
+ @Test
152
+ public void kindSwitchWhileAFrameCallbackIsOutstandingPresentsBehindThePendingDraw() {
153
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
154
+ // updateView() replaced the SurfaceView with a TextureView meanwhile.
155
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
156
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
157
+ mScheduler.onPosted();
158
+ assertEquals(
159
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT),
160
+ mHost.takeCalls());
161
+ }
162
+
163
+ @Test
164
+ public void withoutABackingViewNothingIsPostedOrPresented() {
165
+ mScheduler.requestFrame(null);
166
+ mScheduler.onFrame(null);
167
+ assertEquals(Arrays.asList(), mHost.takeCalls());
168
+ }
169
+
170
+ @Test
171
+ public void cancelDropsOutstandingRequests() {
172
+ for (BackingViewKind kind : BackingViewKind.values()) {
173
+ mScheduler.requestFrame(kind);
174
+ mScheduler.cancel();
175
+ mScheduler.requestFrame(kind);
176
+ mScheduler.cancel();
177
+ }
178
+ assertEquals(
179
+ Arrays.asList(
180
+ Call.POST_FRAME_CALLBACK,
181
+ Call.POST_FRAME_CALLBACK,
182
+ Call.POST_BEHIND_PENDING_DRAW,
183
+ Call.POST_BEHIND_PENDING_DRAW),
184
+ mHost.takeCalls());
185
+ }
186
+ }
@@ -38,7 +38,8 @@ namespace jsi = facebook::jsi;
38
38
  * (SkiaViewApi.makeGraphiteContext), or <Canvas> hands over a recorder (or a
39
39
  * picture) that the render thread pool records for the view. Either way the
40
40
  * recordings are queued on the target; the view presents them onto its canvas
41
- * provider on the main thread, aligned with the platform's display link.
41
+ * provider on the main thread, once the platform view's surface can show a new
42
+ * frame (see setFrameScheduler).
42
43
  *
43
44
  * Threading: the registry side (RNSkJsiViewApi) runs on the JS thread, except
44
45
  * applyUpdates() which a Reanimated mapper calls on the UI runtime; snapshots
@@ -248,8 +249,10 @@ public:
248
249
  // Platform view side -------------------------------------------------------
249
250
 
250
251
  /**
251
- Installed by the platform view: arms its display link. Main thread. Without
252
- one, frames are presented as soon as the main thread gets to them.
252
+ Installed by the platform view, which calls presentFrame() once its surface
253
+ can show a new frame: on the display link on iOS, straight away on macOS,
254
+ and on Android as its FrameScheduler decides. Main thread. Without one,
255
+ frames are presented as soon as the main thread gets to them.
253
256
  */
254
257
  void setFrameScheduler(std::function<void()> scheduler) {
255
258
  _frameScheduler = std::move(scheduler);
@@ -265,10 +268,10 @@ public:
265
268
  }
266
269
 
267
270
  /**
268
- Display link tick: presents the queued recordings. Returns whether more
269
- are waiting, so that the caller keeps its display link armed: also when the
270
- present failed (the app is in the background), so that the recordings are
271
- tried again on the next frame rather than left waiting for a redraw.
271
+ Presents the queued recordings (see setFrameScheduler for when). Returns
272
+ whether more are waiting, so that the platform view asks again: also when
273
+ the present failed (the app is in the background), so that the recordings
274
+ are tried again on the next frame rather than left waiting for a redraw.
272
275
  Without a surface the queue is left alone: the surface presents it when
273
276
  it appears.
274
277
  */
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "setup-skia-web": "scripts/setup-canvaskit.js"
9
9
  },
10
10
  "title": "React Native Skia",
11
- "version": "3.0.3",
11
+ "version": "3.0.4",
12
12
  "description": "High-performance React Native Graphics using Skia",
13
13
  "main": "lib/module/index.js",
14
14
  "react-native": "src/index.ts",