react-native-smooth-clip-view 0.4.6 → 0.5.0
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.
- package/CHANGELOG.md +55 -0
- package/README.md +47 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +18 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
- package/cpp/SmoothClipAnimationCurve.h +36 -11
- package/cpp/SmoothClipRegistry.h +11 -4
- package/ios/SmoothClipBackdropView.h +17 -0
- package/ios/SmoothClipBackdropView.mm +267 -0
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +713 -22
- package/ios/SmoothClipViewRegistry.h +14 -0
- package/lib/module/SmoothClipBackdropView.js +40 -0
- package/lib/module/SmoothClipBackdropView.js.map +1 -0
- package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/lib/module/SmoothClipView.ios.js +17 -1
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
- package/lib/module/geometry.js +44 -6
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groups.native.js +10 -3
- package/lib/module/groups.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationCodec.js +6 -2
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
- package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +19 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groups.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/SmoothClipBackdropView.tsx +53 -0
- package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/src/SmoothClipView.android.tsx +4 -1
- package/src/SmoothClipView.ios.tsx +24 -1
- package/src/SmoothClipView.tsx +4 -1
- package/src/SmoothClipViewNativeComponent.ts +4 -0
- package/src/geometry.ts +59 -4
- package/src/groups.native.ts +9 -3
- package/src/index.ts +7 -1
- package/src/presentationCodec.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/kbrattli/react-native-smooth-clip-view/releases/tag/v0.5.0) — 2026-09-28
|
|
4
|
+
|
|
5
|
+
- **Backdrop channel.** A presentation gains `backdrop: { translateX,
|
|
6
|
+
translateY }`, and the new `SmoothClipBackdropView` (`controller={clip}`)
|
|
7
|
+
is translated by it: every `setFrame` writes the channel in the same native
|
|
8
|
+
call as the clip, and every run animates it on the clip's own epoch (a
|
|
9
|
+
translation group on the shared Core Animation `beginTime` on iOS, the same
|
|
10
|
+
Choreographer advance on Android). Content that must stay locked to the
|
|
11
|
+
aperture no longer needs a Reanimated mapper, which can land a frame after
|
|
12
|
+
the clip. The native packet grows by two values (stride 23 → 25) and the
|
|
13
|
+
host takes `initialBackdropTranslateX/Y`, so a native rebuild is required;
|
|
14
|
+
a JavaScript-only update cannot upgrade an older native build.
|
|
15
|
+
- iOS anchors a native run to the frame stamp JS passes (Reanimated's
|
|
16
|
+
`__frameTimestamp`, the display link's target vsync) as the shared Core
|
|
17
|
+
Animation `beginTime`, the way the Android frame loop already did. A run and
|
|
18
|
+
a `withTiming` begun in the same UI frame now trace one epoch; before, the
|
|
19
|
+
run began at its install time and led the Reanimated model by the rest of
|
|
20
|
+
that frame (about 14 ms at 60 Hz). Animation groups carry backwards fill so a
|
|
21
|
+
stamp up to one frame ahead of the commit shows the run's first frame, not
|
|
22
|
+
the target. JS now stamps runs on every platform.
|
|
23
|
+
- `shadowRendering="baked"` on `SmoothClipView` renders the box shadow from one
|
|
24
|
+
pre-blurred tile per 4 pt corner-radius step that the compositor stretches
|
|
25
|
+
(`contentsCenter` on iOS, nine bitmap pieces on Android), so an animated or
|
|
26
|
+
dragged aperture costs no blur per frame. iOS bakes the tile through Core
|
|
27
|
+
Animation's own shadow path so it matches a `shadowPath` layer; Android
|
|
28
|
+
bakes it with the same `BlurMaskFilter` as the blur path. Runs animate the
|
|
29
|
+
tile layer's frame and opacity and swap tiles in steps where the radius,
|
|
30
|
+
blur or colour changes. Uniform radii only, and a shape at least
|
|
31
|
+
2 × (1.5 × blur + radius) on a side; unequal radii or a smaller shape keep
|
|
32
|
+
the blur path. Default stays `"blur"`.
|
|
33
|
+
- Android builds a uniform circular corner with `addRoundRect` in the shared
|
|
34
|
+
path builder, so the shadow path (not only the clip) stays an rrect the
|
|
35
|
+
renderer can clip and blur analytically. Paths are `rewind()` instead of
|
|
36
|
+
`reset()` between frames, keeping their storage.
|
|
37
|
+
- Baked tiles no longer bake inside the frame. A shadow's first tile bakes on
|
|
38
|
+
the calling thread (at mount, when nothing else can be shown) and a run's
|
|
39
|
+
resting target bakes at install, so the run ends exact; every other radius
|
|
40
|
+
step bakes off the frame (one per main-queue turn on iOS, a background
|
|
41
|
+
thread on Android) while the nearest cached step stands in, and the model
|
|
42
|
+
takes the exact tile when it lands. A run whose intermediate steps are not
|
|
43
|
+
cached yet swaps to the next cached, rounder tile at the missing step's key
|
|
44
|
+
time, so the shown corner is never tighter than the clip's.
|
|
45
|
+
- iOS discrete tile swaps carry the closing key time Core Animation's discrete
|
|
46
|
+
mode requires (`keyTimes` has one more entry than `values`, ending at 1), so
|
|
47
|
+
each swap lands where the radius crosses its step rather than on an even
|
|
48
|
+
grid.
|
|
49
|
+
- iOS trusts a frame stamp only within the same ±1 s window as Android. A
|
|
50
|
+
group held pending longer (a host that could not display, the app inactive)
|
|
51
|
+
or a stamp from a rescaled clock starts now instead of beginning fully
|
|
52
|
+
elapsed.
|
|
53
|
+
- `canonicalizeClipPresentation` validates and canonicalizes in one pass: the
|
|
54
|
+
shadow colour, the geometry and the rotation were each parsed twice per
|
|
55
|
+
`setFrame`. The iOS packet reader reads the array length once per
|
|
56
|
+
presentation instead of once per value.
|
|
57
|
+
|
|
3
58
|
## [0.4.6](https://github.com/kbrattli/react-native-smooth-clip-view/releases/tag/v0.4.6) — 2026-09-20
|
|
4
59
|
|
|
5
60
|
- Draw `continuous` corners on Android as a Figma smoothed corner (smoothing
|
package/README.md
CHANGED
|
@@ -224,6 +224,37 @@ APIs share validation, run ownership, and completion behavior.
|
|
|
224
224
|
curve change are static-only: set them with `setFrame`; `animateTo` returns
|
|
225
225
|
`null`.
|
|
226
226
|
- One outset `boxShadow` is supported. It escapes the aperture but not the host.
|
|
227
|
+
- `shadowRendering="baked"` on the host draws that shadow from one pre-blurred
|
|
228
|
+
tile per corner-radius step (4 pt), stretched by the compositor, instead of
|
|
229
|
+
blurring a path on every frame the aperture moves: a native run or a drag of
|
|
230
|
+
a full-screen window then costs no per-frame blur on either platform. A tile
|
|
231
|
+
is baked once per (radius step, blur, colour), about a millisecond for a
|
|
232
|
+
16 pt blur, and cached for the process. Only two bakes happen on the calling
|
|
233
|
+
thread: a shadow's first tile (at mount, when nothing else can be shown)
|
|
234
|
+
and a run's resting target (at install, so the run ends exact). Every other
|
|
235
|
+
step bakes off the frame while the nearest cached step stands in, so a drag
|
|
236
|
+
that crosses radius steps never blocks a frame. It needs uniform corner radii
|
|
237
|
+
and a shape at least 2 × (1.5 × blur + radius) on each side, room for two
|
|
238
|
+
corner slices (unequal radii or a smaller shape keep the blur path for that
|
|
239
|
+
frame or run); the tile corner
|
|
240
|
+
rounds up to the next step, so a shadow corner may be up to 4 pt rounder
|
|
241
|
+
than its clip, which no blur the tile was made for resolves; a run that
|
|
242
|
+
changes radius, blur or colour swaps tiles in steps along the way. The baked
|
|
243
|
+
shadow is drawn under the aperture on both platforms (the Android blur path
|
|
244
|
+
cuts it out), so content over it should be opaque.
|
|
245
|
+
- `backdrop: { translateX, translateY }` on a presentation translates every
|
|
246
|
+
`SmoothClipBackdropView` bound to the same controller (`controller={clip}`,
|
|
247
|
+
anywhere in the tree, any number of them). The channel is part of the
|
|
248
|
+
presentation, so a `setFrame` writes it in the same native call as the clip
|
|
249
|
+
and a run animates it on the clip's own epoch: content that must stay
|
|
250
|
+
locked to the aperture (a screen-sized canvas centred in the window) is
|
|
251
|
+
sampled by the same clock as the clip, with no Reanimated mapper in
|
|
252
|
+
between. The translation goes on an inner content layer; the view's own
|
|
253
|
+
`transform` style stays React Native's. A backdrop that binds while a run
|
|
254
|
+
is in flight adopts the driver's current value and follows from the next
|
|
255
|
+
run. While no backdrop is bound the channel is not interpolated: a snapshot
|
|
256
|
+
or freeze taken mid-run reports the run's target translation, which is what
|
|
257
|
+
a backdrop binding at that moment adopts.
|
|
227
258
|
- A fully off-host aperture is not touchable, even when only its shadow overlaps.
|
|
228
259
|
- Descendant accessibility is hidden during autonomous native motion and restored
|
|
229
260
|
from aperture/host intersection at the endpoint.
|
|
@@ -281,6 +312,22 @@ See the [changelog](./CHANGELOG.md) for release details.
|
|
|
281
312
|
|
|
282
313
|
- One worklet-to-native call per `ui.setFrame`, or per group batch.
|
|
283
314
|
- Native timing and springs run without JS work between start and completion.
|
|
315
|
+
- A run started inside a UI frame is anchored to that frame's Reanimated stamp
|
|
316
|
+
(`__frameTimestamp`) on both platforms, so it shares one epoch with a
|
|
317
|
+
`withTiming` begun in the same frame.
|
|
318
|
+
- A host with no run in flight and no `setFrame` stream costs nothing per
|
|
319
|
+
frame: iOS keeps no display link, and Android posts its frame callback only
|
|
320
|
+
while a run animates. Static content (a list of cards, say) can be
|
|
321
|
+
decorated with a host at no per-frame cost, and hand off to an animated
|
|
322
|
+
host through the same path builder and shadow renderer.
|
|
323
|
+
- Content driven by Reanimated inside the host (an `Animated.View` with a
|
|
324
|
+
`useAnimatedStyle`) can land a frame after the clip on iOS: Reanimated
|
|
325
|
+
applies mapper props in its own display-link pass, separate from the
|
|
326
|
+
worklet frame that computed them, while a native run is sampled by Core
|
|
327
|
+
Animation for the same vsync. A main-thread overrun widens that to a frame
|
|
328
|
+
either way. Anything that must stay locked to the aperture belongs on the
|
|
329
|
+
run itself: put it in a `SmoothClipBackdropView` and drive it through the
|
|
330
|
+
presentation's `backdrop` channel.
|
|
284
331
|
- The shadow-disabled rendering path keeps no shadow drawing resources.
|
|
285
332
|
- The fixed host is the maximum rendering viewport; consumers should size it to
|
|
286
333
|
the region in which content and shadow may appear.
|
|
@@ -26,6 +26,28 @@ struct JSmoothClipView : facebook::jni::JavaClass<JSmoothClipView> {
|
|
|
26
26
|
void setAutonomousMotion(bool active) const;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// fbjni wrapper around com.smoothclipview.SmoothClipBackdropView: a view the
|
|
30
|
+
// driver's backdrop channel translates on every delivery, in the same
|
|
31
|
+
// Choreographer pass as the clip.
|
|
32
|
+
struct JSmoothClipBackdropView
|
|
33
|
+
: facebook::jni::JavaClass<JSmoothClipBackdropView> {
|
|
34
|
+
static constexpr auto kJavaDescriptor =
|
|
35
|
+
"Lcom/smoothclipview/SmoothClipBackdropView;";
|
|
36
|
+
|
|
37
|
+
void applyBackdropPx(double translateXPx, double translateYPx) const;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Binds a backdrop view to a driver on the UI thread. A view already bound
|
|
41
|
+
// only updates its density. The view is delivered the driver's current
|
|
42
|
+
// backdrop translation at once when the driver has a presentation.
|
|
43
|
+
void registerBackdropViewAndroid(
|
|
44
|
+
uint64_t driverId,
|
|
45
|
+
facebook::jni::alias_ref<JSmoothClipBackdropView> view,
|
|
46
|
+
double density);
|
|
47
|
+
void unregisterBackdropViewAndroid(
|
|
48
|
+
uint64_t driverId,
|
|
49
|
+
facebook::jni::alias_ref<JSmoothClipBackdropView> view);
|
|
50
|
+
|
|
29
51
|
// Called from Kotlin (SmoothClipViewManager) on the UI thread when a Fabric
|
|
30
52
|
// view mounts/unmounts. The registry retains a global ref for the view.
|
|
31
53
|
void registerViewAndroid(
|
|
@@ -27,7 +27,7 @@ namespace smoothclip {
|
|
|
27
27
|
namespace {
|
|
28
28
|
|
|
29
29
|
constexpr double kMaxSafeJavaScriptInteger = 9007199254740991.0;
|
|
30
|
-
constexpr size_t kPresentationStride =
|
|
30
|
+
constexpr size_t kPresentationStride = 25;
|
|
31
31
|
constexpr size_t kSnapshotStride = kPresentationStride + 1;
|
|
32
32
|
constexpr size_t kMotionEntryStride = kPresentationStride * 2 + 2;
|
|
33
33
|
|
|
@@ -155,7 +155,8 @@ bool presentationAt(
|
|
|
155
155
|
Shadow{
|
|
156
156
|
packet[12] == 1.0,
|
|
157
157
|
packet[13], packet[14], packet[15], packet[16],
|
|
158
|
-
packet[17], packet[18], packet[19], packet[20]}, packet[21], packet[22]
|
|
158
|
+
packet[17], packet[18], packet[19], packet[20]}, packet[21], packet[22],
|
|
159
|
+
packet[23], packet[24]};
|
|
159
160
|
return finitePresentation(result);
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -193,6 +194,8 @@ void appendPresentation(
|
|
|
193
194
|
result.setValueAtIndex(runtime, offset++, presentation.shadow.spreadDistance);
|
|
194
195
|
result.setValueAtIndex(runtime, offset++, presentation.rotation);
|
|
195
196
|
result.setValueAtIndex(runtime, offset++, presentation.opacity);
|
|
197
|
+
result.setValueAtIndex(runtime, offset++, presentation.backdropTranslateX);
|
|
198
|
+
result.setValueAtIndex(runtime, offset++, presentation.backdropTranslateY);
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
Array presentationArray(Runtime &runtime, const Presentation &presentation) {
|
|
@@ -835,11 +838,15 @@ void nativeRegisterView(
|
|
|
835
838
|
jdouble shadowSpreadDistance,
|
|
836
839
|
jdouble rotation,
|
|
837
840
|
jdouble opacity,
|
|
841
|
+
jdouble backdropTranslateX,
|
|
842
|
+
jdouble backdropTranslateY,
|
|
838
843
|
jdouble density,
|
|
839
844
|
jdouble widthPx,
|
|
840
845
|
jdouble heightPx,
|
|
841
846
|
jboolean lifecycleVisible) {
|
|
842
847
|
if (driverId <= 0 || !std::isfinite(driverId) || !std::isfinite(x) ||
|
|
848
|
+
!std::isfinite(backdropTranslateX) ||
|
|
849
|
+
!std::isfinite(backdropTranslateY) ||
|
|
843
850
|
!std::isfinite(y) || !std::isfinite(width) || !std::isfinite(height) ||
|
|
844
851
|
!std::isfinite(topLeftRadius) || !std::isfinite(topRightRadius) ||
|
|
845
852
|
!std::isfinite(bottomRightRadius) || !std::isfinite(bottomLeftRadius) ||
|
|
@@ -882,13 +889,36 @@ void nativeRegisterView(
|
|
|
882
889
|
static_cast<uint64_t>(driverId),
|
|
883
890
|
view,
|
|
884
891
|
smoothclip::Presentation{
|
|
885
|
-
geometry, contentTranslateX, contentTranslateY, contentScale, shadow,
|
|
892
|
+
geometry, contentTranslateX, contentTranslateY, contentScale, shadow,
|
|
893
|
+
rotation, opacity, backdropTranslateX, backdropTranslateY},
|
|
886
894
|
density,
|
|
887
895
|
widthPx,
|
|
888
896
|
heightPx,
|
|
889
897
|
lifecycleVisible != 0);
|
|
890
898
|
}
|
|
891
899
|
|
|
900
|
+
void nativeRegisterBackdropView(
|
|
901
|
+
jni::alias_ref<jni::JObject>,
|
|
902
|
+
jdouble driverId,
|
|
903
|
+
jni::alias_ref<smoothclip::JSmoothClipBackdropView> view,
|
|
904
|
+
jdouble density) {
|
|
905
|
+
if (driverId <= 0 || !std::isfinite(driverId) || !std::isfinite(density) ||
|
|
906
|
+
density <= 0) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
smoothclip::registerBackdropViewAndroid(
|
|
910
|
+
static_cast<uint64_t>(driverId), view, density);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
void nativeUnregisterBackdropView(
|
|
914
|
+
jni::alias_ref<jni::JObject>,
|
|
915
|
+
jdouble driverId,
|
|
916
|
+
jni::alias_ref<smoothclip::JSmoothClipBackdropView> view) {
|
|
917
|
+
if (driverId <= 0 || !std::isfinite(driverId)) return;
|
|
918
|
+
smoothclip::unregisterBackdropViewAndroid(
|
|
919
|
+
static_cast<uint64_t>(driverId), view);
|
|
920
|
+
}
|
|
921
|
+
|
|
892
922
|
void nativeSetViewHostGeometry(
|
|
893
923
|
jni::alias_ref<jni::JObject>,
|
|
894
924
|
jdouble driverId,
|
|
@@ -952,6 +982,10 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
|
952
982
|
makeNativeMethod(
|
|
953
983
|
"nativeSetViewHostGeometry", nativeSetViewHostGeometry),
|
|
954
984
|
makeNativeMethod("nativeUnregisterView", nativeUnregisterView),
|
|
985
|
+
makeNativeMethod(
|
|
986
|
+
"nativeRegisterBackdropView", nativeRegisterBackdropView),
|
|
987
|
+
makeNativeMethod(
|
|
988
|
+
"nativeUnregisterBackdropView", nativeUnregisterBackdropView),
|
|
955
989
|
makeNativeMethod("nativeDestroyDriver", nativeDestroyDriver),
|
|
956
990
|
makeNativeMethod(
|
|
957
991
|
"nativeSetViewLifecycleVisibility",
|
|
@@ -90,11 +90,20 @@ struct ViewEntry {
|
|
|
90
90
|
ViewParticipation participation = ViewParticipation::Deferred;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
// A backdrop view bound to the driver. It is translated by the presentation's
|
|
94
|
+
// backdrop channel on every delivery and has no displayability of its own: the
|
|
95
|
+
// clip host decides when a run may start.
|
|
96
|
+
struct BackdropEntry {
|
|
97
|
+
global_ref<JSmoothClipBackdropView> view;
|
|
98
|
+
double density = 1;
|
|
99
|
+
};
|
|
100
|
+
|
|
93
101
|
struct DriverState {
|
|
94
102
|
Presentation latest{{0, 0, 0, 0, 0}, 0, 0};
|
|
95
103
|
bool hasLatest = false;
|
|
96
104
|
Ownership ownership = Ownership::Interactive;
|
|
97
105
|
std::vector<ViewEntry> views;
|
|
106
|
+
std::vector<BackdropEntry> backdrops;
|
|
98
107
|
std::optional<ActiveAnimation> animation;
|
|
99
108
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
100
109
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
@@ -461,10 +470,28 @@ void deliverToView(const ViewEntry &entry, const Presentation &presentation) {
|
|
|
461
470
|
shadowPx, presentation.rotation, presentation.opacity);
|
|
462
471
|
}
|
|
463
472
|
|
|
473
|
+
void deliverToBackdrop(
|
|
474
|
+
const BackdropEntry &entry,
|
|
475
|
+
const Presentation &presentation) {
|
|
476
|
+
const double x = presentation.backdropTranslateX * entry.density;
|
|
477
|
+
const double y = presentation.backdropTranslateY * entry.density;
|
|
478
|
+
if (!std::isfinite(x) || !std::isfinite(y)) return;
|
|
479
|
+
entry.view->applyBackdropPx(x, y);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
void deliverToBackdrops(
|
|
483
|
+
const DriverState &state,
|
|
484
|
+
const Presentation &presentation) {
|
|
485
|
+
for (const BackdropEntry &entry : state.backdrops) {
|
|
486
|
+
deliverToBackdrop(entry, presentation);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
464
490
|
void applyToViews(DriverState &state, const Presentation &presentation) {
|
|
465
491
|
for (const auto &entry : state.views) {
|
|
466
492
|
deliverToView(entry, presentation);
|
|
467
493
|
}
|
|
494
|
+
deliverToBackdrops(state, presentation);
|
|
468
495
|
}
|
|
469
496
|
|
|
470
497
|
void setAutonomousMotion(DriverState &state, bool active) {
|
|
@@ -481,6 +508,7 @@ void applyAnimationFrameToViews(
|
|
|
481
508
|
deliverToView(entry, presentation);
|
|
482
509
|
}
|
|
483
510
|
}
|
|
511
|
+
deliverToBackdrops(state, presentation);
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
void applyPresentation(DriverState &state, const Presentation &presentation) {
|
|
@@ -1667,7 +1695,7 @@ void destroyDriver(uint64_t driverId) {
|
|
|
1667
1695
|
// refresh the staleness clock with motion no finger produced. (iOS clears
|
|
1668
1696
|
// its per-view histories at the same point.)
|
|
1669
1697
|
state.samples = VelocitySampleHistory{};
|
|
1670
|
-
if (state.views.empty()) {
|
|
1698
|
+
if (state.views.empty() && state.backdrops.empty()) {
|
|
1671
1699
|
registry().erase(iterator);
|
|
1672
1700
|
} else {
|
|
1673
1701
|
// Views can outlive the hook briefly (StrictMode effect replay, hosts in
|
|
@@ -1795,6 +1823,61 @@ void JSmoothClipView::setAutonomousMotion(bool active) const {
|
|
|
1795
1823
|
method(self(), active);
|
|
1796
1824
|
}
|
|
1797
1825
|
|
|
1826
|
+
void JSmoothClipBackdropView::applyBackdropPx(
|
|
1827
|
+
double translateXPx,
|
|
1828
|
+
double translateYPx) const {
|
|
1829
|
+
static const auto method =
|
|
1830
|
+
javaClassStatic()->getMethod<void(jfloat, jfloat)>(
|
|
1831
|
+
"setBackdropTranslationPx");
|
|
1832
|
+
method(
|
|
1833
|
+
self(),
|
|
1834
|
+
static_cast<jfloat>(translateXPx),
|
|
1835
|
+
static_cast<jfloat>(translateYPx));
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
void registerBackdropViewAndroid(
|
|
1839
|
+
uint64_t driverId,
|
|
1840
|
+
alias_ref<JSmoothClipBackdropView> view,
|
|
1841
|
+
double density) {
|
|
1842
|
+
if (!isOnMainThread()) return;
|
|
1843
|
+
auto &state = registry()[driverId];
|
|
1844
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
1845
|
+
for (auto &entry : state.backdrops) {
|
|
1846
|
+
if (!env->IsSameObject(entry.view.get(), view.get())) continue;
|
|
1847
|
+
entry.density = density;
|
|
1848
|
+
if (state.hasLatest) {
|
|
1849
|
+
deliverToBackdrop(entry, visiblePresentation(driverId, state));
|
|
1850
|
+
}
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
BackdropEntry entry{facebook::jni::make_global(view), density};
|
|
1854
|
+
// A backdrop that binds mid-run adopts the driver's current value and
|
|
1855
|
+
// follows from the next delivery; a pending run's start is what shows.
|
|
1856
|
+
if (state.hasLatest) {
|
|
1857
|
+
deliverToBackdrop(entry, visiblePresentation(driverId, state));
|
|
1858
|
+
}
|
|
1859
|
+
state.backdrops.push_back(std::move(entry));
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
void unregisterBackdropViewAndroid(
|
|
1863
|
+
uint64_t driverId,
|
|
1864
|
+
alias_ref<JSmoothClipBackdropView> view) {
|
|
1865
|
+
auto iterator = registry().find(driverId);
|
|
1866
|
+
if (iterator == registry().end()) return;
|
|
1867
|
+
auto &state = iterator->second;
|
|
1868
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
1869
|
+
for (auto entry = state.backdrops.begin(); entry != state.backdrops.end();) {
|
|
1870
|
+
if (env->IsSameObject(entry->view.get(), view.get())) {
|
|
1871
|
+
entry = state.backdrops.erase(entry);
|
|
1872
|
+
} else {
|
|
1873
|
+
++entry;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
|
|
1877
|
+
registry().erase(iterator);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1798
1881
|
void registerViewAndroid(
|
|
1799
1882
|
uint64_t driverId,
|
|
1800
1883
|
alias_ref<JSmoothClipView> view,
|
|
@@ -1921,7 +2004,7 @@ void unregisterViewAndroid(
|
|
|
1921
2004
|
if (removed && state.groupId != 0 && isOnMainThread()) {
|
|
1922
2005
|
reconcileGroupReadiness(driverId);
|
|
1923
2006
|
}
|
|
1924
|
-
if (state.destroyed && state.views.empty()) {
|
|
2007
|
+
if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
|
|
1925
2008
|
registry().erase(iterator);
|
|
1926
2009
|
}
|
|
1927
2010
|
}
|
|
@@ -200,6 +200,24 @@ internal fun appendRoundedRectPath(
|
|
|
200
200
|
return
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
if (topLeftRadius == topRightRadius && topLeftRadius == bottomRightRadius &&
|
|
204
|
+
topLeftRadius == bottomLeftRadius
|
|
205
|
+
) {
|
|
206
|
+
// A uniform circular corner stays an rrect-tagged path: the renderer
|
|
207
|
+
// keeps its analytic clip and blur fast paths for it, which the cubic
|
|
208
|
+
// outline below, though the same shape, would lose.
|
|
209
|
+
path.addRoundRect(
|
|
210
|
+
left,
|
|
211
|
+
top,
|
|
212
|
+
right,
|
|
213
|
+
bottom,
|
|
214
|
+
topLeftRadius,
|
|
215
|
+
topLeftRadius,
|
|
216
|
+
Path.Direction.CW,
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
203
221
|
val coefficient = 0.5522848f
|
|
204
222
|
|
|
205
223
|
path.moveTo(left + topLeftRadius, top)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.smoothclipview
|
|
2
|
+
|
|
3
|
+
import android.content.res.Configuration
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A view translated by its controller's backdrop channel. The registry
|
|
11
|
+
* delivers the channel in pixels on every clip delivery — a `setFrame`, a
|
|
12
|
+
* run's frame, a run's end — inside the same Choreographer pass as the clip,
|
|
13
|
+
* so the content never lands a frame after the aperture. The translation goes
|
|
14
|
+
* on an inner container, leaving this view's own transform to React Native.
|
|
15
|
+
*/
|
|
16
|
+
class SmoothClipBackdropView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
17
|
+
internal val contentContainer = ReactViewGroup(context)
|
|
18
|
+
|
|
19
|
+
/** Driver this view is bound to in the native registry (0 = none). */
|
|
20
|
+
internal var boundDriverId: Double = 0.0
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
clipChildren = false
|
|
24
|
+
super.addView(contentContainer)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
28
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
29
|
+
contentContainer.layout(0, 0, w, h)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Registry hot path: the backdrop translation, in pixels. */
|
|
33
|
+
@DoNotStrip
|
|
34
|
+
fun setBackdropTranslationPx(translateX: Float, translateY: Float) {
|
|
35
|
+
contentContainer.translationX = translateX
|
|
36
|
+
contentContainer.translationY = translateY
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal fun bindDriver(driverId: Double) {
|
|
40
|
+
if (boundDriverId != 0.0 && boundDriverId != driverId) {
|
|
41
|
+
SmoothClipBindings.nativeUnregisterBackdropView(boundDriverId, this)
|
|
42
|
+
}
|
|
43
|
+
boundDriverId = driverId
|
|
44
|
+
if (driverId != 0.0) {
|
|
45
|
+
SmoothClipBindings.nativeRegisterBackdropView(driverId, this, densityScale())
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
50
|
+
super.onConfigurationChanged(newConfig)
|
|
51
|
+
// Density can change without a remount (display switch); the registry
|
|
52
|
+
// scales the channel by the pushed density.
|
|
53
|
+
if (boundDriverId != 0.0) {
|
|
54
|
+
SmoothClipBindings.nativeRegisterBackdropView(boundDriverId, this, densityScale())
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun densityScale(): Double = PixelUtil.toPixelFromDIP(1f).toDouble()
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.smoothclipview
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.SmoothClipBackdropViewManagerDelegate
|
|
10
|
+
import com.facebook.react.viewmanagers.SmoothClipBackdropViewManagerInterface
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = SmoothClipBackdropViewManager.NAME)
|
|
13
|
+
class SmoothClipBackdropViewManager : ViewGroupManager<SmoothClipBackdropView>(),
|
|
14
|
+
SmoothClipBackdropViewManagerInterface<SmoothClipBackdropView> {
|
|
15
|
+
private val delegate =
|
|
16
|
+
SmoothClipBackdropViewManagerDelegate<SmoothClipBackdropView, SmoothClipBackdropViewManager>(this)
|
|
17
|
+
|
|
18
|
+
// Fabric delivers the prop before the transaction ends; bind once per
|
|
19
|
+
// transaction, after every prop of it has landed.
|
|
20
|
+
private val pendingDriverIds = java.util.WeakHashMap<SmoothClipBackdropView, Double>()
|
|
21
|
+
|
|
22
|
+
override fun getName(): String = NAME
|
|
23
|
+
|
|
24
|
+
override fun getDelegate(): ViewManagerDelegate<SmoothClipBackdropView> = delegate
|
|
25
|
+
|
|
26
|
+
override fun createViewInstance(context: ThemedReactContext): SmoothClipBackdropView =
|
|
27
|
+
SmoothClipBackdropView(context)
|
|
28
|
+
|
|
29
|
+
@ReactProp(name = "driverId")
|
|
30
|
+
override fun setDriverId(view: SmoothClipBackdropView, value: Double) {
|
|
31
|
+
pendingDriverIds[view] = value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun addView(parent: SmoothClipBackdropView, child: View, index: Int) {
|
|
35
|
+
parent.contentContainer.addView(child, index)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun getChildCount(parent: SmoothClipBackdropView): Int =
|
|
39
|
+
parent.contentContainer.childCount
|
|
40
|
+
|
|
41
|
+
override fun getChildAt(parent: SmoothClipBackdropView, index: Int): View =
|
|
42
|
+
parent.contentContainer.getChildAt(index)
|
|
43
|
+
|
|
44
|
+
override fun removeViewAt(parent: SmoothClipBackdropView, index: Int) {
|
|
45
|
+
parent.contentContainer.removeViewAt(index)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun onAfterUpdateTransaction(view: SmoothClipBackdropView) {
|
|
49
|
+
super.onAfterUpdateTransaction(view)
|
|
50
|
+
val driverId = pendingDriverIds.remove(view) ?: return
|
|
51
|
+
view.bindDriver(driverId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onDropViewInstance(view: SmoothClipBackdropView) {
|
|
55
|
+
pendingDriverIds.remove(view)
|
|
56
|
+
view.bindDriver(0.0)
|
|
57
|
+
super.onDropViewInstance(view)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
companion object {
|
|
61
|
+
const val NAME = "SmoothClipBackdropView"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -45,12 +45,27 @@ internal object SmoothClipBindings {
|
|
|
45
45
|
shadowSpreadDistance: Double,
|
|
46
46
|
rotation: Double,
|
|
47
47
|
opacity: Double,
|
|
48
|
+
backdropTranslateX: Double,
|
|
49
|
+
backdropTranslateY: Double,
|
|
48
50
|
density: Double,
|
|
49
51
|
widthPx: Double,
|
|
50
52
|
heightPx: Double,
|
|
51
53
|
lifecycleVisible: Boolean,
|
|
52
54
|
)
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Binds a backdrop view to a driver: the registry translates it by the
|
|
58
|
+
* presentation's backdrop channel on every delivery. Re-registering an
|
|
59
|
+
* already bound view only updates its density.
|
|
60
|
+
*/
|
|
61
|
+
external fun nativeRegisterBackdropView(
|
|
62
|
+
driverId: Double,
|
|
63
|
+
view: SmoothClipBackdropView,
|
|
64
|
+
density: Double,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
external fun nativeUnregisterBackdropView(driverId: Double, view: SmoothClipBackdropView)
|
|
68
|
+
|
|
54
69
|
/**
|
|
55
70
|
* Pushes a registered view's density and pixel host size. Density converts
|
|
56
71
|
* raw DIP presentations to pixels; host size controls readiness and the
|