react-native-smooth-clip-view 0.1.0 → 0.2.1
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 +171 -43
- package/SmoothClipView.podspec +8 -2
- package/android/CMakeLists.txt +39 -0
- package/android/build.gradle +69 -2
- package/android/proguard-rules.pro +6 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
- package/android/src/main/cpp/SmoothClipRegistry.cpp +945 -0
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
- package/cpp/SmoothClipRegistry.h +93 -0
- package/cpp/SmoothClipSharedGeometry.h +50 -0
- package/ios/SmoothClipModuleProvider.h +9 -0
- package/ios/SmoothClipModuleProvider.mm +16 -0
- package/ios/SmoothClipRegistry.mm +754 -0
- package/ios/SmoothClipTurboModule.cpp +290 -0
- package/ios/SmoothClipTurboModule.h +132 -0
- package/ios/SmoothClipView.mm +976 -85
- package/ios/SmoothClipViewRegistry.h +27 -0
- package/lib/module/NativeSmoothClipModule.js +5 -0
- package/lib/module/NativeSmoothClipModule.js.map +1 -0
- package/lib/module/SmoothClipView.android.js +4 -0
- package/lib/module/SmoothClipView.android.js.map +1 -0
- package/lib/module/SmoothClipView.ios.js +34 -0
- package/lib/module/SmoothClipView.ios.js.map +1 -0
- package/lib/module/SmoothClipView.js +26 -12
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipView.web.js +28 -12
- package/lib/module/SmoothClipView.web.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
- package/lib/module/driverState.js +102 -0
- package/lib/module/driverState.js.map +1 -0
- package/lib/module/driverTypes.js +4 -0
- package/lib/module/driverTypes.js.map +1 -0
- package/lib/module/drivers.android.js +4 -0
- package/lib/module/drivers.android.js.map +1 -0
- package/lib/module/drivers.ios.js +380 -0
- package/lib/module/drivers.ios.js.map +1 -0
- package/lib/module/drivers.js +276 -0
- package/lib/module/drivers.js.map +1 -0
- package/lib/module/geometry.js +19 -0
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/nativeCompletion.js +21 -0
- package/lib/module/nativeCompletion.js.map +1 -0
- package/lib/module/reactRequests.js +50 -0
- package/lib/module/reactRequests.js.map +1 -0
- package/lib/module/smoothClipNative.android.js +15 -0
- package/lib/module/smoothClipNative.android.js.map +1 -0
- package/lib/module/smoothClipNative.js +8 -0
- package/lib/module/smoothClipNative.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.d.ts +5 -6
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
- package/lib/typescript/src/SmoothClipView.web.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/driverState.d.ts +31 -0
- package/lib/typescript/src/driverState.d.ts.map +1 -0
- package/lib/typescript/src/driverTypes.d.ts +61 -0
- package/lib/typescript/src/driverTypes.d.ts.map +1 -0
- package/lib/typescript/src/drivers.android.d.ts +3 -0
- package/lib/typescript/src/drivers.android.d.ts.map +1 -0
- package/lib/typescript/src/drivers.d.ts +5 -0
- package/lib/typescript/src/drivers.d.ts.map +1 -0
- package/lib/typescript/src/drivers.ios.d.ts +5 -0
- package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +8 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +4 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +7 -0
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
- package/lib/typescript/src/reactRequests.d.ts +7 -0
- package/lib/typescript/src/reactRequests.d.ts.map +1 -0
- package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
- package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
- package/lib/typescript/src/smoothClipNative.d.ts +3 -0
- package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
- package/package.json +12 -7
- package/src/NativeSmoothClipModule.ts +104 -0
- package/src/SmoothClipView.android.tsx +2 -0
- package/src/SmoothClipView.ios.tsx +36 -0
- package/src/SmoothClipView.tsx +31 -27
- package/src/SmoothClipView.web.tsx +26 -22
- package/src/SmoothClipViewNativeComponent.ts +14 -1
- package/src/driverState.ts +150 -0
- package/src/driverTypes.ts +91 -0
- package/src/drivers.android.ts +6 -0
- package/src/drivers.ios.ts +665 -0
- package/src/drivers.ts +416 -0
- package/src/geometry.ts +39 -0
- package/src/index.ts +19 -1
- package/src/index.web.ts +19 -1
- package/src/nativeCompletion.ts +37 -0
- package/src/reactRequests.ts +64 -0
- package/src/smoothClipNative.android.ts +20 -0
- package/src/smoothClipNative.ts +5 -0
package/src/drivers.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
cancelAnimation as cancelReanimatedAnimation,
|
|
4
|
+
Easing,
|
|
5
|
+
ReduceMotion,
|
|
6
|
+
useAnimatedReaction,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withSpring,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
12
|
+
import type {
|
|
13
|
+
ClipReduceMotion,
|
|
14
|
+
KeyframedClipAnimation,
|
|
15
|
+
SmoothClipAnimation,
|
|
16
|
+
SmoothClipDriver,
|
|
17
|
+
SmoothClipDriverOptions,
|
|
18
|
+
SpringClipAnimation,
|
|
19
|
+
TimingClipAnimation,
|
|
20
|
+
} from './driverTypes';
|
|
21
|
+
import {
|
|
22
|
+
allocateDriverId,
|
|
23
|
+
attachDriverState,
|
|
24
|
+
createDriverState,
|
|
25
|
+
deliverDriverCompletion,
|
|
26
|
+
detachDriverState,
|
|
27
|
+
finishDriverAnimation,
|
|
28
|
+
getDriverState,
|
|
29
|
+
setDriverState,
|
|
30
|
+
snapshotDriverViews,
|
|
31
|
+
} from './driverState';
|
|
32
|
+
import {
|
|
33
|
+
createClipPresentation,
|
|
34
|
+
clipPresentationEquals,
|
|
35
|
+
isFiniteClipPresentation,
|
|
36
|
+
type ClipGeometry,
|
|
37
|
+
type SmoothClipPresentation,
|
|
38
|
+
} from './geometry';
|
|
39
|
+
import {
|
|
40
|
+
createReactRequest,
|
|
41
|
+
rejectDriverRequests,
|
|
42
|
+
resolveReactRequest,
|
|
43
|
+
} from './reactRequests';
|
|
44
|
+
|
|
45
|
+
function toReanimatedReduceMotion(value: ClipReduceMotion): ReduceMotion {
|
|
46
|
+
switch (value) {
|
|
47
|
+
case 'always':
|
|
48
|
+
return ReduceMotion.Always;
|
|
49
|
+
case 'never':
|
|
50
|
+
return ReduceMotion.Never;
|
|
51
|
+
default:
|
|
52
|
+
return ReduceMotion.System;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function animationIsFinite(animation: SmoothClipAnimation): boolean {
|
|
57
|
+
'worklet';
|
|
58
|
+
if (animation.type === 'timing') {
|
|
59
|
+
return (
|
|
60
|
+
Number.isFinite(animation.duration) &&
|
|
61
|
+
animation.controlPoints.every(Number.isFinite)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (animation.type === 'spring') {
|
|
65
|
+
return [
|
|
66
|
+
animation.mass ?? 1,
|
|
67
|
+
animation.stiffness ?? 100,
|
|
68
|
+
animation.damping ?? 10,
|
|
69
|
+
animation.initialVelocity === 'inherit'
|
|
70
|
+
? 0
|
|
71
|
+
: (animation.initialVelocity ?? 0),
|
|
72
|
+
].every(Number.isFinite);
|
|
73
|
+
}
|
|
74
|
+
let previousOffset = -1;
|
|
75
|
+
if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
for (const frame of animation.frames) {
|
|
79
|
+
if (
|
|
80
|
+
!Number.isFinite(frame.offset) ||
|
|
81
|
+
frame.offset < 0 ||
|
|
82
|
+
frame.offset > 1 ||
|
|
83
|
+
frame.offset <= previousOffset ||
|
|
84
|
+
!isFiniteClipPresentation(frame.presentation)
|
|
85
|
+
) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
previousOffset = frame.offset;
|
|
89
|
+
}
|
|
90
|
+
return animation.frames[0]?.offset === 0 && previousOffset === 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function interpolatePresentation(
|
|
94
|
+
from: SmoothClipPresentation,
|
|
95
|
+
to: SmoothClipPresentation,
|
|
96
|
+
progress: number
|
|
97
|
+
): SmoothClipPresentation {
|
|
98
|
+
'worklet';
|
|
99
|
+
const mix = (start: number, end: number) => start + (end - start) * progress;
|
|
100
|
+
return {
|
|
101
|
+
clip: {
|
|
102
|
+
x: mix(from.clip.x, to.clip.x),
|
|
103
|
+
y: mix(from.clip.y, to.clip.y),
|
|
104
|
+
width: mix(from.clip.width, to.clip.width),
|
|
105
|
+
height: mix(from.clip.height, to.clip.height),
|
|
106
|
+
radius: mix(from.clip.radius, to.clip.radius),
|
|
107
|
+
},
|
|
108
|
+
contentTranslateX: mix(from.contentTranslateX, to.contentTranslateX),
|
|
109
|
+
contentTranslateY: mix(from.contentTranslateY, to.contentTranslateY),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function finishFallback(
|
|
114
|
+
driverId: number,
|
|
115
|
+
animationId: number,
|
|
116
|
+
finished: boolean
|
|
117
|
+
): void {
|
|
118
|
+
deliverDriverCompletion(
|
|
119
|
+
driverId,
|
|
120
|
+
animationId,
|
|
121
|
+
finishDriverAnimation(driverId, animationId, finished)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useSmoothClipDriver(
|
|
126
|
+
initialValue: ClipGeometry | SmoothClipPresentation,
|
|
127
|
+
options: SmoothClipDriverOptions = {}
|
|
128
|
+
): SmoothClipDriver {
|
|
129
|
+
const initialPresentation =
|
|
130
|
+
'clip' in initialValue
|
|
131
|
+
? initialValue
|
|
132
|
+
: createClipPresentation(initialValue);
|
|
133
|
+
const presentation = useSharedValue(initialPresentation);
|
|
134
|
+
const activeAnimationId = useSharedValue(0);
|
|
135
|
+
const nextAnimationId = useSharedValue(0);
|
|
136
|
+
const latestTarget = useSharedValue(initialPresentation);
|
|
137
|
+
const animationStart = useSharedValue(initialPresentation);
|
|
138
|
+
const animationProgress = useSharedValue(0);
|
|
139
|
+
const keyframes = useSharedValue<
|
|
140
|
+
readonly Readonly<{
|
|
141
|
+
offset: number;
|
|
142
|
+
presentation: SmoothClipPresentation;
|
|
143
|
+
}>[]
|
|
144
|
+
>([]);
|
|
145
|
+
const callbackRef = useRef(options.onAnimationComplete);
|
|
146
|
+
const driverRef = useRef<SmoothClipDriver | null>(null);
|
|
147
|
+
callbackRef.current = options.onAnimationComplete;
|
|
148
|
+
|
|
149
|
+
useAnimatedReaction(
|
|
150
|
+
() => animationProgress.value,
|
|
151
|
+
(progress) => {
|
|
152
|
+
const frames = keyframes.value;
|
|
153
|
+
if (frames.length >= 2) {
|
|
154
|
+
let upperIndex = 1;
|
|
155
|
+
while (
|
|
156
|
+
upperIndex < frames.length - 1 &&
|
|
157
|
+
progress > (frames[upperIndex]?.offset ?? 1)
|
|
158
|
+
) {
|
|
159
|
+
upperIndex += 1;
|
|
160
|
+
}
|
|
161
|
+
const lower = frames[upperIndex - 1];
|
|
162
|
+
const upper = frames[upperIndex];
|
|
163
|
+
if (!lower || !upper) return;
|
|
164
|
+
const span = upper.offset - lower.offset;
|
|
165
|
+
const localProgress = span <= 0 ? 1 : (progress - lower.offset) / span;
|
|
166
|
+
presentation.value = interpolatePresentation(
|
|
167
|
+
lower.presentation,
|
|
168
|
+
upper.presentation,
|
|
169
|
+
Math.min(1, Math.max(0, localProgress))
|
|
170
|
+
);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
presentation.value = interpolatePresentation(
|
|
174
|
+
animationStart.value,
|
|
175
|
+
latestTarget.value,
|
|
176
|
+
progress
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
[animationProgress, animationStart, keyframes, latestTarget, presentation]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (driverRef.current === null) {
|
|
183
|
+
const driverId = allocateDriverId();
|
|
184
|
+
const reduceMotion = toReanimatedReduceMotion(
|
|
185
|
+
options.reduceMotion ?? 'system'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const beginOnUI = (): SmoothClipPresentation => {
|
|
189
|
+
'worklet';
|
|
190
|
+
cancelReanimatedAnimation(animationProgress);
|
|
191
|
+
activeAnimationId.value = 0;
|
|
192
|
+
return presentation.value;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const setOnUI = (next: SmoothClipPresentation): void => {
|
|
196
|
+
'worklet';
|
|
197
|
+
if (!isFiniteClipPresentation(next)) return;
|
|
198
|
+
cancelReanimatedAnimation(animationProgress);
|
|
199
|
+
activeAnimationId.value = 0;
|
|
200
|
+
latestTarget.value = next;
|
|
201
|
+
presentation.value = next;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const animateOnUI = (
|
|
205
|
+
next: SmoothClipPresentation,
|
|
206
|
+
animation: SmoothClipAnimation
|
|
207
|
+
): number => {
|
|
208
|
+
'worklet';
|
|
209
|
+
nextAnimationId.value = (nextAnimationId.value % 0x7ffffffe) + 1;
|
|
210
|
+
const animationId = nextAnimationId.value;
|
|
211
|
+
if (!isFiniteClipPresentation(next) || !animationIsFinite(animation)) {
|
|
212
|
+
scheduleOnRN(finishFallback, driverId, animationId, false);
|
|
213
|
+
return animationId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
activeAnimationId.value = animationId;
|
|
217
|
+
animationStart.value = presentation.value;
|
|
218
|
+
latestTarget.value = next;
|
|
219
|
+
keyframes.value = [];
|
|
220
|
+
animationProgress.value = 0;
|
|
221
|
+
scheduleOnRN(snapshotDriverViews, driverId, animationId);
|
|
222
|
+
const onComplete = (finished?: boolean) => {
|
|
223
|
+
'worklet';
|
|
224
|
+
if (activeAnimationId.value === animationId) {
|
|
225
|
+
activeAnimationId.value = 0;
|
|
226
|
+
}
|
|
227
|
+
scheduleOnRN(finishFallback, driverId, animationId, finished === true);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (animation.type === 'timing') {
|
|
231
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
232
|
+
animationProgress.value = withTiming(
|
|
233
|
+
1,
|
|
234
|
+
{
|
|
235
|
+
duration: Math.max(0, animation.duration),
|
|
236
|
+
easing: Easing.bezier(x1, y1, x2, y2),
|
|
237
|
+
reduceMotion,
|
|
238
|
+
},
|
|
239
|
+
onComplete
|
|
240
|
+
);
|
|
241
|
+
} else if (animation.type === 'spring') {
|
|
242
|
+
const configuredVelocity = animation.initialVelocity;
|
|
243
|
+
animationProgress.value = withSpring(
|
|
244
|
+
1,
|
|
245
|
+
{
|
|
246
|
+
damping: animation.damping ?? 10,
|
|
247
|
+
mass: animation.mass ?? 1,
|
|
248
|
+
reduceMotion,
|
|
249
|
+
stiffness: animation.stiffness ?? 100,
|
|
250
|
+
// The fallback driver cannot sample native presentation motion,
|
|
251
|
+
// so 'inherit' (and undefined) degrade to zero launch velocity.
|
|
252
|
+
velocity:
|
|
253
|
+
typeof configuredVelocity === 'number' ? configuredVelocity : 0,
|
|
254
|
+
},
|
|
255
|
+
onComplete
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
if (
|
|
259
|
+
!clipPresentationEquals(
|
|
260
|
+
animation.frames[animation.frames.length - 1]?.presentation ?? null,
|
|
261
|
+
next
|
|
262
|
+
)
|
|
263
|
+
) {
|
|
264
|
+
scheduleOnRN(finishFallback, driverId, animationId, false);
|
|
265
|
+
activeAnimationId.value = 0;
|
|
266
|
+
return animationId;
|
|
267
|
+
}
|
|
268
|
+
keyframes.value = animation.frames;
|
|
269
|
+
animationProgress.value = withTiming(
|
|
270
|
+
1,
|
|
271
|
+
{
|
|
272
|
+
duration: Math.max(0, animation.duration),
|
|
273
|
+
easing: Easing.linear,
|
|
274
|
+
reduceMotion,
|
|
275
|
+
},
|
|
276
|
+
onComplete
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return animationId;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const cancelOnUI = (
|
|
283
|
+
animationId = 0,
|
|
284
|
+
behavior: 'current' | 'target' = 'current'
|
|
285
|
+
): SmoothClipPresentation => {
|
|
286
|
+
'worklet';
|
|
287
|
+
if (animationId !== 0 && animationId !== activeAnimationId.value) {
|
|
288
|
+
return presentation.value;
|
|
289
|
+
}
|
|
290
|
+
cancelReanimatedAnimation(animationProgress);
|
|
291
|
+
activeAnimationId.value = 0;
|
|
292
|
+
if (behavior === 'target') presentation.value = latestTarget.value;
|
|
293
|
+
return presentation.value;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const setScalarsOnUI = (
|
|
297
|
+
x: number,
|
|
298
|
+
y: number,
|
|
299
|
+
width: number,
|
|
300
|
+
height: number,
|
|
301
|
+
radius: number,
|
|
302
|
+
contentTranslateX: number,
|
|
303
|
+
contentTranslateY: number
|
|
304
|
+
): void => {
|
|
305
|
+
'worklet';
|
|
306
|
+
setOnUI({
|
|
307
|
+
clip: { x, y, width, height, radius },
|
|
308
|
+
contentTranslateX,
|
|
309
|
+
contentTranslateY,
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const driver: SmoothClipDriver = {
|
|
314
|
+
kind: 'hybrid',
|
|
315
|
+
presentation,
|
|
316
|
+
// Every worklet here shares the single JS runtime, so unlike the native
|
|
317
|
+
// driver there is no runtime guard on `ui`: calling it from React code
|
|
318
|
+
// is harmless in this fallback.
|
|
319
|
+
ui: {
|
|
320
|
+
beginInteraction: beginOnUI,
|
|
321
|
+
set: setOnUI,
|
|
322
|
+
setScalars: setScalarsOnUI,
|
|
323
|
+
animateTo: animateOnUI,
|
|
324
|
+
cancel: cancelOnUI,
|
|
325
|
+
},
|
|
326
|
+
react: {
|
|
327
|
+
beginInteraction() {
|
|
328
|
+
const { requestId, promise } =
|
|
329
|
+
createReactRequest<SmoothClipPresentation>(driverId);
|
|
330
|
+
scheduleOnUI(() => {
|
|
331
|
+
'worklet';
|
|
332
|
+
const result = beginOnUI();
|
|
333
|
+
scheduleOnRN(resolveReactRequest, driverId, requestId, result);
|
|
334
|
+
});
|
|
335
|
+
return promise;
|
|
336
|
+
},
|
|
337
|
+
set(next) {
|
|
338
|
+
const { requestId, promise } = createReactRequest<void>(driverId);
|
|
339
|
+
scheduleOnUI(() => {
|
|
340
|
+
'worklet';
|
|
341
|
+
setOnUI(next);
|
|
342
|
+
scheduleOnRN(resolveReactRequest, driverId, requestId, undefined);
|
|
343
|
+
});
|
|
344
|
+
return promise;
|
|
345
|
+
},
|
|
346
|
+
animateTo(next, animation) {
|
|
347
|
+
const { requestId, promise } = createReactRequest<number>(
|
|
348
|
+
driverId,
|
|
349
|
+
true
|
|
350
|
+
);
|
|
351
|
+
scheduleOnUI(() => {
|
|
352
|
+
'worklet';
|
|
353
|
+
const result = animateOnUI(next, animation);
|
|
354
|
+
scheduleOnRN(
|
|
355
|
+
resolveReactRequest,
|
|
356
|
+
driverId,
|
|
357
|
+
requestId,
|
|
358
|
+
result,
|
|
359
|
+
true
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
return promise;
|
|
363
|
+
},
|
|
364
|
+
cancel(animationId = 0, behavior = 'current') {
|
|
365
|
+
const { requestId, promise } =
|
|
366
|
+
createReactRequest<SmoothClipPresentation>(driverId, true);
|
|
367
|
+
scheduleOnUI(() => {
|
|
368
|
+
'worklet';
|
|
369
|
+
const result = cancelOnUI(animationId, behavior);
|
|
370
|
+
scheduleOnRN(
|
|
371
|
+
resolveReactRequest,
|
|
372
|
+
driverId,
|
|
373
|
+
requestId,
|
|
374
|
+
result,
|
|
375
|
+
true
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
return promise;
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
setDriverState(
|
|
383
|
+
driver,
|
|
384
|
+
createDriverState(
|
|
385
|
+
driverId,
|
|
386
|
+
initialPresentation,
|
|
387
|
+
presentation,
|
|
388
|
+
callbackRef,
|
|
389
|
+
activeAnimationId
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
driverRef.current = driver;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const driver = driverRef.current;
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
const currentDriver = driverRef.current;
|
|
398
|
+
if (!currentDriver) return undefined;
|
|
399
|
+
const state = getDriverState(currentDriver);
|
|
400
|
+
// Re-attach on effect replays (StrictMode/<Activity>) after a cleanup
|
|
401
|
+
// detached this state.
|
|
402
|
+
attachDriverState(state);
|
|
403
|
+
return () => {
|
|
404
|
+
cancelReanimatedAnimation(animationProgress);
|
|
405
|
+
rejectDriverRequests(state.driverId);
|
|
406
|
+
detachDriverState(state);
|
|
407
|
+
};
|
|
408
|
+
}, [animationProgress]);
|
|
409
|
+
return driver;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export type {
|
|
413
|
+
KeyframedClipAnimation,
|
|
414
|
+
SpringClipAnimation,
|
|
415
|
+
TimingClipAnimation,
|
|
416
|
+
};
|
package/src/geometry.ts
CHANGED
|
@@ -11,6 +11,21 @@ export type ClipBounds = Readonly<{
|
|
|
11
11
|
height: number;
|
|
12
12
|
}>;
|
|
13
13
|
|
|
14
|
+
export type SmoothClipPresentation = Readonly<{
|
|
15
|
+
clip: ClipGeometry;
|
|
16
|
+
contentTranslateX: number;
|
|
17
|
+
contentTranslateY: number;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export function createClipPresentation(
|
|
21
|
+
clip: ClipGeometry,
|
|
22
|
+
contentTranslateX = 0,
|
|
23
|
+
contentTranslateY = 0
|
|
24
|
+
): SmoothClipPresentation {
|
|
25
|
+
'worklet';
|
|
26
|
+
return { clip, contentTranslateX, contentTranslateY };
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export function isFiniteClipGeometry(geometry: ClipGeometry): boolean {
|
|
15
30
|
'worklet';
|
|
16
31
|
return (
|
|
@@ -37,6 +52,30 @@ export function clipGeometryEquals(
|
|
|
37
52
|
);
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
export function isFiniteClipPresentation(
|
|
56
|
+
presentation: SmoothClipPresentation
|
|
57
|
+
): boolean {
|
|
58
|
+
'worklet';
|
|
59
|
+
return (
|
|
60
|
+
isFiniteClipGeometry(presentation.clip) &&
|
|
61
|
+
Number.isFinite(presentation.contentTranslateX) &&
|
|
62
|
+
Number.isFinite(presentation.contentTranslateY)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clipPresentationEquals(
|
|
67
|
+
first: SmoothClipPresentation | null,
|
|
68
|
+
second: SmoothClipPresentation
|
|
69
|
+
): boolean {
|
|
70
|
+
'worklet';
|
|
71
|
+
return (
|
|
72
|
+
first !== null &&
|
|
73
|
+
clipGeometryEquals(first.clip, second.clip) &&
|
|
74
|
+
first.contentTranslateX === second.contentTranslateX &&
|
|
75
|
+
first.contentTranslateY === second.contentTranslateY
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
/**
|
|
41
80
|
* Mirrors the native geometry contract for tests and non-native fallbacks.
|
|
42
81
|
* Native remains authoritative because its bounds are the actual rendered size.
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
export { SmoothClipView } from './SmoothClipView';
|
|
2
2
|
export type { SmoothClipViewProps } from './SmoothClipView';
|
|
3
|
+
export { useSmoothClipDriver } from './drivers';
|
|
4
|
+
export type {
|
|
5
|
+
ClipAnimationResult,
|
|
6
|
+
ClipReduceMotion,
|
|
7
|
+
KeyframedClipAnimation,
|
|
8
|
+
SmoothClipDriver,
|
|
9
|
+
SmoothClipDriverOptions,
|
|
10
|
+
SmoothClipReactControls,
|
|
11
|
+
SmoothClipUIControls,
|
|
12
|
+
SmoothClipAnimation,
|
|
13
|
+
SpringClipAnimation,
|
|
14
|
+
TimingClipAnimation,
|
|
15
|
+
} from './driverTypes';
|
|
3
16
|
export { normalizeClipGeometry } from './geometry';
|
|
4
|
-
export
|
|
17
|
+
export { createClipPresentation } from './geometry';
|
|
18
|
+
export type {
|
|
19
|
+
ClipBounds,
|
|
20
|
+
ClipGeometry,
|
|
21
|
+
SmoothClipPresentation,
|
|
22
|
+
} from './geometry';
|
package/src/index.web.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
export { SmoothClipView } from './SmoothClipView.web';
|
|
2
2
|
export type { SmoothClipViewProps } from './SmoothClipView.web';
|
|
3
|
+
export { useSmoothClipDriver } from './drivers';
|
|
4
|
+
export type {
|
|
5
|
+
ClipAnimationResult,
|
|
6
|
+
ClipReduceMotion,
|
|
7
|
+
KeyframedClipAnimation,
|
|
8
|
+
SmoothClipDriver,
|
|
9
|
+
SmoothClipDriverOptions,
|
|
10
|
+
SmoothClipReactControls,
|
|
11
|
+
SmoothClipUIControls,
|
|
12
|
+
SmoothClipAnimation,
|
|
13
|
+
SpringClipAnimation,
|
|
14
|
+
TimingClipAnimation,
|
|
15
|
+
} from './driverTypes';
|
|
3
16
|
export { normalizeClipGeometry } from './geometry';
|
|
4
|
-
export
|
|
17
|
+
export { createClipPresentation } from './geometry';
|
|
18
|
+
export type {
|
|
19
|
+
ClipBounds,
|
|
20
|
+
ClipGeometry,
|
|
21
|
+
SmoothClipPresentation,
|
|
22
|
+
} from './geometry';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { scheduleOnUI } from 'react-native-worklets';
|
|
2
|
+
import { deliverDriverCompletion } from './driverState';
|
|
3
|
+
|
|
4
|
+
export function completeNativeAnimation(
|
|
5
|
+
driverId: number,
|
|
6
|
+
animationId: number,
|
|
7
|
+
finished: boolean
|
|
8
|
+
): void {
|
|
9
|
+
deliverDriverCompletion(driverId, animationId, finished);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function synchronizeNativeCompletion(
|
|
13
|
+
activeAnimationId: { value: number },
|
|
14
|
+
ownership: { value: number },
|
|
15
|
+
animationId: number,
|
|
16
|
+
_finished: boolean
|
|
17
|
+
): void {
|
|
18
|
+
// Unfinished completions must also release ownership: native emits
|
|
19
|
+
// finished:false when the last participating view unmounts or the system
|
|
20
|
+
// strips the animation. Superseded ids are already filtered by the
|
|
21
|
+
// active-id comparison below, so no finished-based gate is needed.
|
|
22
|
+
scheduleOnUI(
|
|
23
|
+
(
|
|
24
|
+
active: { value: number },
|
|
25
|
+
owner: { value: number },
|
|
26
|
+
completedId: number
|
|
27
|
+
) => {
|
|
28
|
+
'worklet';
|
|
29
|
+
if (active.value !== completedId) return;
|
|
30
|
+
active.value = 0;
|
|
31
|
+
owner.value = 0;
|
|
32
|
+
},
|
|
33
|
+
activeAnimationId,
|
|
34
|
+
ownership,
|
|
35
|
+
animationId
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deferDriverCompletions,
|
|
3
|
+
releaseDriverCompletions,
|
|
4
|
+
} from './driverState';
|
|
5
|
+
|
|
6
|
+
type PendingRequest = {
|
|
7
|
+
resolve(value: unknown): void;
|
|
8
|
+
reject(error: Error): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const requests = new Map<number, PendingRequest>();
|
|
12
|
+
const requestsByDriver = new Map<number, Set<number>>();
|
|
13
|
+
let nextRequestId = 0;
|
|
14
|
+
|
|
15
|
+
export function createReactRequest<T>(
|
|
16
|
+
driverId: number,
|
|
17
|
+
deferCompletions = false
|
|
18
|
+
): { requestId: number; promise: Promise<T> } {
|
|
19
|
+
nextRequestId = (nextRequestId % 0x7ffffffe) + 1;
|
|
20
|
+
const requestId = nextRequestId;
|
|
21
|
+
let resolveRequest!: (value: T) => void;
|
|
22
|
+
let rejectRequest!: (error: Error) => void;
|
|
23
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
24
|
+
resolveRequest = resolve;
|
|
25
|
+
rejectRequest = reject;
|
|
26
|
+
});
|
|
27
|
+
requests.set(requestId, {
|
|
28
|
+
resolve: resolveRequest as (value: unknown) => void,
|
|
29
|
+
reject: rejectRequest,
|
|
30
|
+
});
|
|
31
|
+
const driverRequests = requestsByDriver.get(driverId) ?? new Set<number>();
|
|
32
|
+
driverRequests.add(requestId);
|
|
33
|
+
requestsByDriver.set(driverId, driverRequests);
|
|
34
|
+
if (deferCompletions) deferDriverCompletions(driverId);
|
|
35
|
+
return { requestId, promise };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveReactRequest(
|
|
39
|
+
driverId: number,
|
|
40
|
+
requestId: number,
|
|
41
|
+
value: unknown,
|
|
42
|
+
releaseCompletions = false
|
|
43
|
+
): void {
|
|
44
|
+
const request = requests.get(requestId);
|
|
45
|
+
requests.delete(requestId);
|
|
46
|
+
const driverRequests = requestsByDriver.get(driverId);
|
|
47
|
+
driverRequests?.delete(requestId);
|
|
48
|
+
if (driverRequests?.size === 0) requestsByDriver.delete(driverId);
|
|
49
|
+
request?.resolve(value);
|
|
50
|
+
if (releaseCompletions) {
|
|
51
|
+
queueMicrotask(() => releaseDriverCompletions(driverId));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function rejectDriverRequests(driverId: number): void {
|
|
56
|
+
const driverRequests = requestsByDriver.get(driverId);
|
|
57
|
+
if (!driverRequests) return;
|
|
58
|
+
for (const requestId of driverRequests) {
|
|
59
|
+
const request = requests.get(requestId);
|
|
60
|
+
requests.delete(requestId);
|
|
61
|
+
request?.reject(new Error('[SmoothClipView] Driver was destroyed.'));
|
|
62
|
+
}
|
|
63
|
+
requestsByDriver.delete(driverId);
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
2
|
+
import type { Spec } from './NativeSmoothClipModule';
|
|
3
|
+
|
|
4
|
+
// A Java/Kotlin TurboModule host function cannot be invoked synchronously from
|
|
5
|
+
// the Worklets UI runtime. The Android module therefore installs worklet-callable
|
|
6
|
+
// C++ host functions via TurboModuleWithJSIBindings. Resolving the module runs
|
|
7
|
+
// that installer synchronously (see TurboModuleManager), after which the driver
|
|
8
|
+
// uses the installed `global.__SmoothClipView` on both the UI and JS runtimes.
|
|
9
|
+
TurboModuleRegistry.getEnforcing<Spec>('NativeSmoothClipModule');
|
|
10
|
+
|
|
11
|
+
const bindings = (global as unknown as { __SmoothClipView?: Spec })
|
|
12
|
+
.__SmoothClipView;
|
|
13
|
+
|
|
14
|
+
if (bindings == null) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'[SmoothClipView] Native JSI bindings are not installed. Rebuild the Android app after updating this package.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default bindings;
|