react-native-smooth-clip-view 0.4.1 → 0.4.3
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 +44 -0
- package/README.md +92 -5
- package/android/src/main/cpp/SmoothClipAndroid.h +2 -1
- package/android/src/main/cpp/SmoothClipBindings.cpp +22 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -10
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +23 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +8 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +39 -1
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +124 -32
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +16 -0
- package/cpp/SmoothClipAnimationCurve.h +21 -10
- package/cpp/SmoothClipRegistry.h +2 -0
- package/cpp/SmoothClipSharedGeometry.h +20 -0
- package/cpp/SmoothClipVelocityTracker.h +6 -0
- package/ios/SmoothClipRegistry.mm +173 -17
- package/ios/SmoothClipTurboModule.cpp +17 -6
- package/ios/SmoothClipTurboModule.h +3 -1
- package/ios/SmoothClipView.mm +408 -20
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +3 -0
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +5 -1
- package/lib/module/geometry.js +22 -2
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/presentationCodec.js +5 -3
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +3 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/controllerTypes.d.ts +5 -1
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +9 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- 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 +2 -1
- package/src/NativeSmoothClipModule.ts +3 -1
- package/src/SmoothClipView.ios.tsx +3 -0
- package/src/SmoothClipViewNativeComponent.ts +5 -1
- package/src/controllerTypes.ts +5 -1
- package/src/geometry.ts +35 -0
- package/src/index.ts +1 -0
- package/src/presentationCodec.ts +8 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.4.3](https://github.com/kbrattli/react-native-smooth-clip-view/releases/tag/v0.4.3) — 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Rotate and fade the whole clip
|
|
6
|
+
|
|
7
|
+
Animate rotation and opacity alongside clip geometry on iOS and Android. The
|
|
8
|
+
aperture, its content, and its shadow move and fade together inside the fixed
|
|
9
|
+
host, using the existing controller and atomic group APIs.
|
|
10
|
+
|
|
11
|
+
- **Rotation:** pass degree or radian strings such as `'12deg'` or `'0.2rad'`.
|
|
12
|
+
Rotation is clockwise around the current aperture center, after content
|
|
13
|
+
translation and scale. Angles interpolate numerically, so `'720deg'` animates
|
|
14
|
+
two full turns from zero. Interruption snapshots preserve those turns.
|
|
15
|
+
- **Group opacity:** set `opacity` to fade content and shadow together, including
|
|
16
|
+
overlapping children. Finite values clamp to `[0, 1]`; spring rendering clamps
|
|
17
|
+
opacity while preserving the spring's internal motion state.
|
|
18
|
+
- **Animation continuity:** rotation and opacity participate in timing and
|
|
19
|
+
spring animations, cancellation snapshots, and background spring pause/resume.
|
|
20
|
+
Explicit spring velocity applies to these channels too.
|
|
21
|
+
- **Touch and accessibility:** hit testing follows the rotated aperture, including
|
|
22
|
+
clips rotated back into the viewport. Fully transparent content accepts no new
|
|
23
|
+
touches and is hidden from accessibility.
|
|
24
|
+
- **Rendering:** appearance updates use native compositor properties without
|
|
25
|
+
Yoga layout work or rebuilding unchanged clipping paths. Translucent groups
|
|
26
|
+
may require offscreen compositing.
|
|
27
|
+
- **Types and validation:** export `ClipRotation`, include rotation and opacity in
|
|
28
|
+
canonical presentations, and reject malformed angles or nonfinite values
|
|
29
|
+
atomically. Add JavaScript and native regression coverage for these behaviors.
|
|
30
|
+
|
|
31
|
+
### Usage and upgrade notes
|
|
32
|
+
|
|
33
|
+
`rotation` defaults to `'0deg'` and `opacity` to `1`. Presentations are complete:
|
|
34
|
+
omitting either field resets it to its default. Snapshots return rotation in
|
|
35
|
+
radians. See [Rotate and fade a clip](./README.md#rotate-and-fade-a-clip) for a
|
|
36
|
+
controller example.
|
|
37
|
+
|
|
38
|
+
**Rebuild your native app after upgrading**, and run `pod install` on iOS. The
|
|
39
|
+
native presentation protocol has changed; JavaScript from 0.4.3 must run with the
|
|
40
|
+
matching native library, rather than an older binary updated only through OTA.
|
|
41
|
+
|
|
42
|
+
[All changes since 0.4.2](https://github.com/kbrattli/react-native-smooth-clip-view/compare/v0.4.2...v0.4.3)
|
|
43
|
+
|
|
44
|
+
Earlier release notes are available on [GitHub Releases](https://github.com/kbrattli/react-native-smooth-clip-view/releases).
|
package/README.md
CHANGED
|
@@ -107,9 +107,9 @@ UI-runtime runs without retaining a consumer worklet for the animation lifetime.
|
|
|
107
107
|
It does not update React state, animated props, Yoga, the ShadowTree, or a public
|
|
108
108
|
SharedValue.
|
|
109
109
|
|
|
110
|
-
`beginInteraction()` atomically interrupts native motion and returns
|
|
111
|
-
raw presentation. Apply the final gesture sample with `setFrame()`
|
|
112
|
-
it when the release must start from that exact frame.
|
|
110
|
+
`beginInteraction()` atomically interrupts native motion and returns
|
|
111
|
+
its visible raw presentation. Apply the final gesture sample with `setFrame()`
|
|
112
|
+
before calling it when the release must start from that exact frame.
|
|
113
113
|
|
|
114
114
|
## React API
|
|
115
115
|
|
|
@@ -134,9 +134,37 @@ defaults match Reanimated 4.5: mass `4`, stiffness `900`, damping `120`, and
|
|
|
134
134
|
relative energy threshold `6e-9`. A spring is rejected when its resolved native
|
|
135
135
|
trajectory could make `contentScale` nonpositive.
|
|
136
136
|
|
|
137
|
+
`velocity` is a shared initial progress velocity in inverse seconds, defaulting
|
|
138
|
+
to `0`. Each changing channel starts with velocity proportional to its remaining
|
|
139
|
+
distance: `velocity * (target - start)`. This includes rotation (in radians) and
|
|
140
|
+
opacity. Public controllers and groups do not automatically inherit gesture
|
|
141
|
+
velocity; starting a new spring after `beginInteraction()` still defaults to `0`
|
|
142
|
+
unless `velocity` is supplied. Pausing and resuming an existing native spring
|
|
143
|
+
preserves its internal motion state; public snapshots return clamped opacity.
|
|
144
|
+
|
|
145
|
+
The internal native standalone path also supports inherited velocity. Its
|
|
146
|
+
projection uses only clip position, size, corner radii, and content translation.
|
|
147
|
+
Rotation, opacity, content scale, and shadow channels do not contribute to that
|
|
148
|
+
projection. The resulting normalized velocity can drive all changing channels,
|
|
149
|
+
including rotation and opacity, without mixing their units into the projection.
|
|
150
|
+
Rotation/opacity-only motion therefore supplies no inherited velocity.
|
|
151
|
+
|
|
137
152
|
An animation requested before the host has a positive layout waits, then starts
|
|
138
|
-
with its full duration.
|
|
139
|
-
|
|
153
|
+
with its full duration. While the application is inactive, native animation
|
|
154
|
+
transactions stay pending and do not report cancellation. Timing animations
|
|
155
|
+
retain wall-clock progress and catch up after activation. Springs stay frozen;
|
|
156
|
+
their first foreground step consumes at most 64 ms, matching Reanimated, and
|
|
157
|
+
then they continue normally. If the platform discarded an underlying animation
|
|
158
|
+
object, the transaction settles successfully at its requested target.
|
|
159
|
+
|
|
160
|
+
Interactive `setFrame()` updates remain application-owned. The library does not
|
|
161
|
+
reset a drag or mutate related SharedValues or React state when lifecycle state
|
|
162
|
+
changes, so applications do not need lifecycle integration for the clip itself.
|
|
163
|
+
|
|
164
|
+
Host loss while the application is active keeps the existing semantics:
|
|
165
|
+
standalone animations apply their target and finish successfully, while groups
|
|
166
|
+
(including public controllers because they use one-member groups) freeze every
|
|
167
|
+
member and resolve `false`. Replacement, cancellation, destruction, and
|
|
140
168
|
rejection also settle once with `false`.
|
|
141
169
|
|
|
142
170
|
## Atomic groups
|
|
@@ -176,6 +204,15 @@ APIs share validation, run ownership, and completion behavior.
|
|
|
176
204
|
- Corner-overlap scaling follows CSS rules against the requested rectangle.
|
|
177
205
|
- `contentTranslateX`, `contentTranslateY`, and positive `contentScale` animate
|
|
178
206
|
with the aperture.
|
|
207
|
+
- `rotation` accepts degree or radian strings (for example, `'12deg'` or
|
|
208
|
+
`'0.2rad'`) and defaults to `'0deg'`. It rotates the aperture, content, and
|
|
209
|
+
shadow together, clockwise around the current aperture center. Existing
|
|
210
|
+
content translation and scale happen before this rotation. The host stays
|
|
211
|
+
fixed and crops the rotated result.
|
|
212
|
+
- `opacity` defaults to `1` and fades the aperture's content and shadow as one
|
|
213
|
+
group. Finite values clamp to `[0, 1]`; malformed angles and nonfinite values
|
|
214
|
+
reject the complete presentation. Spring opacity is clamped for rendering
|
|
215
|
+
without clamping its internal spring state.
|
|
179
216
|
- Circular and continuous curves and independent corner radii are supported.
|
|
180
217
|
- One outset `boxShadow` is supported. It escapes the aperture but not the host.
|
|
181
218
|
- A fully off-host aperture is not touchable, even when only its shadow overlaps.
|
|
@@ -185,6 +222,52 @@ APIs share validation, run ownership, and completion behavior.
|
|
|
185
222
|
Use `getSmoothClipCapabilities()` when a consumer needs to decide whether a
|
|
186
223
|
complex native path can be promoted on the current platform.
|
|
187
224
|
|
|
225
|
+
### Rotate and fade a clip
|
|
226
|
+
|
|
227
|
+
Rotation and opacity are presentation fields, so they use the same controller
|
|
228
|
+
and group methods as geometry. For example, start a tilted, translucent card
|
|
229
|
+
and animate it upright:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const clip = useSmoothClipController({
|
|
233
|
+
clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
|
|
234
|
+
contentTranslateX: 0,
|
|
235
|
+
contentTranslateY: 0,
|
|
236
|
+
rotation: '-12deg',
|
|
237
|
+
opacity: 0.4,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Attach this controller to <SmoothClipView controller={clip}> as above.
|
|
241
|
+
const reveal = () =>
|
|
242
|
+
clip.react.animateTo(
|
|
243
|
+
{
|
|
244
|
+
clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
|
|
245
|
+
contentTranslateX: 0,
|
|
246
|
+
contentTranslateY: 0,
|
|
247
|
+
rotation: '0deg',
|
|
248
|
+
opacity: 1,
|
|
249
|
+
},
|
|
250
|
+
{ type: 'spring', stiffness: 180, damping: 22 }
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For gesture-driven updates, supply the same fields from a UI-runtime worklet:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
clip.ui.setFrame({ ...frame, rotation: '12deg', opacity: 0.5 });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Angles interpolate numerically, so `'720deg'` requests two full turns from zero;
|
|
261
|
+
there is no automatic shortest-path wrapping. Interruption snapshots preserve
|
|
262
|
+
full turns and return rotation as a radian string. Each presentation is complete:
|
|
263
|
+
omitting rotation or opacity resets that field to its default. At zero opacity,
|
|
264
|
+
the content admits no new touches and is hidden from accessibility.
|
|
265
|
+
|
|
266
|
+
When upgrading to **0.4.3**, rebuild the native app (and run `pod install` on
|
|
267
|
+
iOS). The native presentation protocol includes new rotation and opacity
|
|
268
|
+
channels, so a JavaScript-only OTA update cannot upgrade an older native build.
|
|
269
|
+
See the [changelog](./CHANGELOG.md) for release details.
|
|
270
|
+
|
|
188
271
|
## Performance contract
|
|
189
272
|
|
|
190
273
|
- One worklet-to-native call per `ui.setFrame`, or per group batch.
|
|
@@ -192,6 +275,10 @@ complex native path can be promoted on the current platform.
|
|
|
192
275
|
- The shadow-disabled rendering path keeps no shadow drawing resources.
|
|
193
276
|
- The fixed host is the maximum rendering viewport; consumers should size it to
|
|
194
277
|
the region in which content and shadow may appear.
|
|
278
|
+
- Rotation and opacity update native compositor properties without Yoga work
|
|
279
|
+
or rebuilding unchanged clipping paths. Correctly fading overlapping children
|
|
280
|
+
may require offscreen compositing; large translucent groups can cost more GPU
|
|
281
|
+
time than opaque ones. No rasterization or software layer is forced.
|
|
195
282
|
|
|
196
283
|
## License
|
|
197
284
|
|
|
@@ -22,7 +22,7 @@ struct JSmoothClipView : facebook::jni::JavaClass<JSmoothClipView> {
|
|
|
22
22
|
double contentTranslateXPx,
|
|
23
23
|
double contentTranslateYPx,
|
|
24
24
|
double contentScale,
|
|
25
|
-
const Shadow &shadowPx) const;
|
|
25
|
+
const Shadow &shadowPx, double rotation, double opacity) const;
|
|
26
26
|
void setAutonomousMotion(bool active) const;
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -54,6 +54,7 @@ void setViewLifecycleVisibilityAndroid(
|
|
|
54
54
|
uint64_t driverId,
|
|
55
55
|
facebook::jni::alias_ref<JSmoothClipView> view,
|
|
56
56
|
bool lifecycleVisible);
|
|
57
|
+
void setApplicationActiveAndroid(bool active);
|
|
57
58
|
|
|
58
59
|
// Advances the registry frame loop. Called from Kotlin (SmoothClipBindings)
|
|
59
60
|
// inside Choreographer#doFrame with the frame's vsync timestamp converted to
|
|
@@ -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 = 23;
|
|
31
31
|
constexpr size_t kSnapshotStride = kPresentationStride + 1;
|
|
32
32
|
constexpr size_t kMotionEntryStride = kPresentationStride * 2 + 2;
|
|
33
33
|
|
|
@@ -61,7 +61,8 @@ bool finiteBasePresentation(const Presentation &presentation) {
|
|
|
61
61
|
std::isfinite(presentation.contentTranslateX) &&
|
|
62
62
|
std::isfinite(presentation.contentTranslateY) &&
|
|
63
63
|
std::isfinite(presentation.contentScale) &&
|
|
64
|
-
presentation.contentScale > 0
|
|
64
|
+
presentation.contentScale > 0 &&
|
|
65
|
+
std::isfinite(presentation.rotation) && std::isfinite(presentation.opacity);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
bool finitePresentation(const Presentation &presentation) {
|
|
@@ -154,7 +155,7 @@ bool presentationAt(
|
|
|
154
155
|
Shadow{
|
|
155
156
|
packet[12] == 1.0,
|
|
156
157
|
packet[13], packet[14], packet[15], packet[16],
|
|
157
|
-
packet[17], packet[18], packet[19], packet[20]}};
|
|
158
|
+
packet[17], packet[18], packet[19], packet[20]}, packet[21], packet[22]};
|
|
158
159
|
return finitePresentation(result);
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -190,6 +191,8 @@ void appendPresentation(
|
|
|
190
191
|
result.setValueAtIndex(runtime, offset++, presentation.shadow.offsetY);
|
|
191
192
|
result.setValueAtIndex(runtime, offset++, presentation.shadow.blurRadius);
|
|
192
193
|
result.setValueAtIndex(runtime, offset++, presentation.shadow.spreadDistance);
|
|
194
|
+
result.setValueAtIndex(runtime, offset++, presentation.rotation);
|
|
195
|
+
result.setValueAtIndex(runtime, offset++, presentation.opacity);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
Array presentationArray(Runtime &runtime, const Presentation &presentation) {
|
|
@@ -551,9 +554,9 @@ void installBindings(
|
|
|
551
554
|
});
|
|
552
555
|
|
|
553
556
|
setHostFunction(
|
|
554
|
-
runtime, bindings, "setClipFrameScalars",
|
|
557
|
+
runtime, bindings, "setClipFrameScalars", 24,
|
|
555
558
|
[](Runtime &, const Value &, const Value *args, size_t count) -> Value {
|
|
556
|
-
if (count <
|
|
559
|
+
if (count < 24) return Value::undefined();
|
|
557
560
|
const double driverId = args[0].asNumber();
|
|
558
561
|
Geometry geometry{
|
|
559
562
|
args[1].asNumber(), args[2].asNumber(), args[3].asNumber(),
|
|
@@ -584,7 +587,7 @@ void installBindings(
|
|
|
584
587
|
args[19].asNumber(),
|
|
585
588
|
args[20].asNumber(),
|
|
586
589
|
args[21].asNumber(),
|
|
587
|
-
}};
|
|
590
|
+
}, args[22].asNumber(), args[23].asNumber()};
|
|
588
591
|
if (validDriverId(driverId) && validCurve(args[9].asNumber()) &&
|
|
589
592
|
finitePresentation(presentation)) {
|
|
590
593
|
setPresentation(
|
|
@@ -828,6 +831,8 @@ void nativeRegisterView(
|
|
|
828
831
|
jdouble shadowOffsetY,
|
|
829
832
|
jdouble shadowBlurRadius,
|
|
830
833
|
jdouble shadowSpreadDistance,
|
|
834
|
+
jdouble rotation,
|
|
835
|
+
jdouble opacity,
|
|
831
836
|
jdouble density,
|
|
832
837
|
jdouble widthPx,
|
|
833
838
|
jdouble heightPx,
|
|
@@ -842,7 +847,8 @@ void nativeRegisterView(
|
|
|
842
847
|
!std::isfinite(shadowBlue) || !std::isfinite(shadowAlpha) ||
|
|
843
848
|
!std::isfinite(shadowOffsetX) ||
|
|
844
849
|
!std::isfinite(shadowOffsetY) || !std::isfinite(shadowBlurRadius) ||
|
|
845
|
-
!std::isfinite(shadowSpreadDistance) ||
|
|
850
|
+
!std::isfinite(shadowSpreadDistance) ||
|
|
851
|
+
!std::isfinite(rotation) || !std::isfinite(opacity) || contentScale <= 0 ||
|
|
846
852
|
shadowRed < 0 || shadowRed > 1 || shadowGreen < 0 || shadowGreen > 1 ||
|
|
847
853
|
shadowBlue < 0 || shadowBlue > 1 || shadowAlpha < 0 || shadowAlpha > 1 ||
|
|
848
854
|
shadowBlurRadius < 0 ||
|
|
@@ -874,7 +880,7 @@ void nativeRegisterView(
|
|
|
874
880
|
static_cast<uint64_t>(driverId),
|
|
875
881
|
view,
|
|
876
882
|
smoothclip::Presentation{
|
|
877
|
-
geometry, contentTranslateX, contentTranslateY, contentScale, shadow},
|
|
883
|
+
geometry, contentTranslateX, contentTranslateY, contentScale, shadow, rotation, opacity},
|
|
878
884
|
density,
|
|
879
885
|
widthPx,
|
|
880
886
|
heightPx,
|
|
@@ -918,6 +924,12 @@ void nativeSetViewLifecycleVisibility(
|
|
|
918
924
|
static_cast<uint64_t>(driverId), view, lifecycleVisible != 0);
|
|
919
925
|
}
|
|
920
926
|
|
|
927
|
+
void nativeSetApplicationActive(
|
|
928
|
+
jni::alias_ref<jni::JObject>,
|
|
929
|
+
jboolean active) {
|
|
930
|
+
smoothclip::setApplicationActiveAndroid(active != 0);
|
|
931
|
+
}
|
|
932
|
+
|
|
921
933
|
void nativeInvalidate(jni::alias_ref<jni::JObject>) {
|
|
922
934
|
smoothclip::invalidateBindings();
|
|
923
935
|
}
|
|
@@ -942,6 +954,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
|
942
954
|
makeNativeMethod(
|
|
943
955
|
"nativeSetViewLifecycleVisibility",
|
|
944
956
|
nativeSetViewLifecycleVisibility),
|
|
957
|
+
makeNativeMethod(
|
|
958
|
+
"nativeSetApplicationActive", nativeSetApplicationActive),
|
|
945
959
|
makeNativeMethod("nativeInvalidate", nativeInvalidate),
|
|
946
960
|
makeNativeMethod("nativeOnFrame", nativeOnFrame),
|
|
947
961
|
});
|
|
@@ -166,6 +166,16 @@ std::vector<int32_t> &animatingGroups() {
|
|
|
166
166
|
return value;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
bool &applicationActiveState() {
|
|
170
|
+
static bool value = true;
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
std::optional<double> &applicationInactiveSinceS() {
|
|
175
|
+
static std::optional<double> value;
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
// The sink is written from the JS thread (installBindings / invalidate) and
|
|
170
180
|
// invoked on main. Invoking under the lock also keeps the owning bindings
|
|
171
181
|
// state alive for the duration of the callback.
|
|
@@ -448,7 +458,7 @@ void deliverToView(const ViewEntry &entry, const Presentation &presentation) {
|
|
|
448
458
|
translateX,
|
|
449
459
|
translateY,
|
|
450
460
|
presentation.contentScale,
|
|
451
|
-
shadowPx);
|
|
461
|
+
shadowPx, presentation.rotation, presentation.opacity);
|
|
452
462
|
}
|
|
453
463
|
|
|
454
464
|
void applyToViews(DriverState &state, const Presentation &presentation) {
|
|
@@ -639,6 +649,7 @@ void startPendingGroup(ActiveGroup &group) {
|
|
|
639
649
|
}
|
|
640
650
|
|
|
641
651
|
void reconcileGroupReadiness(uint64_t driverId) {
|
|
652
|
+
if (!applicationActiveState()) return;
|
|
642
653
|
const auto stateIterator = registry().find(driverId);
|
|
643
654
|
if (stateIterator == registry().end() || stateIterator->second.groupId == 0) {
|
|
644
655
|
return;
|
|
@@ -704,7 +715,8 @@ int32_t startAnimation(
|
|
|
704
715
|
// visible all read animation->current, giving a never-rendered pending-run
|
|
705
716
|
// freeze-at-start / replace-from-start semantics with no extra branches.
|
|
706
717
|
animation.current = animation.start;
|
|
707
|
-
animation.started =
|
|
718
|
+
animation.started =
|
|
719
|
+
applicationActiveState() && anyDisplayableView(state);
|
|
708
720
|
state.animation = std::move(animation);
|
|
709
721
|
for (ViewEntry &entry : state.views) {
|
|
710
722
|
entry.participation = state.animation->started && entryDisplayable(entry)
|
|
@@ -730,7 +742,8 @@ int32_t startAnimation(
|
|
|
730
742
|
}
|
|
731
743
|
|
|
732
744
|
void finishIfHostUnavailable(uint64_t driverId, DriverState &state) {
|
|
733
|
-
if (!
|
|
745
|
+
if (!applicationActiveState() || !state.animation.has_value() ||
|
|
746
|
+
!state.animation->started ||
|
|
734
747
|
anyDisplayableView(state)) {
|
|
735
748
|
return;
|
|
736
749
|
}
|
|
@@ -746,7 +759,7 @@ void reconcileViewDisplayability(
|
|
|
746
759
|
uint64_t driverId,
|
|
747
760
|
DriverState &state,
|
|
748
761
|
ViewEntry &entry) {
|
|
749
|
-
if (!state.animation.has_value()) return;
|
|
762
|
+
if (!applicationActiveState() || !state.animation.has_value()) return;
|
|
750
763
|
if (!state.animation->started) {
|
|
751
764
|
if (entryDisplayable(entry)) startPendingAnimation(driverId, state);
|
|
752
765
|
return;
|
|
@@ -892,6 +905,7 @@ void advanceGroup(int32_t groupId, ActiveGroup &group, double now) {
|
|
|
892
905
|
void onFrameImpl(double now) {
|
|
893
906
|
SMOOTH_CLIP_TRACE("SmoothClip.frame");
|
|
894
907
|
gFrameScheduled = false;
|
|
908
|
+
if (!applicationActiveState()) return;
|
|
895
909
|
// Snapshot: advance() clears live-list entries on completion. advance()
|
|
896
910
|
// never erases registry entries and never starts animations (completion
|
|
897
911
|
// delivery hops through CallInvoker::invokeAsync), so the find() guard
|
|
@@ -939,6 +953,66 @@ void onFrameAndroid(double frameTimeS) {
|
|
|
939
953
|
}
|
|
940
954
|
}
|
|
941
955
|
|
|
956
|
+
void setApplicationActiveAndroid(bool active) {
|
|
957
|
+
if (!isOnMainThread() || applicationActiveState() == active) return;
|
|
958
|
+
if (!active) {
|
|
959
|
+
applicationActiveState() = false;
|
|
960
|
+
applicationInactiveSinceS() = nowSeconds();
|
|
961
|
+
gFrameScheduled = false;
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const double now = nowSeconds();
|
|
966
|
+
const double inactiveDurationS = applicationInactiveSinceS().has_value()
|
|
967
|
+
? std::max(0.0, now - *applicationInactiveSinceS())
|
|
968
|
+
: 0.0;
|
|
969
|
+
applicationInactiveSinceS().reset();
|
|
970
|
+
applicationActiveState() = true;
|
|
971
|
+
|
|
972
|
+
// Reanimated's spring frame delta includes the inactive interval and is
|
|
973
|
+
// clamped to 64 ms, but its termination is energy-based rather than a wall
|
|
974
|
+
// clock timeout. Keep lastFrameS unchanged so the first resumed step sees
|
|
975
|
+
// that clamped delta, while moving only the safety-cap origin forward so an
|
|
976
|
+
// ordinary background interval cannot terminate the spring by itself.
|
|
977
|
+
if (inactiveDurationS > 0) {
|
|
978
|
+
for (auto &[driverId, state] : registry()) {
|
|
979
|
+
(void)driverId;
|
|
980
|
+
if (state.groupId == 0 && state.animation.has_value() &&
|
|
981
|
+
state.animation->started &&
|
|
982
|
+
state.animation->kind == AnimationKind::Spring) {
|
|
983
|
+
state.animation->startedAtS = rebaseSpringStartAfterInactivity(
|
|
984
|
+
state.animation->startedAtS, inactiveDurationS);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
for (auto &[groupId, group] : groupRegistry()) {
|
|
988
|
+
(void)groupId;
|
|
989
|
+
if (!group.started || group.kind != AnimationKind::Spring) continue;
|
|
990
|
+
group.startedAtS = rebaseSpringStartAfterInactivity(
|
|
991
|
+
group.startedAtS, inactiveDurationS);
|
|
992
|
+
for (GroupMemberAnimation &member : group.members) {
|
|
993
|
+
member.animation.startedAtS = rebaseSpringStartAfterInactivity(
|
|
994
|
+
member.animation.startedAtS, inactiveDurationS);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
for (auto &[driverId, state] : registry()) {
|
|
1000
|
+
if (state.groupId == 0 && state.animation.has_value() &&
|
|
1001
|
+
!state.animation->started && anyDisplayableView(state)) {
|
|
1002
|
+
startPendingAnimation(driverId, state);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (auto &[groupId, group] : groupRegistry()) {
|
|
1006
|
+
(void)groupId;
|
|
1007
|
+
if (!group.started && allGroupMembersReady(group)) {
|
|
1008
|
+
startPendingGroup(group);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (!animatingDrivers().empty() || !animatingGroups().empty()) {
|
|
1012
|
+
scheduleFrame();
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
942
1016
|
// --- Public smoothclip:: interface (shared with iOS via SmoothClipRegistry.h)
|
|
943
1017
|
|
|
944
1018
|
void setCompletionCallback(const void *owner, CompletionCallback callback) {
|
|
@@ -1241,7 +1315,8 @@ int32_t startGroupCommon(
|
|
|
1241
1315
|
member.animation.lastFrameS = stamp.startedAtS;
|
|
1242
1316
|
member.animation.frameClockAnchored = stamp.frameClockAnchored;
|
|
1243
1317
|
}
|
|
1244
|
-
group.started =
|
|
1318
|
+
group.started =
|
|
1319
|
+
applicationActiveState() && allGroupMembersReady(group);
|
|
1245
1320
|
groupRegistry().emplace(groupId, std::move(group));
|
|
1246
1321
|
if (reduce) {
|
|
1247
1322
|
finishGroupImpl(groupId, true, true);
|
|
@@ -1630,7 +1705,7 @@ void JSmoothClipView::applyClip(const Presentation &presentation) const {
|
|
|
1630
1705
|
jdouble,
|
|
1631
1706
|
jdouble,
|
|
1632
1707
|
jdouble,
|
|
1633
|
-
jdouble)>("setClipPresentationDip");
|
|
1708
|
+
jdouble, jdouble, jdouble)>("setClipPresentationDip");
|
|
1634
1709
|
method(
|
|
1635
1710
|
self(),
|
|
1636
1711
|
presentation.clip.x,
|
|
@@ -1657,7 +1732,7 @@ void JSmoothClipView::applyClip(const Presentation &presentation) const {
|
|
|
1657
1732
|
presentation.shadow.offsetX,
|
|
1658
1733
|
presentation.shadow.offsetY,
|
|
1659
1734
|
presentation.shadow.blurRadius,
|
|
1660
|
-
presentation.shadow.spreadDistance);
|
|
1735
|
+
presentation.shadow.spreadDistance, presentation.rotation, presentation.opacity);
|
|
1661
1736
|
}
|
|
1662
1737
|
|
|
1663
1738
|
void JSmoothClipView::applyClipPx(
|
|
@@ -1665,7 +1740,7 @@ void JSmoothClipView::applyClipPx(
|
|
|
1665
1740
|
double contentTranslateXPx,
|
|
1666
1741
|
double contentTranslateYPx,
|
|
1667
1742
|
double contentScale,
|
|
1668
|
-
const Shadow &shadowPx) const {
|
|
1743
|
+
const Shadow &shadowPx, double rotation, double opacity) const {
|
|
1669
1744
|
static const auto method =
|
|
1670
1745
|
javaClassStatic()
|
|
1671
1746
|
->getMethod<void(
|
|
@@ -1689,7 +1764,7 @@ void JSmoothClipView::applyClipPx(
|
|
|
1689
1764
|
jfloat,
|
|
1690
1765
|
jfloat,
|
|
1691
1766
|
jfloat,
|
|
1692
|
-
jfloat)>("setClipPresentationPx");
|
|
1767
|
+
jfloat, jdouble, jfloat)>("setClipPresentationPx");
|
|
1693
1768
|
method(
|
|
1694
1769
|
self(),
|
|
1695
1770
|
static_cast<jfloat>(clip.left),
|
|
@@ -1712,7 +1787,7 @@ void JSmoothClipView::applyClipPx(
|
|
|
1712
1787
|
static_cast<jfloat>(shadowPx.offsetX),
|
|
1713
1788
|
static_cast<jfloat>(shadowPx.offsetY),
|
|
1714
1789
|
static_cast<jfloat>(shadowPx.blurRadius),
|
|
1715
|
-
static_cast<jfloat>(shadowPx.spreadDistance));
|
|
1790
|
+
static_cast<jfloat>(shadowPx.spreadDistance), rotation, static_cast<jfloat>(opacity));
|
|
1716
1791
|
}
|
|
1717
1792
|
|
|
1718
1793
|
void JSmoothClipView::setAutonomousMotion(bool active) const {
|
|
@@ -240,3 +240,26 @@ internal fun clipAccessibility(
|
|
|
240
240
|
} else {
|
|
241
241
|
requestedAccessibility
|
|
242
242
|
}
|
|
243
|
+
|
|
244
|
+
/** Conservative for rounded corners, exact for the rotated bounding rectangle. */
|
|
245
|
+
internal fun rotatedRectIntersectsHost(
|
|
246
|
+
left: Double, top: Double, right: Double, bottom: Double,
|
|
247
|
+
pivotX: Double, pivotY: Double, rotation: Double, hostWidth: Double, hostHeight: Double,
|
|
248
|
+
): Boolean {
|
|
249
|
+
if (right <= left || bottom <= top || hostWidth <= 0 || hostHeight <= 0) return false
|
|
250
|
+
if (rotation == 0.0) return right > 0 && bottom > 0 && left < hostWidth && top < hostHeight
|
|
251
|
+
val c = kotlin.math.cos(rotation)
|
|
252
|
+
val s = kotlin.math.sin(rotation)
|
|
253
|
+
val dx = (left + right) / 2 - pivotX
|
|
254
|
+
val dy = (top + bottom) / 2 - pivotY
|
|
255
|
+
val x = pivotX + c * dx - s * dy - hostWidth / 2
|
|
256
|
+
val y = pivotY + s * dx + c * dy - hostHeight / 2
|
|
257
|
+
val hw = (right - left) / 2
|
|
258
|
+
val hh = (bottom - top) / 2
|
|
259
|
+
val ac = kotlin.math.abs(c)
|
|
260
|
+
val ass = kotlin.math.abs(s)
|
|
261
|
+
return kotlin.math.abs(x) < hostWidth / 2 + ac * hw + ass * hh &&
|
|
262
|
+
kotlin.math.abs(y) < hostHeight / 2 + ass * hw + ac * hh &&
|
|
263
|
+
kotlin.math.abs(c * x + s * y) < hw + ac * hostWidth / 2 + ass * hostHeight / 2 &&
|
|
264
|
+
kotlin.math.abs(-s * x + c * y) < hh + ass * hostWidth / 2 + ac * hostHeight / 2
|
|
265
|
+
}
|
|
@@ -43,6 +43,8 @@ internal object SmoothClipBindings {
|
|
|
43
43
|
shadowOffsetY: Double,
|
|
44
44
|
shadowBlurRadius: Double,
|
|
45
45
|
shadowSpreadDistance: Double,
|
|
46
|
+
rotation: Double,
|
|
47
|
+
opacity: Double,
|
|
46
48
|
density: Double,
|
|
47
49
|
widthPx: Double,
|
|
48
50
|
heightPx: Double,
|
|
@@ -77,6 +79,8 @@ internal object SmoothClipBindings {
|
|
|
77
79
|
lifecycleVisible: Boolean,
|
|
78
80
|
)
|
|
79
81
|
|
|
82
|
+
external fun nativeSetApplicationActive(active: Boolean)
|
|
83
|
+
|
|
80
84
|
/**
|
|
81
85
|
* Releases runtime-bound listener state during host teardown; must run
|
|
82
86
|
* while the JS runtime is still alive (module invalidate).
|
|
@@ -99,6 +103,10 @@ internal object SmoothClipBindings {
|
|
|
99
103
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
fun pauseFrameLoop() {
|
|
107
|
+
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
/** Advances the frame loop with the doFrame vsync timestamp. */
|
|
103
111
|
external fun nativeOnFrame(frameTimeNanos: Long)
|
|
104
112
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
package com.smoothclipview
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
4
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
6
|
import com.facebook.react.bridge.ReadableArray
|
|
6
7
|
import com.facebook.react.bridge.UiThreadUtil
|
|
7
8
|
import com.facebook.react.bridge.WritableArray
|
|
9
|
+
import com.facebook.react.common.LifecycleState
|
|
8
10
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
11
|
import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
|
|
10
12
|
import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
|
|
@@ -17,12 +19,46 @@ import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
|
|
|
17
19
|
*/
|
|
18
20
|
@ReactModule(name = NativeSmoothClipModuleSpec.NAME)
|
|
19
21
|
class SmoothClipModule(context: ReactApplicationContext) :
|
|
20
|
-
NativeSmoothClipModuleSpec(context),
|
|
22
|
+
NativeSmoothClipModuleSpec(context),
|
|
23
|
+
TurboModuleWithJSIBindings,
|
|
24
|
+
LifecycleEventListener {
|
|
25
|
+
|
|
26
|
+
init {
|
|
27
|
+
context.addLifecycleEventListener(this)
|
|
28
|
+
setApplicationActive(context.lifecycleState == LifecycleState.RESUMED)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun setApplicationActive(active: Boolean) {
|
|
32
|
+
val updateState = {
|
|
33
|
+
if (!active) {
|
|
34
|
+
SmoothClipBindings.pauseFrameLoop()
|
|
35
|
+
}
|
|
36
|
+
SmoothClipBindings.nativeSetApplicationActive(active)
|
|
37
|
+
}
|
|
38
|
+
if (UiThreadUtil.isOnUiThread()) {
|
|
39
|
+
updateState()
|
|
40
|
+
} else {
|
|
41
|
+
UiThreadUtil.runOnUiThread(updateState)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onHostResume() {
|
|
46
|
+
setApplicationActive(true)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onHostPause() {
|
|
50
|
+
setApplicationActive(false)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onHostDestroy() {
|
|
54
|
+
setApplicationActive(false)
|
|
55
|
+
}
|
|
21
56
|
|
|
22
57
|
override fun getBindingsInstaller(): BindingsInstallerHolder =
|
|
23
58
|
SmoothClipBindings.getBindingsInstaller()
|
|
24
59
|
|
|
25
60
|
override fun invalidate() {
|
|
61
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
26
62
|
super.invalidate()
|
|
27
63
|
// Completions must stop and runtime-bound listener functions must be
|
|
28
64
|
// released before the JS runtime is torn down; otherwise a later host
|
|
@@ -104,6 +140,8 @@ class SmoothClipModule(context: ReactApplicationContext) :
|
|
|
104
140
|
shadowOffsetY: Double,
|
|
105
141
|
shadowBlurRadius: Double,
|
|
106
142
|
shadowSpreadDistance: Double,
|
|
143
|
+
rotation: Double,
|
|
144
|
+
opacity: Double,
|
|
107
145
|
) = Unit
|
|
108
146
|
|
|
109
147
|
override fun beginInteraction(driverId: Double): WritableArray =
|