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
|
@@ -19,17 +19,22 @@
|
|
|
19
19
|
#include <utility>
|
|
20
20
|
#include <vector>
|
|
21
21
|
|
|
22
|
+
#include "SmoothClipAnimationId.h"
|
|
23
|
+
#include "SmoothClipAnimationCurve.h"
|
|
24
|
+
|
|
22
25
|
@interface SmoothClipView (Registry)
|
|
23
26
|
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation;
|
|
27
|
+
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation
|
|
28
|
+
recordVelocitySample:(BOOL)recordVelocitySample;
|
|
24
29
|
- (smoothclip::Presentation)smoothClipFreezePresentation;
|
|
25
30
|
- (smoothclip::Presentation)smoothClipCurrentPresentation;
|
|
26
|
-
- (
|
|
31
|
+
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
27
32
|
animation:(smoothclip::TimingAnimation)animation
|
|
28
33
|
animationId:(int32_t)animationId;
|
|
29
|
-
- (
|
|
34
|
+
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
30
35
|
animation:(smoothclip::SpringAnimation)animation
|
|
31
36
|
animationId:(int32_t)animationId;
|
|
32
|
-
- (
|
|
37
|
+
- (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
|
|
33
38
|
keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
|
|
34
39
|
durationMs:(double)durationMs
|
|
35
40
|
animationId:(int32_t)animationId;
|
|
@@ -37,6 +42,7 @@
|
|
|
37
42
|
- (BOOL)smoothClipIsJoinable;
|
|
38
43
|
- (BOOL)smoothClipCanDisplay;
|
|
39
44
|
- (BOOL)smoothClipHasPendingInstall;
|
|
45
|
+
- (double)smoothClipSpringContinuationVelocity;
|
|
40
46
|
@end
|
|
41
47
|
|
|
42
48
|
namespace smoothclip {
|
|
@@ -60,9 +66,13 @@ struct ActiveAnimation {
|
|
|
60
66
|
std::vector<Keyframe> keyframes;
|
|
61
67
|
double durationMs = 0;
|
|
62
68
|
CFTimeInterval startedAt = 0;
|
|
63
|
-
// False while the animation is latched: built before any host
|
|
64
|
-
//
|
|
65
|
-
//
|
|
69
|
+
// False while the animation is latched. Two ways in: built before any host
|
|
70
|
+
// view could display (pre-registration, or a host in a detached subtree), or
|
|
71
|
+
// re-latched by unregisterView when the last DISPLAYABLE host left
|
|
72
|
+
// mid-flight. Started by whichever of registerView, viewBecameDisplayable
|
|
73
|
+
// (window attach or first layout) or joinActiveAnimation first finds a host
|
|
74
|
+
// that can produce a frame; each rebases startedAt through
|
|
75
|
+
// startLatchedAnimation. startedAt is only meaningful once started.
|
|
66
76
|
bool started = false;
|
|
67
77
|
};
|
|
68
78
|
|
|
@@ -72,7 +82,6 @@ struct DriverState {
|
|
|
72
82
|
Ownership ownership = Ownership::Interactive;
|
|
73
83
|
std::vector<ViewKey> views;
|
|
74
84
|
std::optional<ActiveAnimation> animation;
|
|
75
|
-
int32_t nextAnimationId = 0;
|
|
76
85
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
77
86
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
78
87
|
// last view leaves and revived by a take-ownership setPresentation.
|
|
@@ -167,14 +176,6 @@ void emitStandaloneCompletion(
|
|
|
167
176
|
#endif
|
|
168
177
|
}
|
|
169
178
|
|
|
170
|
-
int32_t allocateAnimationId(DriverState &state) {
|
|
171
|
-
state.nextAnimationId =
|
|
172
|
-
state.nextAnimationId == std::numeric_limits<int32_t>::max()
|
|
173
|
-
? 1
|
|
174
|
-
: state.nextAnimationId + 1;
|
|
175
|
-
return state.nextAnimationId;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
179
|
Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
179
180
|
// A latched animation never rendered, so state.latest already holds its
|
|
180
181
|
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
@@ -203,12 +204,33 @@ void cancelActive(
|
|
|
203
204
|
bool useTarget) {
|
|
204
205
|
if (!state.animation.has_value()) return;
|
|
205
206
|
const int32_t animationId = state.animation->animationId;
|
|
206
|
-
|
|
207
|
+
// Deferred views are deliberately not completion participants, but they do
|
|
208
|
+
// hold a pending install and requested target. Cancellation must still reach
|
|
209
|
+
// every registered view so a later attach cannot resurrect the old id.
|
|
210
|
+
for (const ViewKey key : state.views) {
|
|
207
211
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
|
|
208
212
|
}
|
|
209
213
|
emitCompletion(driverId, state, animationId, false);
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
217
|
+
if (!state.animation.has_value() || !state.animation->started ||
|
|
218
|
+
!state.animation->participants.empty()) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const int32_t animationId = state.animation->animationId;
|
|
222
|
+
const bool finished = state.animation->finished;
|
|
223
|
+
// A detached/unlaid-out view can have a requested animation without any CA
|
|
224
|
+
// delegate capable of completing it. Once every installed animation ends,
|
|
225
|
+
// make those deferred views statically correct and invalidate their pending
|
|
226
|
+
// ids before releasing the driver-level completion.
|
|
227
|
+
for (const ViewKey key : state.views) {
|
|
228
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
229
|
+
}
|
|
230
|
+
state.ownership = Ownership::Interactive;
|
|
231
|
+
emitCompletion(driverId, state, animationId, finished);
|
|
232
|
+
}
|
|
233
|
+
|
|
212
234
|
void applyPresentation(DriverState &state, Presentation presentation) {
|
|
213
235
|
state.latest = presentation;
|
|
214
236
|
state.hasLatest = true;
|
|
@@ -258,21 +280,33 @@ void dispatchActiveAnimationJoin(
|
|
|
258
280
|
DriverState &state,
|
|
259
281
|
SmoothClipView *view,
|
|
260
282
|
ViewKey key,
|
|
261
|
-
const Presentation &visible
|
|
283
|
+
const Presentation &visible,
|
|
284
|
+
SmoothClipView *reference = nil) {
|
|
262
285
|
auto &active = *state.animation;
|
|
263
|
-
active.participants.insert(key);
|
|
264
286
|
const double elapsedMs = (CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
265
287
|
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
288
|
+
BOOL installed = NO;
|
|
266
289
|
if (active.kind == AnimationKind::Timing) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
const double progress = active.durationMs <= 0
|
|
291
|
+
? 1
|
|
292
|
+
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
293
|
+
TimingAnimation timing = timingRemainder(active.timing, progress).animation;
|
|
294
|
+
installed = [view smoothClipAnimateTiming:state.latest
|
|
295
|
+
animation:timing
|
|
296
|
+
animationId:active.animationId];
|
|
272
297
|
} else if (active.kind == AnimationKind::Spring) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
298
|
+
SpringAnimation spring = active.spring;
|
|
299
|
+
if (reference != nil) {
|
|
300
|
+
// A late host must join the spring's CURRENT physical state. Reusing the
|
|
301
|
+
// original launch velocity (or resolving `inherit` from the joining
|
|
302
|
+
// host's unrelated samples) creates a different spring at the seam.
|
|
303
|
+
spring.inheritVelocity = false;
|
|
304
|
+
spring.initialVelocity =
|
|
305
|
+
[reference smoothClipSpringContinuationVelocity];
|
|
306
|
+
}
|
|
307
|
+
installed = [view smoothClipAnimateSpring:state.latest
|
|
308
|
+
animation:spring
|
|
309
|
+
animationId:active.animationId];
|
|
276
310
|
} else {
|
|
277
311
|
const double progress = active.durationMs <= 0
|
|
278
312
|
? 1
|
|
@@ -285,11 +319,12 @@ void dispatchActiveAnimationJoin(
|
|
|
285
319
|
remaining.push_back({offset, frame.presentation});
|
|
286
320
|
}
|
|
287
321
|
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
288
|
-
[view smoothClipAnimateKeyframes:state.latest
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
322
|
+
installed = [view smoothClipAnimateKeyframes:state.latest
|
|
323
|
+
keyframes:remaining
|
|
324
|
+
durationMs:remainingMs
|
|
325
|
+
animationId:active.animationId];
|
|
292
326
|
}
|
|
327
|
+
if (installed) active.participants.insert(key);
|
|
293
328
|
}
|
|
294
329
|
|
|
295
330
|
// An animation may only start once a registered view can actually produce a
|
|
@@ -311,6 +346,9 @@ bool anyDisplayableView(const DriverState &state) {
|
|
|
311
346
|
void startLatchedAnimation(DriverState &state) {
|
|
312
347
|
state.animation->startedAt = CACurrentMediaTime();
|
|
313
348
|
state.animation->started = true;
|
|
349
|
+
// Participants describe live CA delegates, not registered views. Anything
|
|
350
|
+
// left from the frozen run can only produce a stale callback for that run.
|
|
351
|
+
state.animation->participants.clear();
|
|
314
352
|
const Presentation start = state.animation->start;
|
|
315
353
|
for (const ViewKey key : state.views) {
|
|
316
354
|
SmoothClipView *participant = viewForKey(key);
|
|
@@ -362,12 +400,12 @@ void registerView(
|
|
|
362
400
|
const bool shouldReplay =
|
|
363
401
|
hasAnimation && state.animation->started && !state.views.empty();
|
|
364
402
|
Presentation visible = state.latest;
|
|
403
|
+
SmoothClipView *reference = nil;
|
|
365
404
|
if (shouldReplay) {
|
|
366
405
|
// A registered peer that never laid out reports zero geometry; joining
|
|
367
406
|
// from it would visibly collapse the clip. Prefer the first laid-out
|
|
368
407
|
// peer and fall back to the animation's start.
|
|
369
|
-
|
|
370
|
-
for (const ViewKey candidate : state.views) {
|
|
408
|
+
for (const ViewKey candidate : state.animation->participants) {
|
|
371
409
|
SmoothClipView *candidateView = viewForKey(candidate);
|
|
372
410
|
if ([candidateView smoothClipIsJoinable]) {
|
|
373
411
|
reference = candidateView;
|
|
@@ -389,14 +427,13 @@ void registerView(
|
|
|
389
427
|
}
|
|
390
428
|
[view smoothClipApplyPresentation:visible];
|
|
391
429
|
if (startsLatch) {
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// orphan-safe for a mount-then-unmount before first layout).
|
|
430
|
+
// Rebase the join clock and dispatch to every registered view. Only views
|
|
431
|
+
// that actually install their CA groups become completion participants.
|
|
395
432
|
startLatchedAnimation(state);
|
|
396
433
|
return;
|
|
397
434
|
}
|
|
398
435
|
if (!shouldReplay) return;
|
|
399
|
-
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
436
|
+
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
400
437
|
}
|
|
401
438
|
|
|
402
439
|
void viewBecameDisplayable(uint64_t driverId, SmoothClipView *view) {
|
|
@@ -433,33 +470,41 @@ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
|
433
470
|
if (!state.animation.has_value() || state.views.empty()) {
|
|
434
471
|
return false;
|
|
435
472
|
}
|
|
436
|
-
// Unreachable in the normal flow (a pending install implies the animation
|
|
437
|
-
// was dispatched, which implies started), but a join must never compute
|
|
438
|
-
// elapsed time against an un-rebased latch clock.
|
|
439
|
-
if (!state.animation->started) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
473
|
const ViewKey key = keyForView(view);
|
|
443
474
|
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
444
475
|
state.views.end()) {
|
|
445
476
|
return false;
|
|
446
477
|
}
|
|
478
|
+
// A pending install used to imply the animation was dispatched, which
|
|
479
|
+
// implied started. Re-latching on displayability broke that: when the last
|
|
480
|
+
// displayable host leaves, a peer that deferred its install keeps
|
|
481
|
+
// `_pendingAnimationInstall` while the animation goes back to un-started, and
|
|
482
|
+
// its first layout lands here. Start the latch — returning false would make
|
|
483
|
+
// the caller clear `_activeAnimationId` and apply the requested geometry,
|
|
484
|
+
// snapping this view straight to the target. A *join* must still never
|
|
485
|
+
// compute elapsed time against an un-rebased latch clock, so this returns
|
|
486
|
+
// instead of falling through.
|
|
487
|
+
if (!state.animation->started) {
|
|
488
|
+
if (![view smoothClipCanDisplay]) return false;
|
|
489
|
+
startLatchedAnimation(state);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
447
492
|
// Join from a laid-out peer's live presentation, exactly as a view that had
|
|
448
493
|
// registered at this moment would. When the joining view is the only
|
|
449
494
|
// registered host, its own layer never received geometry (the install was
|
|
450
495
|
// deferred for lack of layout), so fall back to the animation's start.
|
|
451
|
-
|
|
452
|
-
for (const ViewKey candidate : state.
|
|
496
|
+
SmoothClipView *reference = nil;
|
|
497
|
+
for (const ViewKey candidate : state.animation->participants) {
|
|
453
498
|
if (candidate != key && [viewForKey(candidate) smoothClipIsJoinable]) {
|
|
454
|
-
|
|
499
|
+
reference = viewForKey(candidate);
|
|
455
500
|
break;
|
|
456
501
|
}
|
|
457
502
|
}
|
|
458
|
-
const Presentation visible =
|
|
459
|
-
? [
|
|
503
|
+
const Presentation visible = reference != nil
|
|
504
|
+
? [reference smoothClipCurrentPresentation]
|
|
460
505
|
: state.animation->start;
|
|
461
506
|
[view smoothClipApplyPresentation:visible];
|
|
462
|
-
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
507
|
+
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
463
508
|
return true;
|
|
464
509
|
}
|
|
465
510
|
|
|
@@ -470,11 +515,49 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
470
515
|
|
|
471
516
|
auto &state = iterator->second;
|
|
472
517
|
const ViewKey key = keyForView(view);
|
|
518
|
+
state.views.erase(
|
|
519
|
+
std::remove(state.views.begin(), state.views.end(), key),
|
|
520
|
+
state.views.end());
|
|
473
521
|
if (state.animation.has_value() &&
|
|
474
522
|
state.animation->participants.erase(key) > 0) {
|
|
475
523
|
state.animation->finished = false;
|
|
476
|
-
if (state.animation->
|
|
524
|
+
if (!state.animation->started) {
|
|
525
|
+
// A held latch remains pending for the next displayable registration.
|
|
526
|
+
// Removing this participant also makes any late callback a no-op.
|
|
527
|
+
} else if (!anyDisplayableView(state)) {
|
|
477
528
|
auto &active = *state.animation;
|
|
529
|
+
// Freeze before removing/reusing the layer. stopLayerAnimation invalidates
|
|
530
|
+
// its delegate, and erasing the participant above independently rejects
|
|
531
|
+
// any callback already queued by CoreAnimation.
|
|
532
|
+
//
|
|
533
|
+
// The freeze is a teardown side effect that must reach the departing
|
|
534
|
+
// view either way, but only a laid-out view may *define* the frozen
|
|
535
|
+
// geometry — smoothClipCurrentPresentation reports {0,0,0,0} for one
|
|
536
|
+
// that never laid out, and re-latching there would resume the animation
|
|
537
|
+
// out of a collapsed clip. Same filter canonicalFrozenPresentation uses:
|
|
538
|
+
// this view if joinable, else a laid-out survivor, else the start.
|
|
539
|
+
double springContinuationVelocity = 0;
|
|
540
|
+
if (active.kind == AnimationKind::Spring &&
|
|
541
|
+
[view smoothClipIsJoinable]) {
|
|
542
|
+
springContinuationVelocity =
|
|
543
|
+
[view smoothClipSpringContinuationVelocity];
|
|
544
|
+
}
|
|
545
|
+
const Presentation frozen = [view smoothClipFreezePresentation];
|
|
546
|
+
Presentation visible = active.start;
|
|
547
|
+
if ([view smoothClipIsJoinable]) {
|
|
548
|
+
visible = frozen;
|
|
549
|
+
} else {
|
|
550
|
+
for (const ViewKey candidate : state.views) {
|
|
551
|
+
SmoothClipView *candidateView = viewForKey(candidate);
|
|
552
|
+
if (![candidateView smoothClipIsJoinable]) continue;
|
|
553
|
+
visible = [candidateView smoothClipCurrentPresentation];
|
|
554
|
+
if (active.kind == AnimationKind::Spring) {
|
|
555
|
+
springContinuationVelocity =
|
|
556
|
+
[candidateView smoothClipSpringContinuationVelocity];
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
478
561
|
const double elapsedMs =
|
|
479
562
|
(CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
480
563
|
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
@@ -486,20 +569,21 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
486
569
|
state.ownership = Ownership::Interactive;
|
|
487
570
|
emitCompletion(driverId, state, active.animationId, false);
|
|
488
571
|
} else {
|
|
489
|
-
// The last
|
|
572
|
+
// The last displayable host left mid-flight. A registered detached or
|
|
573
|
+
// unlaid-out peer must not let the animation finish invisibly.
|
|
490
574
|
// Completing here would leave `latest` at the target and statically
|
|
491
575
|
// snap any re-registering host straight to it. Re-latch instead:
|
|
492
|
-
// freeze the remaining animation at the
|
|
493
|
-
//
|
|
576
|
+
// freeze the remaining animation at the visible geometry resolved
|
|
577
|
+
// above so the next displayable host resumes it with the true
|
|
494
578
|
// remaining time. Ownership stays Native — the pending animation
|
|
495
579
|
// still owns rendering intent; destroyDriver cancels a latch that
|
|
496
580
|
// never finds a host, so the completion cannot hang.
|
|
497
|
-
const Presentation visible = [view smoothClipIsJoinable]
|
|
498
|
-
? [view smoothClipCurrentPresentation]
|
|
499
|
-
: active.start;
|
|
500
581
|
if (active.kind == AnimationKind::Timing) {
|
|
501
|
-
active.durationMs
|
|
502
|
-
|
|
582
|
+
const double progress = active.durationMs <= 0
|
|
583
|
+
? 1
|
|
584
|
+
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
585
|
+
active.timing = timingRemainder(active.timing, progress).animation;
|
|
586
|
+
active.durationMs = active.timing.durationMs;
|
|
503
587
|
} else if (active.kind == AnimationKind::Keyframes) {
|
|
504
588
|
const double progress = active.durationMs <= 0
|
|
505
589
|
? 1
|
|
@@ -514,15 +598,37 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
514
598
|
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
515
599
|
active.keyframes = std::move(remaining);
|
|
516
600
|
active.durationMs = remainingMs;
|
|
601
|
+
} else if (active.kind == AnimationKind::Spring) {
|
|
602
|
+
// The departing view resolved `inherit` before installing its CA
|
|
603
|
+
// spring. Re-express that spring's analytic velocity against the
|
|
604
|
+
// frozen remaining distance, so the resumed spring has the same
|
|
605
|
+
// physical derivative instead of restarting at rest.
|
|
606
|
+
active.spring.inheritVelocity = false;
|
|
607
|
+
active.spring.initialVelocity = springContinuationVelocity;
|
|
517
608
|
}
|
|
518
609
|
active.start = visible;
|
|
519
610
|
active.started = false;
|
|
520
611
|
}
|
|
612
|
+
} else if (state.animation->participants.empty()) {
|
|
613
|
+
// Every participant has either reported its stop or unregistered, yet a
|
|
614
|
+
// registered host can still display. viewAnimationDidStop is the only
|
|
615
|
+
// other exhaustion path and it can never run again for this id, so
|
|
616
|
+
// nothing is left to complete the animation: end it here.
|
|
617
|
+
//
|
|
618
|
+
// Reachable only with several hosts on one driver, where per-view spring
|
|
619
|
+
// settling durations let one host finish while another is still running
|
|
620
|
+
// (smoothClipAnimateSpring resolves 'inherit' per view, and
|
|
621
|
+
// springSettlingDurationWithVelocity derives the group duration from it).
|
|
622
|
+
//
|
|
623
|
+
// This branch MUST stay below the displayability test. A sole host that
|
|
624
|
+
// unregisters mid-flight also empties `participants`, and that case must
|
|
625
|
+
// RE-LATCH rather than complete — the next displayable host resuming the
|
|
626
|
+
// remainder is the entire point of the latch. Testing displayability
|
|
627
|
+
// first keeps the one-view-per-driver path on the branch above, which
|
|
628
|
+
// makes this one unreachable for a single host by construction.
|
|
629
|
+
finishIfNoInstalledParticipants(driverId, state);
|
|
521
630
|
}
|
|
522
631
|
}
|
|
523
|
-
state.views.erase(
|
|
524
|
-
std::remove(state.views.begin(), state.views.end(), key),
|
|
525
|
-
state.views.end());
|
|
526
632
|
if (state.destroyed && state.views.empty()) {
|
|
527
633
|
registry().erase(iterator);
|
|
528
634
|
}
|
|
@@ -531,7 +637,8 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
531
637
|
void setPresentation(
|
|
532
638
|
uint64_t driverId,
|
|
533
639
|
Presentation presentation,
|
|
534
|
-
bool takeOwnership
|
|
640
|
+
bool takeOwnership,
|
|
641
|
+
bool overridePendingAnimation) {
|
|
535
642
|
// CALayer writes require the main thread (not main-queue drain context).
|
|
536
643
|
if (!NSThread.isMainThread) {
|
|
537
644
|
// Never block an off-main caller: a synchronous hop can deadlock against
|
|
@@ -539,7 +646,8 @@ void setPresentation(
|
|
|
539
646
|
// thread while holding it, and the main thread routinely waits for the
|
|
540
647
|
// same mutex when draining scheduled worklets).
|
|
541
648
|
RCTExecuteOnMainQueue(^{
|
|
542
|
-
setPresentation(
|
|
649
|
+
setPresentation(
|
|
650
|
+
driverId, presentation, takeOwnership, overridePendingAnimation);
|
|
543
651
|
});
|
|
544
652
|
return;
|
|
545
653
|
}
|
|
@@ -574,13 +682,10 @@ void setPresentation(
|
|
|
574
682
|
}
|
|
575
683
|
if (takeOwnership) {
|
|
576
684
|
state.destroyed = false;
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
// animation into a static jump. Callers that want to override a latch
|
|
582
|
-
// cancel it explicitly first.
|
|
583
|
-
if (state.animation.has_value() && !state.animation->started) {
|
|
685
|
+
// Passive seeds and public setters must not displace newer pending intent.
|
|
686
|
+
// The fused animation.from write is authoritative and opts in explicitly.
|
|
687
|
+
if (state.animation.has_value() && !state.animation->started &&
|
|
688
|
+
!overridePendingAnimation) {
|
|
584
689
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
585
690
|
if (signpostsEnabled) {
|
|
586
691
|
os_signpost_interval_end(
|
|
@@ -633,13 +738,17 @@ int32_t animateTiming(
|
|
|
633
738
|
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
634
739
|
// documented rejection sentinel.
|
|
635
740
|
return 0;
|
|
636
|
-
}
|
|
741
|
+
}
|
|
742
|
+
auto iterator = registry().find(driverId);
|
|
637
743
|
if (iterator == registry().end()) {
|
|
638
|
-
return 0;
|
|
744
|
+
if (!start.hasInteractiveStart) return 0;
|
|
745
|
+
iterator = registry().try_emplace(driverId).first;
|
|
746
|
+
iterator->second.latest = start.interactiveStart;
|
|
747
|
+
iterator->second.hasLatest = true;
|
|
639
748
|
}
|
|
640
749
|
auto &state = iterator->second;
|
|
641
750
|
state.destroyed = false;
|
|
642
|
-
const int32_t animationId = allocateAnimationId(
|
|
751
|
+
const int32_t animationId = allocateAnimationId();
|
|
643
752
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
644
753
|
prepareAnimation(driverId, state, start, presentation);
|
|
645
754
|
|
|
@@ -655,7 +764,6 @@ int32_t animateTiming(
|
|
|
655
764
|
active.timing = animation;
|
|
656
765
|
active.durationMs = animation.durationMs;
|
|
657
766
|
active.startedAt = CACurrentMediaTime();
|
|
658
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
659
767
|
active.started = anyDisplayableView(state);
|
|
660
768
|
state.animation = std::move(active);
|
|
661
769
|
if (!state.animation->started) {
|
|
@@ -675,9 +783,11 @@ int32_t animateTiming(
|
|
|
675
783
|
static_cast<unsigned long>(state.views.size()));
|
|
676
784
|
#endif
|
|
677
785
|
for (const ViewKey key : state.views) {
|
|
678
|
-
[viewForKey(key) smoothClipAnimateTiming:presentation
|
|
679
|
-
|
|
680
|
-
|
|
786
|
+
if ([viewForKey(key) smoothClipAnimateTiming:presentation
|
|
787
|
+
animation:animation
|
|
788
|
+
animationId:animationId]) {
|
|
789
|
+
state.animation->participants.insert(key);
|
|
790
|
+
}
|
|
681
791
|
}
|
|
682
792
|
return animationId;
|
|
683
793
|
}
|
|
@@ -690,11 +800,17 @@ int32_t animateSpring(
|
|
|
690
800
|
if (!NSThread.isMainThread) {
|
|
691
801
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
692
802
|
return 0;
|
|
693
|
-
}
|
|
694
|
-
|
|
803
|
+
}
|
|
804
|
+
auto iterator = registry().find(driverId);
|
|
805
|
+
if (iterator == registry().end()) {
|
|
806
|
+
if (!start.hasInteractiveStart) return 0;
|
|
807
|
+
iterator = registry().try_emplace(driverId).first;
|
|
808
|
+
iterator->second.latest = start.interactiveStart;
|
|
809
|
+
iterator->second.hasLatest = true;
|
|
810
|
+
}
|
|
695
811
|
auto &state = iterator->second;
|
|
696
812
|
state.destroyed = false;
|
|
697
|
-
const int32_t animationId = allocateAnimationId(
|
|
813
|
+
const int32_t animationId = allocateAnimationId();
|
|
698
814
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
699
815
|
prepareAnimation(driverId, state, start, presentation);
|
|
700
816
|
|
|
@@ -709,7 +825,6 @@ int32_t animateSpring(
|
|
|
709
825
|
active.start = resolvedStart;
|
|
710
826
|
active.spring = animation;
|
|
711
827
|
active.startedAt = CACurrentMediaTime();
|
|
712
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
713
828
|
active.started = anyDisplayableView(state);
|
|
714
829
|
state.animation = std::move(active);
|
|
715
830
|
if (!state.animation->started) {
|
|
@@ -724,9 +839,11 @@ int32_t animateSpring(
|
|
|
724
839
|
static_cast<unsigned long>(state.views.size()));
|
|
725
840
|
#endif
|
|
726
841
|
for (const ViewKey key : state.views) {
|
|
727
|
-
[viewForKey(key) smoothClipAnimateSpring:presentation
|
|
728
|
-
|
|
729
|
-
|
|
842
|
+
if ([viewForKey(key) smoothClipAnimateSpring:presentation
|
|
843
|
+
animation:animation
|
|
844
|
+
animationId:animationId]) {
|
|
845
|
+
state.animation->participants.insert(key);
|
|
846
|
+
}
|
|
730
847
|
}
|
|
731
848
|
return animationId;
|
|
732
849
|
}
|
|
@@ -741,11 +858,17 @@ int32_t animateKeyframes(
|
|
|
741
858
|
if (!NSThread.isMainThread) {
|
|
742
859
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
743
860
|
return 0;
|
|
744
|
-
}
|
|
745
|
-
|
|
861
|
+
}
|
|
862
|
+
auto iterator = registry().find(driverId);
|
|
863
|
+
if (iterator == registry().end()) {
|
|
864
|
+
if (!start.hasInteractiveStart) return 0;
|
|
865
|
+
iterator = registry().try_emplace(driverId).first;
|
|
866
|
+
iterator->second.latest = start.interactiveStart;
|
|
867
|
+
iterator->second.hasLatest = true;
|
|
868
|
+
}
|
|
746
869
|
auto &state = iterator->second;
|
|
747
870
|
state.destroyed = false;
|
|
748
|
-
const int32_t animationId = allocateAnimationId(
|
|
871
|
+
const int32_t animationId = allocateAnimationId();
|
|
749
872
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
750
873
|
prepareAnimation(driverId, state, start, presentation);
|
|
751
874
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
|
|
@@ -760,7 +883,6 @@ int32_t animateKeyframes(
|
|
|
760
883
|
active.keyframes = keyframes;
|
|
761
884
|
active.durationMs = durationMs;
|
|
762
885
|
active.startedAt = CACurrentMediaTime();
|
|
763
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
764
886
|
active.started = anyDisplayableView(state);
|
|
765
887
|
state.animation = std::move(active);
|
|
766
888
|
if (!state.animation->started) {
|
|
@@ -768,10 +890,12 @@ int32_t animateKeyframes(
|
|
|
768
890
|
return animationId;
|
|
769
891
|
}
|
|
770
892
|
for (const ViewKey key : state.views) {
|
|
771
|
-
[viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
893
|
+
if ([viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
894
|
+
keyframes:keyframes
|
|
895
|
+
durationMs:durationMs
|
|
896
|
+
animationId:animationId]) {
|
|
897
|
+
state.animation->participants.insert(key);
|
|
898
|
+
}
|
|
775
899
|
}
|
|
776
900
|
return animationId;
|
|
777
901
|
}
|
|
@@ -782,7 +906,7 @@ int32_t rejectAnimation(uint64_t driverId) {
|
|
|
782
906
|
return 0;
|
|
783
907
|
} auto iterator = registry().find(driverId);
|
|
784
908
|
if (iterator == registry().end()) return 0;
|
|
785
|
-
const int32_t animationId = allocateAnimationId(
|
|
909
|
+
const int32_t animationId = allocateAnimationId();
|
|
786
910
|
emitStandaloneCompletion(driverId, animationId, false);
|
|
787
911
|
return animationId;
|
|
788
912
|
}
|
|
@@ -812,7 +936,30 @@ CancelResult cancelAnimation(
|
|
|
812
936
|
#endif
|
|
813
937
|
if (useTarget) {
|
|
814
938
|
const Presentation target = state.latest;
|
|
939
|
+
// A latch never dispatched to its views, so every participant still has
|
|
940
|
+
// _activeAnimationId == 0 and smoothClipCancelAnimationUsingTarget
|
|
941
|
+
// early-returns without writing anything: state.latest would report the
|
|
942
|
+
// target while every layer still showed the pre-animation geometry, and
|
|
943
|
+
// nothing else re-applies it. Android's cancelAnimation fans the result out
|
|
944
|
+
// unconditionally — match that. Read `started` before cancelActive, which
|
|
945
|
+
// resets state.animation.
|
|
946
|
+
//
|
|
947
|
+
// Gated on the latch case so a started animation is not written twice
|
|
948
|
+
// (smoothClipCancelAnimationUsingTarget already applied the target there),
|
|
949
|
+
// and applied WITHOUT a velocity sample: a jump to the target is not
|
|
950
|
+
// interactive motion, and the cancel-to-target path deliberately keeps it
|
|
951
|
+
// out of the 'inherit' history on both platforms — Android's cancel
|
|
952
|
+
// fan-out records nothing either.
|
|
953
|
+
const bool wasLatched = !state.animation->started;
|
|
815
954
|
cancelActive(driverId, state, true);
|
|
955
|
+
if (wasLatched) {
|
|
956
|
+
// state.latest already holds the target (set at animate time); only the
|
|
957
|
+
// view fan-out is missing.
|
|
958
|
+
for (const ViewKey key : state.views) {
|
|
959
|
+
[viewForKey(key) smoothClipApplyPresentation:target
|
|
960
|
+
recordVelocitySample:NO];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
816
963
|
state.ownership = Ownership::Interactive;
|
|
817
964
|
return {true, target};
|
|
818
965
|
}
|
|
@@ -853,15 +1000,17 @@ void viewAnimationDidStop(
|
|
|
853
1000
|
auto &state = iterator->second;
|
|
854
1001
|
if (!state.animation.has_value() ||
|
|
855
1002
|
state.animation->animationId != animationId) return;
|
|
1003
|
+
// A delegate from the frozen run can still report a stop for this id after
|
|
1004
|
+
// the animation has been re-latched. Draining participants there could
|
|
1005
|
+
// complete an animation that is waiting to resume. Only a running animation
|
|
1006
|
+
// may be completed by its installed delegates; a latch is completed by
|
|
1007
|
+
// replacement, cancel or destroy.
|
|
1008
|
+
if (!state.animation->started) return;
|
|
856
1009
|
|
|
857
1010
|
const ViewKey key = keyForView(view);
|
|
858
1011
|
if (state.animation->participants.erase(key) == 0) return;
|
|
859
1012
|
state.animation->finished = state.animation->finished && finished;
|
|
860
|
-
|
|
861
|
-
state.ownership = Ownership::Interactive;
|
|
862
|
-
emitCompletion(
|
|
863
|
-
driverId, state, animationId, state.animation->finished);
|
|
864
|
-
}
|
|
1013
|
+
finishIfNoInstalledParticipants(driverId, state);
|
|
865
1014
|
}
|
|
866
1015
|
|
|
867
1016
|
size_t registeredViewCount(uint64_t driverId) {
|
|
@@ -75,12 +75,14 @@ void SmoothClipTurboModule::setClipPresentation(
|
|
|
75
75
|
double radius,
|
|
76
76
|
double contentTranslateX,
|
|
77
77
|
double contentTranslateY,
|
|
78
|
-
bool takeOwnership
|
|
78
|
+
bool takeOwnership,
|
|
79
|
+
bool overridePendingAnimation) {
|
|
79
80
|
const smoothclip::Presentation presentation{
|
|
80
81
|
{x, y, width, height, radius}, contentTranslateX, contentTranslateY};
|
|
81
82
|
if (validDriverId(driverId) && finitePresentation(presentation)) {
|
|
82
83
|
smoothclip::setPresentation(
|
|
83
|
-
static_cast<uint64_t>(driverId), presentation, takeOwnership
|
|
84
|
+
static_cast<uint64_t>(driverId), presentation, takeOwnership,
|
|
85
|
+
overridePendingAnimation);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -49,7 +49,8 @@ class SmoothClipTurboModule final
|
|
|
49
49
|
double radius,
|
|
50
50
|
double contentTranslateX,
|
|
51
51
|
double contentTranslateY,
|
|
52
|
-
bool takeOwnership
|
|
52
|
+
bool takeOwnership,
|
|
53
|
+
bool overridePendingAnimation);
|
|
53
54
|
jsi::Array beginInteraction(jsi::Runtime &runtime, double driverId);
|
|
54
55
|
int32_t animateTiming(
|
|
55
56
|
jsi::Runtime &runtime,
|