react-native-smooth-clip-view 0.2.4 → 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 +62 -25
- 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 +198 -189
- 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 +32 -15
- 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.android.js +3 -1
- package/lib/module/drivers.android.js.map +1 -1
- package/lib/module/drivers.ios.js +3 -391
- package/lib/module/drivers.ios.js.map +1 -1
- package/lib/module/drivers.native.js +470 -0
- package/lib/module/drivers.native.js.map +1 -0
- 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 +6 -3
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.android.d.ts +2 -2
- package/lib/typescript/src/drivers.android.d.ts.map +1 -1
- package/lib/typescript/src/drivers.ios.d.ts +2 -4
- package/lib/typescript/src/drivers.ios.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts +5 -0
- package/lib/typescript/src/drivers.native.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverTypes.ts +6 -3
- package/src/drivers.android.ts +4 -2
- package/src/drivers.ios.ts +4 -687
- package/src/drivers.native.ts +805 -0
package/ios/SmoothClipView.mm
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import "SmoothClipGeometry.h"
|
|
4
4
|
#import "SmoothClipViewRegistry.h"
|
|
5
5
|
|
|
6
|
+
#include "SmoothClipAnimationCurve.h"
|
|
6
7
|
#include "SmoothClipVelocityTracker.h"
|
|
7
8
|
|
|
8
9
|
#import <QuartzCore/QuartzCore.h>
|
|
@@ -332,11 +333,18 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation {
|
|
336
|
+
[self smoothClipApplyPresentation:presentation recordVelocitySample:YES];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation
|
|
340
|
+
recordVelocitySample:(BOOL)recordVelocitySample {
|
|
335
341
|
_animationPaused = NO;
|
|
336
342
|
[self stopLayerAnimationWithoutCallback];
|
|
337
343
|
[self storeRequestedPresentation:presentation];
|
|
338
344
|
[self applyRequestedClip];
|
|
339
|
-
|
|
345
|
+
// Skipped for writes that are not interactive motion (a latched
|
|
346
|
+
// cancel-to-target): they must not enter the 'inherit' history.
|
|
347
|
+
if (recordVelocitySample && _hasLayout) {
|
|
340
348
|
[self recordInteractiveRect:_normalizedClip
|
|
341
349
|
radius:_normalizedRadius
|
|
342
350
|
contentTranslation:_normalizedContentTranslation];
|
|
@@ -380,6 +388,15 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
380
388
|
return _pendingAnimationInstall;
|
|
381
389
|
}
|
|
382
390
|
|
|
391
|
+
- (double)smoothClipSpringContinuationVelocity {
|
|
392
|
+
if (_activeAnimationId == 0 || _activeAnimationKind != 2 ||
|
|
393
|
+
_pendingAnimationInstall) {
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
const double elapsed = MAX(0, CACurrentMediaTime() - _animationStartedAt);
|
|
397
|
+
return smoothclip::springContinuationVelocity(_springAnimation, elapsed);
|
|
398
|
+
}
|
|
399
|
+
|
|
383
400
|
- (smoothclip::Presentation)smoothClipFreezePresentation {
|
|
384
401
|
const smoothclip::Presentation visible =
|
|
385
402
|
[self smoothClipCurrentPresentation];
|
|
@@ -623,13 +640,13 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
623
640
|
forKey:@"smoothClip.content"];
|
|
624
641
|
}
|
|
625
642
|
|
|
626
|
-
- (
|
|
643
|
+
- (BOOL)startAnimationToRequestedGeometryWithDuration:(CFTimeInterval)duration {
|
|
627
644
|
if (![self smoothClipCanDisplay]) {
|
|
628
645
|
_pendingAnimationInstall = YES;
|
|
629
|
-
return;
|
|
646
|
+
return NO;
|
|
630
647
|
}
|
|
631
648
|
SmoothNormalizedClipGeometry target;
|
|
632
|
-
if (![self normalizedRequestedGeometry:&target]) return;
|
|
649
|
+
if (![self normalizedRequestedGeometry:&target]) return NO;
|
|
633
650
|
const smoothclip::Presentation current = [self smoothClipCurrentPresentation];
|
|
634
651
|
[self installAnimationFromRect:CGRectMake(
|
|
635
652
|
current.clip.x,
|
|
@@ -644,20 +661,21 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
644
661
|
toRadius:target.radius
|
|
645
662
|
toContentTranslation:_requestedContentTranslation
|
|
646
663
|
duration:duration];
|
|
664
|
+
return YES;
|
|
647
665
|
}
|
|
648
666
|
|
|
649
|
-
- (
|
|
667
|
+
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
650
668
|
animation:(smoothclip::TimingAnimation)animation
|
|
651
669
|
animationId:(int32_t)animationId {
|
|
652
670
|
[self storeRequestedPresentation:presentation];
|
|
653
671
|
_timingAnimation = animation;
|
|
654
672
|
_activeAnimationKind = 1;
|
|
655
673
|
_activeAnimationId = animationId;
|
|
656
|
-
[self startAnimationToRequestedGeometryWithDuration:
|
|
657
|
-
|
|
674
|
+
return [self startAnimationToRequestedGeometryWithDuration:
|
|
675
|
+
MAX(0, animation.durationMs) / 1000.0];
|
|
658
676
|
}
|
|
659
677
|
|
|
660
|
-
- (
|
|
678
|
+
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
661
679
|
animation:(smoothclip::SpringAnimation)animation
|
|
662
680
|
animationId:(int32_t)animationId {
|
|
663
681
|
[self storeRequestedPresentation:presentation];
|
|
@@ -675,7 +693,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
675
693
|
}
|
|
676
694
|
_activeAnimationKind = 2;
|
|
677
695
|
_activeAnimationId = animationId;
|
|
678
|
-
[self startAnimationToRequestedGeometryWithDuration:0];
|
|
696
|
+
return [self startAnimationToRequestedGeometryWithDuration:0];
|
|
679
697
|
}
|
|
680
698
|
|
|
681
699
|
- (CAKeyframeAnimation *)keyframeAnimationForKeyPath:(NSString *)keyPath
|
|
@@ -689,17 +707,17 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
689
707
|
return animation;
|
|
690
708
|
}
|
|
691
709
|
|
|
692
|
-
- (
|
|
710
|
+
- (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
|
|
693
711
|
keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
|
|
694
712
|
durationMs:(double)durationMs
|
|
695
713
|
animationId:(int32_t)animationId {
|
|
696
|
-
if (keyframes.size() < 2) return;
|
|
714
|
+
if (keyframes.size() < 2) return NO;
|
|
697
715
|
[self storeRequestedPresentation:presentation];
|
|
698
716
|
_activeAnimationKind = 3;
|
|
699
717
|
_activeAnimationId = animationId;
|
|
700
718
|
if (![self smoothClipCanDisplay]) {
|
|
701
719
|
_pendingAnimationInstall = YES;
|
|
702
|
-
return;
|
|
720
|
+
return NO;
|
|
703
721
|
}
|
|
704
722
|
[self stopLayerAnimationWithoutCallback];
|
|
705
723
|
_activeAnimationKind = 3;
|
|
@@ -726,7 +744,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
726
744
|
frame.presentation.clip.height,
|
|
727
745
|
frame.presentation.clip.radius,
|
|
728
746
|
hostSize,
|
|
729
|
-
&normalized)) return;
|
|
747
|
+
&normalized)) return NO;
|
|
730
748
|
[times addObject:@(frame.offset)];
|
|
731
749
|
[boundsValues addObject:[NSValue valueWithCGRect:normalized.rect]];
|
|
732
750
|
[positionValues addObject:[NSValue valueWithCGPoint:CGPointMake(
|
|
@@ -737,7 +755,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
737
755
|
}
|
|
738
756
|
|
|
739
757
|
SmoothNormalizedClipGeometry target;
|
|
740
|
-
if (![self normalizedRequestedGeometry:&target]) return;
|
|
758
|
+
if (![self normalizedRequestedGeometry:&target]) return NO;
|
|
741
759
|
[self writeLayerRect:target.rect radius:target.radius];
|
|
742
760
|
[self writeContentTranslation:_requestedContentTranslation];
|
|
743
761
|
[self setClipContainerHidden:NO];
|
|
@@ -776,6 +794,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
776
794
|
[_clipContainer.layer addAnimation:group forKey:@"smoothClip.geometry"];
|
|
777
795
|
[_contentContainer.layer addAnimation:contentGroup
|
|
778
796
|
forKey:@"smoothClip.content"];
|
|
797
|
+
return YES;
|
|
779
798
|
}
|
|
780
799
|
|
|
781
800
|
- (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget {
|
|
@@ -809,6 +828,10 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
809
828
|
|
|
810
829
|
- (void)applicationWillResignActive {
|
|
811
830
|
if (_activeAnimationId == 0 || _animationPaused) return;
|
|
831
|
+
const double springContinuationVelocity =
|
|
832
|
+
_activeAnimationKind == 2
|
|
833
|
+
? [self smoothClipSpringContinuationVelocity]
|
|
834
|
+
: 0;
|
|
812
835
|
const smoothclip::Presentation visible =
|
|
813
836
|
[self smoothClipCurrentPresentation];
|
|
814
837
|
_pausedRect = CGRectMake(
|
|
@@ -822,6 +845,16 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
822
845
|
visible.contentTranslateY);
|
|
823
846
|
_pausedRemaining = MAX(
|
|
824
847
|
0, _animationDuration - (CACurrentMediaTime() - _animationStartedAt));
|
|
848
|
+
if (_activeAnimationKind == 1 && _animationDuration > 0) {
|
|
849
|
+
const double progress = smoothclip::clamp01(
|
|
850
|
+
1 - _pausedRemaining / _animationDuration);
|
|
851
|
+
_timingAnimation =
|
|
852
|
+
smoothclip::timingRemainder(_timingAnimation, progress).animation;
|
|
853
|
+
_pausedRemaining = _timingAnimation.durationMs / 1000.0;
|
|
854
|
+
} else if (_activeAnimationKind == 2) {
|
|
855
|
+
_springAnimation.initialVelocity = springContinuationVelocity;
|
|
856
|
+
_springAnimation.inheritVelocity = false;
|
|
857
|
+
}
|
|
825
858
|
const int32_t animationId = _activeAnimationId;
|
|
826
859
|
const NSInteger animationKind = _activeAnimationKind;
|
|
827
860
|
[self stopLayerAnimationWithoutCallback];
|
|
@@ -944,10 +977,14 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
944
977
|
const CGPoint visibleContentTranslation = CGPointMake(
|
|
945
978
|
visible.contentTranslateX,
|
|
946
979
|
visible.contentTranslateY);
|
|
947
|
-
|
|
980
|
+
CFTimeInterval remaining = wasAnimating
|
|
948
981
|
? MAX(0, _animationDuration -
|
|
949
982
|
(CACurrentMediaTime() - _animationStartedAt))
|
|
950
983
|
: 0;
|
|
984
|
+
const double springContinuationVelocity =
|
|
985
|
+
wasAnimating && !_animationPaused && _activeAnimationKind == 2
|
|
986
|
+
? [self smoothClipSpringContinuationVelocity]
|
|
987
|
+
: 0;
|
|
951
988
|
|
|
952
989
|
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
953
990
|
_hasLayout = YES;
|
|
@@ -980,7 +1017,16 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
980
1017
|
[self stopLayerAnimationWithoutCallback];
|
|
981
1018
|
_activeAnimationId = animationId;
|
|
982
1019
|
_activeAnimationKind = animationKind;
|
|
983
|
-
if (_activeAnimationKind ==
|
|
1020
|
+
if (_activeAnimationKind == 1 && _animationDuration > 0) {
|
|
1021
|
+
const double progress = smoothclip::clamp01(
|
|
1022
|
+
1 - remaining / _animationDuration);
|
|
1023
|
+
_timingAnimation =
|
|
1024
|
+
smoothclip::timingRemainder(_timingAnimation, progress).animation;
|
|
1025
|
+
remaining = _timingAnimation.durationMs / 1000.0;
|
|
1026
|
+
} else if (_activeAnimationKind == 2) {
|
|
1027
|
+
_springAnimation.initialVelocity = springContinuationVelocity;
|
|
1028
|
+
_springAnimation.inheritVelocity = false;
|
|
1029
|
+
} else if (_activeAnimationKind == 3) {
|
|
984
1030
|
_activeAnimationKind = 1;
|
|
985
1031
|
_timingAnimation = {remaining * 1000.0, 0, 0, 1, 1, 2};
|
|
986
1032
|
}
|
|
@@ -1006,6 +1052,17 @@ static std::array<double, 7> SmoothClipVelocityChannels(
|
|
|
1006
1052
|
} else {
|
|
1007
1053
|
[self applyRequestedClip];
|
|
1008
1054
|
}
|
|
1055
|
+
// First layout is the other moment this view can become displayable: when
|
|
1056
|
+
// the window attach ran first, didMoveToWindow saw _hasLayout == NO and did
|
|
1057
|
+
// nothing, and nothing else tells the registry. Android notifies from both
|
|
1058
|
+
// triggers (setViewHostGeometryAndroid and viewBecameDisplayableAndroid);
|
|
1059
|
+
// without this a held latch — including one re-latched by unregisterView
|
|
1060
|
+
// when the last displayable host left — would never start on iOS. Costs one
|
|
1061
|
+
// registry lookup per layout; viewBecameDisplayable returns immediately when
|
|
1062
|
+
// there is no animation to resume.
|
|
1063
|
+
if (_driverId != 0 && [self smoothClipCanDisplay]) {
|
|
1064
|
+
smoothclip::viewBecameDisplayable(_driverId, self);
|
|
1065
|
+
}
|
|
1009
1066
|
}
|
|
1010
1067
|
|
|
1011
1068
|
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSmoothClipModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSmoothClipModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAuGlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,wBAAwB,CAAC","ignoreList":[]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
// Platform re-export only — the implementation is shared with iOS.
|
|
4
|
+
// Edit drivers.native.ts, not this file.
|
|
5
|
+
export { useSmoothClipDriver } from "./drivers.native.js";
|
|
4
6
|
//# sourceMappingURL=drivers.android.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useSmoothClipDriver"],"sourceRoot":"../../src","sources":["drivers.android.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,
|
|
1
|
+
{"version":3,"names":["useSmoothClipDriver"],"sourceRoot":"../../src","sources":["drivers.android.ts"],"mappings":";;AAAA;AACA;AACA,SAASA,mBAAmB,QAAQ,qBAAkB","ignoreList":[]}
|
|
@@ -1,394 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import NativeSmoothClipModule from './smoothClipNative';
|
|
7
|
-
import { allocateDriverId, attachDriverState, createDriverState, detachDriverState, getDriverState, setDriverState } from "./driverState.js";
|
|
8
|
-
import { completeNativeAnimation, synchronizeNativeCompletion } from "./nativeCompletion.js";
|
|
9
|
-
import { clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
|
|
10
|
-
import { createReactRequest, rejectDriverRequests, resolveReactRequest } from "./reactRequests.js";
|
|
11
|
-
const INTERACTIVE = 0;
|
|
12
|
-
const NATIVE = 1;
|
|
13
|
-
const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
|
|
14
|
-
const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
|
|
15
|
-
const animateTimingHostFunction = NativeSmoothClipModule.animateTiming;
|
|
16
|
-
const animateSpringHostFunction = NativeSmoothClipModule.animateSpring;
|
|
17
|
-
const animateKeyframesHostFunction = NativeSmoothClipModule.animateKeyframes;
|
|
18
|
-
const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
|
|
19
|
-
const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
|
|
20
|
-
const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
|
|
21
|
-
|
|
22
|
-
// Workaround for react-native-worklets 0.10: the RN-side serialization cache
|
|
23
|
-
// hands out one remote-function registry id per function forever, but the
|
|
24
|
-
// registry entry is deleted as soon as any transient UI-side proxy of that
|
|
25
|
-
// function is garbage collected. A later scheduleOnRN for the same function
|
|
26
|
-
// then reads `undefined` from the registry and aborts in Value::getObject.
|
|
27
|
-
// Pinning one UI-side reference for the app lifetime keeps the proxy — and
|
|
28
|
-
// with it the registry entry — alive. Remove once worklets decouples registry
|
|
29
|
-
// cleanup from proxy lifetime.
|
|
30
|
-
scheduleOnUI(resolver => {
|
|
31
|
-
'worklet';
|
|
32
|
-
|
|
33
|
-
globalThis.__smoothClipPinnedResolver = resolver;
|
|
34
|
-
}, resolveReactRequest);
|
|
35
|
-
function reduceMotionCode(value) {
|
|
36
|
-
switch (value) {
|
|
37
|
-
case 'always':
|
|
38
|
-
return 1;
|
|
39
|
-
case 'never':
|
|
40
|
-
return 2;
|
|
41
|
-
default:
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function animationIsFinite(target, animation) {
|
|
46
|
-
'worklet';
|
|
47
|
-
|
|
48
|
-
if (animation.from !== undefined && !isFiniteClipPresentation(animation.from)) {
|
|
49
|
-
// Silently dropping an invalid explicit start would re-open the exact
|
|
50
|
-
// stale-start handoff `from` exists to close — reject the whole call.
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
if (animation.type === 'timing') {
|
|
54
|
-
return Number.isFinite(animation.duration) && animation.controlPoints.every(Number.isFinite);
|
|
55
|
-
}
|
|
56
|
-
if (animation.type === 'spring') {
|
|
57
|
-
return [animation.mass ?? 1, animation.stiffness ?? 100, animation.damping ?? 10, animation.initialVelocity === 'inherit' ? 0 : animation.initialVelocity ?? 0].every(Number.isFinite);
|
|
58
|
-
}
|
|
59
|
-
if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
let previousOffset = -1;
|
|
63
|
-
for (const frame of animation.frames) {
|
|
64
|
-
if (!Number.isFinite(frame.offset) || frame.offset < 0 || frame.offset > 1 || frame.offset <= previousOffset || !isFiniteClipPresentation(frame.presentation)) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
previousOffset = frame.offset;
|
|
68
|
-
}
|
|
69
|
-
return animation.frames[0]?.offset === 0 && previousOffset === 1 && clipPresentationEquals(animation.frames[animation.frames.length - 1]?.presentation ?? null, target);
|
|
70
|
-
}
|
|
71
|
-
function presentationFromNative(values, fallback, offset = 0) {
|
|
72
|
-
'worklet';
|
|
73
|
-
|
|
74
|
-
if (values.length < offset + 7) return fallback;
|
|
75
|
-
const presentation = {
|
|
76
|
-
clip: {
|
|
77
|
-
x: values[offset],
|
|
78
|
-
y: values[offset + 1],
|
|
79
|
-
width: values[offset + 2],
|
|
80
|
-
height: values[offset + 3],
|
|
81
|
-
radius: values[offset + 4]
|
|
82
|
-
},
|
|
83
|
-
contentTranslateX: values[offset + 5],
|
|
84
|
-
contentTranslateY: values[offset + 6]
|
|
85
|
-
};
|
|
86
|
-
return isFiniteClipPresentation(presentation) ? presentation : fallback;
|
|
87
|
-
}
|
|
88
|
-
function flattenedKeyframes(animation) {
|
|
89
|
-
'worklet';
|
|
90
|
-
|
|
91
|
-
const values = [];
|
|
92
|
-
for (const frame of animation.frames) {
|
|
93
|
-
const {
|
|
94
|
-
clip,
|
|
95
|
-
contentTranslateX,
|
|
96
|
-
contentTranslateY
|
|
97
|
-
} = frame.presentation;
|
|
98
|
-
values.push(frame.offset, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY);
|
|
99
|
-
}
|
|
100
|
-
return values;
|
|
101
|
-
}
|
|
102
|
-
function uiOnly() {
|
|
103
|
-
'worklet';
|
|
104
|
-
|
|
105
|
-
throw new Error('[SmoothClipView] driver.ui methods must run on the UI runtime. Use driver.react from React code.');
|
|
106
|
-
}
|
|
107
|
-
export function useSmoothClipDriver(initialValue, options = {}) {
|
|
108
|
-
const initialPresentation = 'clip' in initialValue ? initialValue : createClipPresentation(initialValue);
|
|
109
|
-
const presentation = useSharedValue(initialPresentation);
|
|
110
|
-
const ownership = useSharedValue(INTERACTIVE);
|
|
111
|
-
const suppressDeliveries = useSharedValue(0);
|
|
112
|
-
const activeAnimationId = useSharedValue(0);
|
|
113
|
-
// Non-zero after a setScalars hot write: driver.presentation is then stale,
|
|
114
|
-
// so animateTo must start from the native registry's latest value instead
|
|
115
|
-
// of snapping back to the stale SharedValue.
|
|
116
|
-
const scalarsStale = useSharedValue(0);
|
|
117
|
-
const callbackRef = useRef(options.onAnimationComplete);
|
|
118
|
-
const driverRef = useRef(null);
|
|
119
|
-
callbackRef.current = options.onAnimationComplete;
|
|
120
|
-
if (driverRef.current === null) {
|
|
121
|
-
const driverId = allocateDriverId();
|
|
122
|
-
const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
|
|
123
|
-
const seedPresentation = next => {
|
|
124
|
-
'worklet';
|
|
125
|
-
|
|
126
|
-
if (!clipPresentationEquals(presentation.value, next)) {
|
|
127
|
-
suppressDeliveries.value += 1;
|
|
128
|
-
presentation.value = next;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
const beginOnUI = () => {
|
|
132
|
-
'worklet';
|
|
133
|
-
|
|
134
|
-
const current = presentationFromNative(beginInteractionHostFunction(driverId), presentation.value);
|
|
135
|
-
activeAnimationId.value = 0;
|
|
136
|
-
ownership.value = INTERACTIVE;
|
|
137
|
-
scalarsStale.value = 0;
|
|
138
|
-
seedPresentation(current);
|
|
139
|
-
return current;
|
|
140
|
-
};
|
|
141
|
-
const setOnUI = next => {
|
|
142
|
-
'worklet';
|
|
143
|
-
|
|
144
|
-
if (!isFiniteClipPresentation(next)) return;
|
|
145
|
-
activeAnimationId.value = 0;
|
|
146
|
-
ownership.value = INTERACTIVE;
|
|
147
|
-
scalarsStale.value = 0;
|
|
148
|
-
const {
|
|
149
|
-
clip,
|
|
150
|
-
contentTranslateX,
|
|
151
|
-
contentTranslateY
|
|
152
|
-
} = next;
|
|
153
|
-
setPresentationHostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, true);
|
|
154
|
-
seedPresentation(next);
|
|
155
|
-
};
|
|
156
|
-
const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY) => {
|
|
157
|
-
'worklet';
|
|
158
|
-
|
|
159
|
-
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(radius) || !Number.isFinite(contentTranslateX) || !Number.isFinite(contentTranslateY)) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
|
|
163
|
-
if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
|
|
164
|
-
if (scalarsStale.value === 0) scalarsStale.value = 1;
|
|
165
|
-
setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true);
|
|
166
|
-
};
|
|
167
|
-
const animateOnUI = (target, animation) => {
|
|
168
|
-
'worklet';
|
|
169
|
-
|
|
170
|
-
if (!isFiniteClipPresentation(target) || !animationIsFinite(target, animation)) {
|
|
171
|
-
return rejectAnimationHostFunction(driverId);
|
|
172
|
-
}
|
|
173
|
-
const from = animation.from;
|
|
174
|
-
if (from !== undefined) {
|
|
175
|
-
// Fused hot write: exactly setScalars(from…) issued immediately
|
|
176
|
-
// before the handoff, so native's latest value — the animation
|
|
177
|
-
// start below — is the caller's explicit presentation. Also
|
|
178
|
-
// re-grabs from a running animation, which the implicit
|
|
179
|
-
// interactive-start path would silently skip.
|
|
180
|
-
setScalarsOnUI(from.clip.x, from.clip.y, from.clip.width, from.clip.height, from.clip.radius, from.contentTranslateX, from.contentTranslateY);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// After a setScalars hot write the SharedValue no longer matches what
|
|
184
|
-
// is on screen; passing it as the start would snap the animation back.
|
|
185
|
-
// Without an interactive start, native resolves the start from its own
|
|
186
|
-
// latest value instead.
|
|
187
|
-
const scalarsWereStale = scalarsStale.value !== 0;
|
|
188
|
-
const hasInteractiveStart = ownership.value === INTERACTIVE && !scalarsWereStale;
|
|
189
|
-
const start = presentation.value;
|
|
190
|
-
ownership.value = NATIVE;
|
|
191
|
-
presentation.value = target;
|
|
192
|
-
scalarsStale.value = 0;
|
|
193
|
-
const startClip = start.clip;
|
|
194
|
-
const targetClip = target.clip;
|
|
195
|
-
let animationId;
|
|
196
|
-
if (animation.type === 'timing') {
|
|
197
|
-
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
198
|
-
animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), x1, y1, x2, y2, reduceMotion);
|
|
199
|
-
} else if (animation.type === 'spring') {
|
|
200
|
-
const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
|
|
201
|
-
animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0.0001, animation.mass ?? 1), Math.max(0.0001, animation.stiffness ?? 100), Math.max(0, animation.damping ?? 10), inheritVelocity ? 0 : animation.initialVelocity, inheritVelocity, reduceMotion);
|
|
202
|
-
} else {
|
|
203
|
-
animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), flattenedKeyframes(animation), reduceMotion);
|
|
204
|
-
}
|
|
205
|
-
if (animationId === 0) {
|
|
206
|
-
// Native rejected the transition after ownership was transferred.
|
|
207
|
-
// Restore interactive ownership and the pre-animation value, then
|
|
208
|
-
// emit a standalone finished:false completion so completion-driven
|
|
209
|
-
// state machines observe exactly one result per animateTo call.
|
|
210
|
-
ownership.value = INTERACTIVE;
|
|
211
|
-
presentation.value = start;
|
|
212
|
-
if (scalarsWereStale) scalarsStale.value = 1;
|
|
213
|
-
activeAnimationId.value = 0;
|
|
214
|
-
return rejectAnimationHostFunction(driverId);
|
|
215
|
-
}
|
|
216
|
-
activeAnimationId.value = animationId;
|
|
217
|
-
return animationId;
|
|
218
|
-
};
|
|
219
|
-
const cancelOnUI = (animationId = 0, behavior = 'current') => {
|
|
220
|
-
'worklet';
|
|
221
|
-
|
|
222
|
-
const values = cancelAnimationHostFunction(driverId, animationId, behavior === 'target' ? 1 : 0);
|
|
223
|
-
if (values.length < 8 || values[0] !== 1) return presentation.value;
|
|
224
|
-
const current = presentationFromNative(values, presentation.value, 1);
|
|
225
|
-
activeAnimationId.value = 0;
|
|
226
|
-
ownership.value = INTERACTIVE;
|
|
227
|
-
scalarsStale.value = 0;
|
|
228
|
-
seedPresentation(current);
|
|
229
|
-
return current;
|
|
230
|
-
};
|
|
231
|
-
const driver = {
|
|
232
|
-
kind: 'hybrid',
|
|
233
|
-
presentation,
|
|
234
|
-
ui: {
|
|
235
|
-
beginInteraction() {
|
|
236
|
-
'worklet';
|
|
237
|
-
|
|
238
|
-
if (isRNRuntime()) return uiOnly();
|
|
239
|
-
return beginOnUI();
|
|
240
|
-
},
|
|
241
|
-
set(next) {
|
|
242
|
-
'worklet';
|
|
243
|
-
|
|
244
|
-
if (isRNRuntime()) return uiOnly();
|
|
245
|
-
setOnUI(next);
|
|
246
|
-
},
|
|
247
|
-
setScalars(x, y, width, height, radius, contentTranslateX, contentTranslateY) {
|
|
248
|
-
'worklet';
|
|
249
|
-
|
|
250
|
-
if (isRNRuntime()) return uiOnly();
|
|
251
|
-
setScalarsOnUI(x, y, width, height, radius, contentTranslateX, contentTranslateY);
|
|
252
|
-
},
|
|
253
|
-
animateTo(target, animation) {
|
|
254
|
-
'worklet';
|
|
255
|
-
|
|
256
|
-
if (isRNRuntime()) return uiOnly();
|
|
257
|
-
return animateOnUI(target, animation);
|
|
258
|
-
},
|
|
259
|
-
cancel(animationId = 0, behavior = 'current') {
|
|
260
|
-
'worklet';
|
|
261
|
-
|
|
262
|
-
if (isRNRuntime()) return uiOnly();
|
|
263
|
-
return cancelOnUI(animationId, behavior);
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
react: {
|
|
267
|
-
beginInteraction() {
|
|
268
|
-
const {
|
|
269
|
-
requestId,
|
|
270
|
-
promise
|
|
271
|
-
} = createReactRequest(driverId);
|
|
272
|
-
scheduleOnUI(() => {
|
|
273
|
-
'worklet';
|
|
274
|
-
|
|
275
|
-
const result = beginOnUI();
|
|
276
|
-
scheduleOnRN(resolveReactRequest, driverId, requestId, result);
|
|
277
|
-
});
|
|
278
|
-
return promise;
|
|
279
|
-
},
|
|
280
|
-
set(next) {
|
|
281
|
-
const {
|
|
282
|
-
requestId,
|
|
283
|
-
promise
|
|
284
|
-
} = createReactRequest(driverId);
|
|
285
|
-
scheduleOnUI(() => {
|
|
286
|
-
'worklet';
|
|
287
|
-
|
|
288
|
-
setOnUI(next);
|
|
289
|
-
scheduleOnRN(resolveReactRequest, driverId, requestId, undefined);
|
|
290
|
-
});
|
|
291
|
-
return promise;
|
|
292
|
-
},
|
|
293
|
-
animateTo(target, animation) {
|
|
294
|
-
const {
|
|
295
|
-
requestId,
|
|
296
|
-
promise
|
|
297
|
-
} = createReactRequest(driverId, true);
|
|
298
|
-
scheduleOnUI(() => {
|
|
299
|
-
'worklet';
|
|
300
|
-
|
|
301
|
-
const result = animateOnUI(target, animation);
|
|
302
|
-
scheduleOnRN(resolveReactRequest, driverId, requestId, result, true);
|
|
303
|
-
});
|
|
304
|
-
return promise;
|
|
305
|
-
},
|
|
306
|
-
cancel(animationId = 0, behavior = 'current') {
|
|
307
|
-
const {
|
|
308
|
-
requestId,
|
|
309
|
-
promise
|
|
310
|
-
} = createReactRequest(driverId, true);
|
|
311
|
-
scheduleOnUI(() => {
|
|
312
|
-
'worklet';
|
|
313
|
-
|
|
314
|
-
const result = cancelOnUI(animationId, behavior);
|
|
315
|
-
scheduleOnRN(resolveReactRequest, driverId, requestId, result, true);
|
|
316
|
-
});
|
|
317
|
-
return promise;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
setDriverState(driver, createDriverState(driverId, initialPresentation, presentation, callbackRef, activeAnimationId, ownership));
|
|
322
|
-
driverRef.current = driver;
|
|
323
|
-
}
|
|
324
|
-
const driver = driverRef.current;
|
|
325
|
-
useEffect(() => {
|
|
326
|
-
const currentDriver = driverRef.current;
|
|
327
|
-
if (!currentDriver) return undefined;
|
|
328
|
-
const state = getDriverState(currentDriver);
|
|
329
|
-
const {
|
|
330
|
-
driverId
|
|
331
|
-
} = state;
|
|
332
|
-
// Re-attach on every effect run: StrictMode/<Activity> replay the effect
|
|
333
|
-
// after its cleanup detached this state and destroyed the native driver.
|
|
334
|
-
// The authoritative seed below recreates the native entry in that case.
|
|
335
|
-
attachDriverState(state);
|
|
336
|
-
const subscription = NativeSmoothClipModule.onClipAnimationComplete(result => {
|
|
337
|
-
if (result.driverId !== driverId) return;
|
|
338
|
-
synchronizeNativeCompletion(activeAnimationId, ownership, result.animationId, result.finished);
|
|
339
|
-
completeNativeAnimation(driverId, result.animationId, result.finished);
|
|
340
|
-
});
|
|
341
|
-
scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, setter) => {
|
|
342
|
-
'worklet';
|
|
343
|
-
|
|
344
|
-
let last = null;
|
|
345
|
-
const deliver = next => {
|
|
346
|
-
if (!isFiniteClipPresentation(next) || clipPresentationEquals(last, next)) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
// Record `last` only for values native has actually observed:
|
|
350
|
-
// deliveries dropped during native ownership must stay eligible
|
|
351
|
-
// for re-delivery once interactive ownership returns.
|
|
352
|
-
if (owner.value !== INTERACTIVE) return;
|
|
353
|
-
if (suppressed.value > 0) {
|
|
354
|
-
suppressed.value -= 1;
|
|
355
|
-
last = next;
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
last = next;
|
|
359
|
-
if (stale.value !== 0) stale.value = 0;
|
|
360
|
-
const {
|
|
361
|
-
clip,
|
|
362
|
-
contentTranslateX,
|
|
363
|
-
contentTranslateY
|
|
364
|
-
} = next;
|
|
365
|
-
setter(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, false);
|
|
366
|
-
};
|
|
367
|
-
source.addListener(listenerId, deliver);
|
|
368
|
-
// Authoritative take-ownership seed. Creates the native entry before
|
|
369
|
-
// any view mounts on the first run, and revives a driver destroyed by
|
|
370
|
-
// an effect-replay cleanup (StrictMode/<Activity>) on later runs.
|
|
371
|
-
const current = source.value;
|
|
372
|
-
if (isFiniteClipPresentation(current)) {
|
|
373
|
-
active.value = 0;
|
|
374
|
-
owner.value = INTERACTIVE;
|
|
375
|
-
stale.value = 0;
|
|
376
|
-
last = current;
|
|
377
|
-
setter(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.radius, current.contentTranslateX, current.contentTranslateY, true);
|
|
378
|
-
}
|
|
379
|
-
}, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, setPresentationHostFunction);
|
|
380
|
-
return () => {
|
|
381
|
-
subscription.remove();
|
|
382
|
-
rejectDriverRequests(driverId);
|
|
383
|
-
detachDriverState(state);
|
|
384
|
-
scheduleOnUI((source, listenerId, nativeDriverId, destroy) => {
|
|
385
|
-
'worklet';
|
|
386
|
-
|
|
387
|
-
source.removeListener(listenerId);
|
|
388
|
-
destroy(nativeDriverId);
|
|
389
|
-
}, presentation, driverId, driverId, destroyDriverHostFunction);
|
|
390
|
-
};
|
|
391
|
-
}, [activeAnimationId, ownership, presentation, scalarsStale, suppressDeliveries]);
|
|
392
|
-
return driver;
|
|
393
|
-
}
|
|
3
|
+
// Platform re-export only — the implementation is shared with Android.
|
|
4
|
+
// Edit drivers.native.ts, not this file.
|
|
5
|
+
export { useSmoothClipDriver } from "./drivers.native.js";
|
|
394
6
|
//# sourceMappingURL=drivers.ios.js.map
|