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/README.md
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-native-smooth-clip-view)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## High-performance geometry animations for React Native
|
|
6
6
|
|
|
7
|
-
`
|
|
8
|
-
updates only the native clipping layer. It is
|
|
9
|
-
useful for expanding cards, sheets, maps, media, zoom transitions, and other reveals where
|
|
10
|
-
animating layout width and height cause lag because of yoga/fabric calculating layout every frame.
|
|
7
|
+
`react-native-smooth-clip-view` lets you animate `x`, `y`, `width`, `height`, and `borderRadius` with Reanimated without triggering expensive layout work on every frame.
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
Instead of resizing the Yoga layout, `SmoothClipView` keeps a fixed footprint and updates only the native clipping layer. This makes geometry-heavy animations smooth and inexpensive—even for shared-element transitions, zoom transitions, expanding cards, reveals, sheets, maps, and media.
|
|
10
|
+
|
|
11
|
+
Use it for transitions that previously struggled with performance when animating layout dimensions directly.
|
|
13
12
|
|
|
14
13
|
## Requirements
|
|
15
14
|
|
|
@@ -166,6 +165,15 @@ const gesture = Gesture.Pan()
|
|
|
166
165
|
| `driver` | `SmoothClipDriver` | Reusable hybrid clip driver. |
|
|
167
166
|
| `children` | `ReactNode` | Content rendered inside the fixed host. |
|
|
168
167
|
|
|
168
|
+
The host hides itself, drops out of the accessibility tree, and stops accepting
|
|
169
|
+
touches while the clip is empty. The two platforms draw the boundary where they
|
|
170
|
+
actually stop rendering, which differs below one pixel: Android emits an integer
|
|
171
|
+
`Outline`, which collapses to nothing under half a physical pixel, so an extent
|
|
172
|
+
in `(0, 0.5)` px counts as empty there; iOS masks in floats and treats only a
|
|
173
|
+
zero-or-negative extent as empty. A clip animating through that band therefore
|
|
174
|
+
turns non-interactive one frame earlier on Android — matching what each platform
|
|
175
|
+
puts on screen, which is the property worth keeping identical.
|
|
176
|
+
|
|
169
177
|
### Driver
|
|
170
178
|
|
|
171
179
|
- `useSmoothClipDriver(initialPresentation, options)` returns one hybrid driver
|
|
@@ -191,11 +199,19 @@ const gesture = Gesture.Pan()
|
|
|
191
199
|
remaining distance in one second). Every geometry channel continues with the
|
|
192
200
|
same normalized rate, so grab/release preserves the felt direction and
|
|
193
201
|
speed. `'inherit'` (the default) estimates the scalar from the last two
|
|
194
|
-
interactive samples on iOS and Android;
|
|
195
|
-
|
|
202
|
+
interactive samples on iOS and Android; the web fallback always inherits
|
|
203
|
+
zero. How long the finger has been still since that last sample scales the
|
|
204
|
+
result: full credit for one frame (16.7 ms), then a linear decay to zero at
|
|
205
|
+
100 ms. A release straight out of a drag is therefore untouched, and holding
|
|
206
|
+
still before releasing bleeds the momentum off smoothly instead of keeping
|
|
207
|
+
all of it until 99 ms and none at 101 ms. Two writes landing inside the same frame
|
|
196
208
|
(< 4 ms apart, e.g. a release-sample `from` seed right after the last drag
|
|
197
209
|
write) coalesce into one sample, and an identical re-write is ignored, so a
|
|
198
210
|
fused handoff can neither zero nor inflate the inherited velocity.
|
|
211
|
+
`beginInteraction()` records the frozen presentation as a plain sample on
|
|
212
|
+
both platforms, so a grab-and-instant-refling inherits bounded recent
|
|
213
|
+
motion (the staleness guard still zeroes a stale pair) instead of
|
|
214
|
+
launching dead.
|
|
199
215
|
- `driver.react` exposes `beginInteraction`, `set`, `animateTo`, and `cancel`
|
|
200
216
|
as Promises (`setScalars` is UI-worklet-only). React code never blocks
|
|
201
217
|
waiting for main/UI-thread work. An immediate animation request resolves
|
|
@@ -206,37 +222,58 @@ const gesture = Gesture.Pan()
|
|
|
206
222
|
animation can run the identical curve without hand-deriving it; springs
|
|
207
223
|
accept mass, stiffness, damping, and an
|
|
208
224
|
explicit normalized velocity or `'inherit'` (the default). Keyframes accept
|
|
209
|
-
validated, monotonically increasing offsets from zero through one
|
|
225
|
+
validated, monotonically increasing offsets from zero through one; there is
|
|
226
|
+
deliberately no keyframe easing field — playback is linear between offsets
|
|
227
|
+
and the frames encode the curve, which also expresses per-channel-nonlinear
|
|
228
|
+
paths that no single time-warp could reproduce. Every
|
|
210
229
|
kind accepts an optional `from` presentation — a fused take-ownership hot
|
|
211
230
|
write issued immediately before the handoff, so the animation starts from
|
|
212
231
|
exactly that value (pass `frames[0].presentation` for keyframes, which
|
|
213
232
|
interpolate absolutely). A non-finite `from` rejects the whole call; against
|
|
214
|
-
a held pending-animation latch
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
a held pending-animation latch, explicit `from` is the newer intent: it
|
|
234
|
+
cancels that latch once with `finished: false`, records/applies `from`, then
|
|
235
|
+
starts the replacement from that native value. Passive hook seeds and public
|
|
236
|
+
`set`/`setScalars` writes still leave a held latch intact. `from` behaves the
|
|
237
|
+
same on both platforms (it is driver-layer, not native): on iOS the seed
|
|
238
|
+
stops any running Core Animation and writes the model layer first; keyframes
|
|
239
|
+
then start exactly at `from`, while timing/spring sample their from-value off
|
|
240
|
+
the presentation layer — the last committed frame, at most one frame behind
|
|
241
|
+
`from` — identical to the explicit two-call pattern.
|
|
221
242
|
- `cancel()` freezes visible presentation by default. Pass `'target'` as its
|
|
222
243
|
behavior to jump to the requested endpoint.
|
|
223
244
|
- `options.onAnimationComplete` fires exactly once per animation with its ID
|
|
224
245
|
and `finished` state, including cancellation, replacement, and native-side
|
|
225
246
|
rejection (`animateTo` then returns a fresh non-zero id whose single
|
|
226
247
|
`finished: false` completion follows — key completion handling by the
|
|
227
|
-
returned id, never by `0`).
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
248
|
+
returned id, never by `0`). A valid pre-registration request carries its
|
|
249
|
+
authoritative interactive start, creates the missing driver state and
|
|
250
|
+
returns a real id. `0` — with no completion — is reserved for off-main,
|
|
251
|
+
invalid-id, or otherwise unsupported dispatch: a missing-state native request
|
|
252
|
+
with no authoritative start, and any `driver.ui.animateTo` issued after the
|
|
253
|
+
driver's hook has unmounted. (The second case is decided on the UI runtime,
|
|
254
|
+
because a destroyed driver and a not-yet-seeded one are the same missing
|
|
255
|
+
registry entry to native — accepting it would build a latch nothing can start
|
|
256
|
+
and nothing can cancel.) Removing the last displayable
|
|
257
|
+
host mid-flight does not complete the animation: the remainder is re-latched
|
|
258
|
+
even if detached or unsized peers remain registered, and the next displayable
|
|
259
|
+
host resumes it, so the completion arrives when it finishes there — or unfinished on
|
|
232
260
|
replacement, cancellation, `beginInteraction()`, or driver destruction.
|
|
261
|
+
With multiple hosts on one driver, unregistering any host that installed the
|
|
262
|
+
running animation makes the eventual completion `finished: false` — a
|
|
263
|
+
detached or un-laid-out host does not become a completion participant until
|
|
264
|
+
its native animation is actually installed. `finished: true` means every
|
|
265
|
+
installed participant ran the animation to its end.
|
|
233
266
|
- An `animateTo` issued before any host view can produce a visible frame (for
|
|
234
267
|
example from an effect in the same commit that mounts the host, or inside a
|
|
235
268
|
modal route whose subtree attaches to its window late) is held pending and
|
|
236
|
-
starts with its full duration at the first
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
269
|
+
starts with its full duration at the first moment a registered host can
|
|
270
|
+
produce a frame — registration, first layout, or window attach, whichever
|
|
271
|
+
comes last. This also covers an animation worklet that runs before the
|
|
272
|
+
hook's seed worklet: the animation creates the state and the later passive
|
|
273
|
+
seed cannot reset its ownership or active id. A pending animation owns the
|
|
274
|
+
driver: ordinary take-ownership writes (`set`, `setScalars`, the hook's seed)
|
|
275
|
+
are dropped while it is held. Replace it with another `animateTo`, override
|
|
276
|
+
it with an explicit `animation.from`, or cancel it via `beginInteraction()`
|
|
240
277
|
or `cancel()`. If no view ever becomes displayable, it survives until it is
|
|
241
278
|
replaced, cancelled, or the driver is destroyed — at which point its single
|
|
242
279
|
`finished: false` completion is delivered.
|
package/android/CMakeLists.txt
CHANGED
|
@@ -53,6 +53,11 @@ void viewBecameDisplayableAndroid(
|
|
|
53
53
|
uint64_t driverId,
|
|
54
54
|
facebook::jni::alias_ref<JSmoothClipView> view);
|
|
55
55
|
|
|
56
|
+
// Advances the registry frame loop. Called from Kotlin (SmoothClipBindings)
|
|
57
|
+
// inside Choreographer#doFrame with the frame's vsync timestamp converted to
|
|
58
|
+
// seconds; never lets a failure unwind back into doFrame.
|
|
59
|
+
void onFrameAndroid(double frameTimeS);
|
|
60
|
+
|
|
56
61
|
// Installs `global.__SmoothClipView` (worklet-callable host functions) into the
|
|
57
62
|
// JS runtime and wires native animation completion delivery through the
|
|
58
63
|
// CallInvoker. Invoked by the TurboModule BindingsInstaller.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <jsi/jsi.h>
|
|
7
7
|
|
|
8
8
|
#include <cmath>
|
|
9
|
+
#include <limits>
|
|
9
10
|
#include <memory>
|
|
10
11
|
#include <unordered_map>
|
|
11
12
|
#include <utility>
|
|
@@ -45,6 +46,18 @@ bool boolArg(const Value *args, size_t count, size_t index) {
|
|
|
45
46
|
return index < count && args[index].isBool() && args[index].getBool();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// Optional trailing start stamp (Reanimated-rule milliseconds captured in the
|
|
50
|
+
// issuing worklet), converted to the registry's CLOCK_MONOTONIC seconds. A
|
|
51
|
+
// missing or non-numeric argument — and the JS side's deliberate NaN fallback
|
|
52
|
+
// when the worklet globals are absent — resolves to NaN, which
|
|
53
|
+
// resolveStartStamp() treats as "no hint": nowSeconds() plus the min() anchor,
|
|
54
|
+
// exactly the pre-hint behavior.
|
|
55
|
+
double startStampArg(const Value *args, size_t count, size_t index) {
|
|
56
|
+
return index < count && args[index].isNumber()
|
|
57
|
+
? args[index].asNumber() / 1000.0
|
|
58
|
+
: std::numeric_limits<double>::quiet_NaN();
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
Presentation presentationFromArgs(const Value *args, size_t offset) {
|
|
49
62
|
return Presentation{
|
|
50
63
|
{args[offset].asNumber(),
|
|
@@ -153,15 +166,17 @@ void installBindings(
|
|
|
153
166
|
Object bindings(runtime);
|
|
154
167
|
|
|
155
168
|
setHostFunction(
|
|
156
|
-
runtime, bindings, "setClipPresentation",
|
|
169
|
+
runtime, bindings, "setClipPresentation", 10,
|
|
157
170
|
[](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
|
|
158
171
|
if (count < 9) return Value::undefined();
|
|
159
172
|
const double driverId = args[0].asNumber();
|
|
160
173
|
const Presentation presentation = presentationFromArgs(args, 1);
|
|
161
174
|
const bool takeOwnership = boolArg(args, count, 8);
|
|
175
|
+
const bool overridePendingAnimation = boolArg(args, count, 9);
|
|
162
176
|
if (validDriverId(driverId) && finitePresentation(presentation)) {
|
|
163
177
|
setPresentation(
|
|
164
|
-
static_cast<uint64_t>(driverId), presentation, takeOwnership
|
|
178
|
+
static_cast<uint64_t>(driverId), presentation, takeOwnership,
|
|
179
|
+
overridePendingAnimation);
|
|
165
180
|
}
|
|
166
181
|
return Value::undefined();
|
|
167
182
|
});
|
|
@@ -177,7 +192,7 @@ void installBindings(
|
|
|
177
192
|
});
|
|
178
193
|
|
|
179
194
|
setHostFunction(
|
|
180
|
-
runtime, bindings, "animateTiming",
|
|
195
|
+
runtime, bindings, "animateTiming", 23,
|
|
181
196
|
[](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
|
|
182
197
|
if (count < 22) return Value(0);
|
|
183
198
|
const double driverId = args[0].asNumber();
|
|
@@ -194,13 +209,13 @@ void installBindings(
|
|
|
194
209
|
}
|
|
195
210
|
return Value(animateTiming(
|
|
196
211
|
static_cast<uint64_t>(driverId),
|
|
197
|
-
{boolArg(args, count, 1), start},
|
|
212
|
+
{boolArg(args, count, 1), start, startStampArg(args, count, 22)},
|
|
198
213
|
target,
|
|
199
214
|
animation));
|
|
200
215
|
});
|
|
201
216
|
|
|
202
217
|
setHostFunction(
|
|
203
|
-
runtime, bindings, "animateSpring",
|
|
218
|
+
runtime, bindings, "animateSpring", 23,
|
|
204
219
|
[](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
|
|
205
220
|
if (count < 22) return Value(0);
|
|
206
221
|
const double driverId = args[0].asNumber();
|
|
@@ -221,13 +236,13 @@ void installBindings(
|
|
|
221
236
|
}
|
|
222
237
|
return Value(animateSpring(
|
|
223
238
|
static_cast<uint64_t>(driverId),
|
|
224
|
-
{boolArg(args, count, 1), start},
|
|
239
|
+
{boolArg(args, count, 1), start, startStampArg(args, count, 22)},
|
|
225
240
|
target,
|
|
226
241
|
animation));
|
|
227
242
|
});
|
|
228
243
|
|
|
229
244
|
setHostFunction(
|
|
230
|
-
runtime, bindings, "animateKeyframes",
|
|
245
|
+
runtime, bindings, "animateKeyframes", 20,
|
|
231
246
|
[](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
|
|
232
247
|
if (count < 19 || !args[17].isObject() ||
|
|
233
248
|
!args[17].getObject(rt).isArray(rt)) {
|
|
@@ -270,7 +285,7 @@ void installBindings(
|
|
|
270
285
|
}
|
|
271
286
|
return Value(animateKeyframes(
|
|
272
287
|
static_cast<uint64_t>(driverId),
|
|
273
|
-
{boolArg(args, count, 1), start},
|
|
288
|
+
{boolArg(args, count, 1), start, startStampArg(args, count, 19)},
|
|
274
289
|
target,
|
|
275
290
|
durationMs,
|
|
276
291
|
std::move(keyframes),
|
|
@@ -419,6 +434,10 @@ void nativeInvalidate(jni::alias_ref<jni::JObject>) {
|
|
|
419
434
|
smoothclip::invalidateBindings();
|
|
420
435
|
}
|
|
421
436
|
|
|
437
|
+
void nativeOnFrame(jni::alias_ref<jni::JObject>, jlong frameTimeNanos) {
|
|
438
|
+
smoothclip::onFrameAndroid(static_cast<double>(frameTimeNanos) / 1e9);
|
|
439
|
+
}
|
|
440
|
+
|
|
422
441
|
} // namespace
|
|
423
442
|
|
|
424
443
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
@@ -434,6 +453,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
|
434
453
|
makeNativeMethod(
|
|
435
454
|
"nativeViewBecameDisplayable", nativeViewBecameDisplayable),
|
|
436
455
|
makeNativeMethod("nativeInvalidate", nativeInvalidate),
|
|
456
|
+
makeNativeMethod("nativeOnFrame", nativeOnFrame),
|
|
437
457
|
});
|
|
438
458
|
});
|
|
439
459
|
}
|