react-native-smooth-clip-view 0.2.5 → 0.2.6
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/README.md +49 -21
- package/android/CMakeLists.txt +0 -1
- package/android/src/main/cpp/SmoothClipAndroid.h +5 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +28 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +192 -191
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +20 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +105 -34
- package/cpp/SmoothClipAnimationCurve.h +468 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +27 -5
- package/ios/SmoothClipRegistry.mm +246 -97
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +73 -16
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/drivers.native.js +98 -29
- package/lib/module/drivers.native.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/driverTypes.d.ts +3 -2
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverTypes.ts +3 -2
- package/src/drivers.native.ts +132 -25
package/package.json
CHANGED
|
@@ -18,7 +18,8 @@ export interface Spec extends TurboModule {
|
|
|
18
18
|
radius: CodegenTypes.Double,
|
|
19
19
|
contentTranslateX: CodegenTypes.Double,
|
|
20
20
|
contentTranslateY: CodegenTypes.Double,
|
|
21
|
-
takeOwnership: boolean
|
|
21
|
+
takeOwnership: boolean,
|
|
22
|
+
overridePendingAnimation: boolean
|
|
22
23
|
): void;
|
|
23
24
|
beginInteraction(
|
|
24
25
|
driverId: CodegenTypes.Double
|
package/src/driverTypes.ts
CHANGED
|
@@ -16,8 +16,9 @@ type ClipAnimationBase = Readonly<{
|
|
|
16
16
|
* On the native drivers `driver.presentation.value` is not written from
|
|
17
17
|
* `from` (it stays stale until success sets it to the target); the web
|
|
18
18
|
* fallback does write it — its presentation SharedValue is the render
|
|
19
|
-
* channel itself. Against a held pending-animation latch
|
|
20
|
-
*
|
|
19
|
+
* channel itself. Against a held pending-animation latch, explicit `from`
|
|
20
|
+
* is newer intent: the fused write cancels that latch once unfinished,
|
|
21
|
+
* applies `from`, and starts the replacement from that native value.
|
|
21
22
|
*
|
|
22
23
|
* Keyframes interpolate absolutely, so pass `frames[0].presentation` —
|
|
23
24
|
* the seed renders it before the first animation frame, making the
|
package/src/drivers.native.ts
CHANGED
|
@@ -46,9 +46,25 @@ const NATIVE = 1;
|
|
|
46
46
|
|
|
47
47
|
const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
|
|
48
48
|
const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
|
|
50
|
+
// The animate* host functions take one argument beyond the TurboModule spec:
|
|
51
|
+
// the Reanimated-rule start stamp in milliseconds (see animateOnUI). Android's
|
|
52
|
+
// JSI bindings read it as an optional trailing argument; the iOS TurboModule
|
|
53
|
+
// reads its declared parameters positionally and ignores extras, so the
|
|
54
|
+
// codegen spec stays untouched and both platforms take the same call. A
|
|
55
|
+
// narrower function is assignable to the widened type, so no cast is needed.
|
|
56
|
+
type WithStartStamp<F> = F extends (...args: infer A) => infer R
|
|
57
|
+
? (...args: [...A, startedAtMs: number]) => R
|
|
58
|
+
: never;
|
|
59
|
+
const animateTimingHostFunction: WithStartStamp<
|
|
60
|
+
typeof NativeSmoothClipModule.animateTiming
|
|
61
|
+
> = NativeSmoothClipModule.animateTiming;
|
|
62
|
+
const animateSpringHostFunction: WithStartStamp<
|
|
63
|
+
typeof NativeSmoothClipModule.animateSpring
|
|
64
|
+
> = NativeSmoothClipModule.animateSpring;
|
|
65
|
+
const animateKeyframesHostFunction: WithStartStamp<
|
|
66
|
+
typeof NativeSmoothClipModule.animateKeyframes
|
|
67
|
+
> = NativeSmoothClipModule.animateKeyframes;
|
|
52
68
|
const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
|
|
53
69
|
const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
|
|
54
70
|
const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
|
|
@@ -195,6 +211,14 @@ export function useSmoothClipDriver(
|
|
|
195
211
|
// so animateTo must start from the native registry's latest value instead
|
|
196
212
|
// of snapping back to the stale SharedValue.
|
|
197
213
|
const scalarsStale = useSharedValue(0);
|
|
214
|
+
// Non-zero between the effect cleanup's native teardown and the next effect
|
|
215
|
+
// run. Native cannot tell "this driver was never seeded" from "this driver
|
|
216
|
+
// was destroyed and erased" — both are a missing registry entry — so the
|
|
217
|
+
// animation entry points would happily create a fresh state for a call that
|
|
218
|
+
// arrives after the hook is gone, leaving a latch that no view starts and no
|
|
219
|
+
// destroy cancels. Only the UI side knows the difference, so it is decided
|
|
220
|
+
// here.
|
|
221
|
+
const disposed = useSharedValue(0);
|
|
198
222
|
const callbackRef = useRef(options.onAnimationComplete);
|
|
199
223
|
const driverRef = useRef<SmoothClipDriver | null>(null);
|
|
200
224
|
callbackRef.current = options.onAnimationComplete;
|
|
@@ -205,14 +229,21 @@ export function useSmoothClipDriver(
|
|
|
205
229
|
|
|
206
230
|
const seedPresentation = (next: SmoothClipPresentation) => {
|
|
207
231
|
'worklet';
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
if (clipPresentationEquals(presentation.value, next)) return;
|
|
233
|
+
suppressDeliveries.value += 1;
|
|
234
|
+
presentation.value = next;
|
|
235
|
+
// The listener runs synchronously inside that assignment, so the credit
|
|
236
|
+
// is either spent by now or was never spendable. deliver() returns before
|
|
237
|
+
// the decrement whenever the value dedupes against `last` — which happens
|
|
238
|
+
// exactly when native already holds it, e.g. freezing a latch back to the
|
|
239
|
+
// value it started from. An unspent credit would then swallow the next
|
|
240
|
+
// real declarative write and leave native a geometry behind for good.
|
|
241
|
+
if (suppressDeliveries.value > 0) suppressDeliveries.value -= 1;
|
|
212
242
|
};
|
|
213
243
|
|
|
214
244
|
const beginOnUI = (): SmoothClipPresentation => {
|
|
215
245
|
'worklet';
|
|
246
|
+
if (disposed.value !== 0) return presentation.value;
|
|
216
247
|
const current = presentationFromNative(
|
|
217
248
|
beginInteractionHostFunction(driverId),
|
|
218
249
|
presentation.value
|
|
@@ -226,7 +257,7 @@ export function useSmoothClipDriver(
|
|
|
226
257
|
|
|
227
258
|
const setOnUI = (next: SmoothClipPresentation): void => {
|
|
228
259
|
'worklet';
|
|
229
|
-
if (!isFiniteClipPresentation(next)) return;
|
|
260
|
+
if (disposed.value !== 0 || !isFiniteClipPresentation(next)) return;
|
|
230
261
|
activeAnimationId.value = 0;
|
|
231
262
|
ownership.value = INTERACTIVE;
|
|
232
263
|
scalarsStale.value = 0;
|
|
@@ -240,7 +271,8 @@ export function useSmoothClipDriver(
|
|
|
240
271
|
clip.radius,
|
|
241
272
|
contentTranslateX,
|
|
242
273
|
contentTranslateY,
|
|
243
|
-
true
|
|
274
|
+
true,
|
|
275
|
+
false
|
|
244
276
|
);
|
|
245
277
|
seedPresentation(next);
|
|
246
278
|
};
|
|
@@ -252,10 +284,12 @@ export function useSmoothClipDriver(
|
|
|
252
284
|
height: number,
|
|
253
285
|
radius: number,
|
|
254
286
|
contentTranslateX: number,
|
|
255
|
-
contentTranslateY: number
|
|
287
|
+
contentTranslateY: number,
|
|
288
|
+
overridePendingAnimation = false
|
|
256
289
|
): void => {
|
|
257
290
|
'worklet';
|
|
258
291
|
if (
|
|
292
|
+
disposed.value !== 0 ||
|
|
259
293
|
!Number.isFinite(x) ||
|
|
260
294
|
!Number.isFinite(y) ||
|
|
261
295
|
!Number.isFinite(width) ||
|
|
@@ -278,7 +312,8 @@ export function useSmoothClipDriver(
|
|
|
278
312
|
radius,
|
|
279
313
|
contentTranslateX,
|
|
280
314
|
contentTranslateY,
|
|
281
|
-
true
|
|
315
|
+
true,
|
|
316
|
+
overridePendingAnimation
|
|
282
317
|
);
|
|
283
318
|
};
|
|
284
319
|
|
|
@@ -293,6 +328,13 @@ export function useSmoothClipDriver(
|
|
|
293
328
|
) {
|
|
294
329
|
return rejectAnimationHostFunction(driverId);
|
|
295
330
|
}
|
|
331
|
+
// Past the hook's cleanup the native entry is gone for good, and an
|
|
332
|
+
// interactive start here would recreate it as a latch nothing can ever
|
|
333
|
+
// start or cancel. Reject before any side effect — not via
|
|
334
|
+
// rejectAnimation, which would mint an id and a completion for a driver
|
|
335
|
+
// whose JS state is already detached. This is the "unsupported dispatch"
|
|
336
|
+
// arm of the documented 0 contract.
|
|
337
|
+
if (disposed.value !== 0) return 0;
|
|
296
338
|
|
|
297
339
|
const from = animation.from;
|
|
298
340
|
if (from !== undefined) {
|
|
@@ -308,7 +350,8 @@ export function useSmoothClipDriver(
|
|
|
308
350
|
from.clip.height,
|
|
309
351
|
from.clip.radius,
|
|
310
352
|
from.contentTranslateX,
|
|
311
|
-
from.contentTranslateY
|
|
353
|
+
from.contentTranslateY,
|
|
354
|
+
true
|
|
312
355
|
);
|
|
313
356
|
}
|
|
314
357
|
|
|
@@ -325,6 +368,26 @@ export function useSmoothClipDriver(
|
|
|
325
368
|
scalarsStale.value = 0;
|
|
326
369
|
const startClip = start.clip;
|
|
327
370
|
const targetClip = target.clip;
|
|
371
|
+
// Reanimated stamps a parallel animation's t0 on this same runtime as
|
|
372
|
+
// `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
|
|
373
|
+
// Capturing the identical value here and handing it to the integrator
|
|
374
|
+
// keeps clip and content phase-locked in every branch — including a
|
|
375
|
+
// start issued from CALLBACK_INPUT (batched gesture moves), where the
|
|
376
|
+
// dispatching frame's stamp is EARLIER than the call: the native min()
|
|
377
|
+
// anchor alone would adopt that frame stamp while Reanimated, outside
|
|
378
|
+
// its rAF flush where __frameTimestamp is cleared, keeps the wall
|
|
379
|
+
// clock — a lead lasting the whole animation. NaN when the globals are
|
|
380
|
+
// absent (tests, non-worklets runtimes): native then falls back to its
|
|
381
|
+
// own clock plus the min() anchor, the pre-stamp behavior.
|
|
382
|
+
const workletGlobal = globalThis as {
|
|
383
|
+
__frameTimestamp?: number;
|
|
384
|
+
_getAnimationTimestamp?: () => number;
|
|
385
|
+
};
|
|
386
|
+
const startedAtMs =
|
|
387
|
+
workletGlobal.__frameTimestamp ||
|
|
388
|
+
(typeof workletGlobal._getAnimationTimestamp === 'function'
|
|
389
|
+
? workletGlobal._getAnimationTimestamp()
|
|
390
|
+
: Number.NaN);
|
|
328
391
|
let animationId: number;
|
|
329
392
|
|
|
330
393
|
if (animation.type === 'timing') {
|
|
@@ -351,7 +414,8 @@ export function useSmoothClipDriver(
|
|
|
351
414
|
y1,
|
|
352
415
|
x2,
|
|
353
416
|
y2,
|
|
354
|
-
reduceMotion
|
|
417
|
+
reduceMotion,
|
|
418
|
+
startedAtMs
|
|
355
419
|
);
|
|
356
420
|
} else if (animation.type === 'spring') {
|
|
357
421
|
const inheritVelocity =
|
|
@@ -379,7 +443,8 @@ export function useSmoothClipDriver(
|
|
|
379
443
|
Math.max(0, animation.damping ?? 10),
|
|
380
444
|
inheritVelocity ? 0 : (animation.initialVelocity as number),
|
|
381
445
|
inheritVelocity,
|
|
382
|
-
reduceMotion
|
|
446
|
+
reduceMotion,
|
|
447
|
+
startedAtMs
|
|
383
448
|
);
|
|
384
449
|
} else {
|
|
385
450
|
animationId = animateKeyframesHostFunction(
|
|
@@ -401,16 +466,17 @@ export function useSmoothClipDriver(
|
|
|
401
466
|
target.contentTranslateY,
|
|
402
467
|
Math.max(0, animation.duration),
|
|
403
468
|
flattenedKeyframes(animation),
|
|
404
|
-
reduceMotion
|
|
469
|
+
reduceMotion,
|
|
470
|
+
startedAtMs
|
|
405
471
|
);
|
|
406
472
|
}
|
|
407
473
|
if (animationId === 0) {
|
|
408
|
-
// The 0 sentinel:
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
474
|
+
// The 0 sentinel: off-main, invalid-id, or otherwise unsupported
|
|
475
|
+
// dispatch. A missing driver is accepted when this call carries the
|
|
476
|
+
// authoritative interactive start above; a start-less missing-state
|
|
477
|
+
// request remains unsupported rather than inventing zero geometry.
|
|
478
|
+
// Validation failures never get here (they reject before any side
|
|
479
|
+
// effect with a fresh id and one finished:false completion).
|
|
414
480
|
ownership.value = INTERACTIVE;
|
|
415
481
|
presentation.value = start;
|
|
416
482
|
if (scalarsWereStale) scalarsStale.value = 1;
|
|
@@ -426,6 +492,10 @@ export function useSmoothClipDriver(
|
|
|
426
492
|
behavior: 'current' | 'target' = 'current'
|
|
427
493
|
): SmoothClipPresentation => {
|
|
428
494
|
'worklet';
|
|
495
|
+
// Past the hook's cleanup the native entry is gone; the call would fail
|
|
496
|
+
// defined (handled=false) but it is the last driver.ui path that still
|
|
497
|
+
// crosses into native after the tombstone. Guard it like the rest.
|
|
498
|
+
if (disposed.value !== 0) return presentation.value;
|
|
429
499
|
const values = cancelAnimationHostFunction(
|
|
430
500
|
driverId,
|
|
431
501
|
animationId,
|
|
@@ -591,9 +661,14 @@ export function useSmoothClipDriver(
|
|
|
591
661
|
suppressed: SharedValue<number>,
|
|
592
662
|
active: SharedValue<number>,
|
|
593
663
|
stale: SharedValue<number>,
|
|
664
|
+
gone: SharedValue<number>,
|
|
594
665
|
setter: typeof setPresentationHostFunction
|
|
595
666
|
) => {
|
|
596
667
|
'worklet';
|
|
668
|
+
// First, before the listener or the seed: this effect run owns the
|
|
669
|
+
// driver again, so a StrictMode/<Activity> replay must clear the
|
|
670
|
+
// previous cleanup's tombstone or every later write would be rejected.
|
|
671
|
+
gone.value = 0;
|
|
597
672
|
let last: SmoothClipPresentation | null = null;
|
|
598
673
|
const deliver = (next: SmoothClipPresentation) => {
|
|
599
674
|
if (
|
|
@@ -623,15 +698,19 @@ export function useSmoothClipDriver(
|
|
|
623
698
|
clip.radius,
|
|
624
699
|
contentTranslateX,
|
|
625
700
|
contentTranslateY,
|
|
701
|
+
false,
|
|
626
702
|
false
|
|
627
703
|
);
|
|
628
704
|
};
|
|
629
705
|
source.addListener(listenerId, deliver);
|
|
630
706
|
// Authoritative take-ownership seed. Creates the native entry before
|
|
631
|
-
// any view mounts on the
|
|
632
|
-
// an effect-replay cleanup (StrictMode/<Activity>) on later runs.
|
|
707
|
+
// any view mounts on the ordinary path, and revives a driver destroyed
|
|
708
|
+
// by an effect-replay cleanup (StrictMode/<Activity>) on later runs.
|
|
709
|
+
// An animation worklet can win the scheduling race and create a latch
|
|
710
|
+
// first; Native ownership then marks that intent as newer, so this
|
|
711
|
+
// passive seed must not clear its active id or replay the target value.
|
|
633
712
|
const current = source.value;
|
|
634
|
-
if (isFiniteClipPresentation(current)) {
|
|
713
|
+
if (owner.value === INTERACTIVE && isFiniteClipPresentation(current)) {
|
|
635
714
|
active.value = 0;
|
|
636
715
|
owner.value = INTERACTIVE;
|
|
637
716
|
stale.value = 0;
|
|
@@ -645,7 +724,8 @@ export function useSmoothClipDriver(
|
|
|
645
724
|
current.clip.radius,
|
|
646
725
|
current.contentTranslateX,
|
|
647
726
|
current.contentTranslateY,
|
|
648
|
-
true
|
|
727
|
+
true,
|
|
728
|
+
false
|
|
649
729
|
);
|
|
650
730
|
}
|
|
651
731
|
},
|
|
@@ -656,6 +736,7 @@ export function useSmoothClipDriver(
|
|
|
656
736
|
suppressDeliveries,
|
|
657
737
|
activeAnimationId,
|
|
658
738
|
scalarsStale,
|
|
739
|
+
disposed,
|
|
659
740
|
setPresentationHostFunction
|
|
660
741
|
);
|
|
661
742
|
|
|
@@ -668,20 +749,46 @@ export function useSmoothClipDriver(
|
|
|
668
749
|
source: SharedValue<SmoothClipPresentation>,
|
|
669
750
|
listenerId: number,
|
|
670
751
|
nativeDriverId: number,
|
|
752
|
+
owner: SharedValue<number>,
|
|
753
|
+
active: SharedValue<number>,
|
|
754
|
+
stale: SharedValue<number>,
|
|
755
|
+
suppressed: SharedValue<number>,
|
|
756
|
+
gone: SharedValue<number>,
|
|
671
757
|
destroy: typeof destroyDriverHostFunction
|
|
672
758
|
) => {
|
|
673
759
|
'worklet';
|
|
674
760
|
source.removeListener(listenerId);
|
|
675
761
|
destroy(nativeDriverId);
|
|
762
|
+
// StrictMode replays the effect with these same SharedValues. Reset
|
|
763
|
+
// the UI-side tombstone only after native teardown so the next setup
|
|
764
|
+
// can seed or animate without inheriting stale Native ownership.
|
|
765
|
+
active.value = 0;
|
|
766
|
+
owner.value = INTERACTIVE;
|
|
767
|
+
stale.value = 0;
|
|
768
|
+
// Defensive: seedPresentation now reclaims its own unspent credit at
|
|
769
|
+
// the point of issue, so this should already be 0. Kept because the
|
|
770
|
+
// failure mode — one declarative write silently dropped for the rest
|
|
771
|
+
// of the next hook's life — is invisible until someone reports a
|
|
772
|
+
// stale clip rect.
|
|
773
|
+
suppressed.value = 0;
|
|
774
|
+
// Last: the native entry is now erased, so until the next effect run
|
|
775
|
+
// clears this, any call that would recreate it is rejected instead.
|
|
776
|
+
gone.value = 1;
|
|
676
777
|
},
|
|
677
778
|
presentation,
|
|
678
779
|
driverId,
|
|
679
780
|
driverId,
|
|
781
|
+
ownership,
|
|
782
|
+
activeAnimationId,
|
|
783
|
+
scalarsStale,
|
|
784
|
+
suppressDeliveries,
|
|
785
|
+
disposed,
|
|
680
786
|
destroyDriverHostFunction
|
|
681
787
|
);
|
|
682
788
|
};
|
|
683
789
|
}, [
|
|
684
790
|
activeAnimationId,
|
|
791
|
+
disposed,
|
|
685
792
|
ownership,
|
|
686
793
|
presentation,
|
|
687
794
|
scalarsStale,
|