react-native-smooth-clip-view 0.2.6 → 0.2.8
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 +32 -18
- package/android/build.gradle +15 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +7 -8
- package/android/src/main/cpp/SmoothClipBindings.cpp +11 -7
- package/android/src/main/cpp/SmoothClipRegistry.cpp +139 -137
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +8 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +75 -8
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
- package/cpp/SmoothClipAnimationCurve.h +92 -0
- package/cpp/SmoothClipVelocityTracker.h +5 -7
- package/ios/SmoothClipRegistry.mm +231 -132
- package/ios/SmoothClipView.mm +56 -150
- package/ios/SmoothClipViewRegistry.h +4 -1
- package/lib/module/driverState.js +8 -0
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.native.js +27 -6
- package/lib/module/drivers.native.js.map +1 -1
- package/lib/module/reactRequests.js +9 -3
- package/lib/module/reactRequests.js.map +1 -1
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/lib/typescript/src/reactRequests.d.ts +3 -1
- package/lib/typescript/src/reactRequests.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/driverState.ts +8 -0
- package/src/drivers.native.ts +31 -7
- package/src/reactRequests.ts +14 -2
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
- (BOOL)smoothClipCanDisplay;
|
|
44
44
|
- (BOOL)smoothClipHasPendingInstall;
|
|
45
45
|
- (double)smoothClipSpringContinuationVelocity;
|
|
46
|
+
- (void)smoothClipClearVelocitySamples;
|
|
46
47
|
@end
|
|
47
48
|
|
|
48
49
|
namespace smoothclip {
|
|
@@ -56,6 +57,10 @@ enum class AnimationKind { Timing, Spring, Keyframes };
|
|
|
56
57
|
struct ActiveAnimation {
|
|
57
58
|
int32_t animationId;
|
|
58
59
|
std::unordered_set<ViewKey> participants;
|
|
60
|
+
// Hosts that installed this id and temporarily lost displayability. They
|
|
61
|
+
// may rejoin the same remainder; completion is false if any remain
|
|
62
|
+
// unresolved when the run ends or unregister.
|
|
63
|
+
std::unordered_set<ViewKey> suspendedParticipants;
|
|
59
64
|
bool finished = true;
|
|
60
65
|
AnimationKind kind = AnimationKind::Timing;
|
|
61
66
|
// Geometry the transition started from. Used when a view must join the
|
|
@@ -69,10 +74,10 @@ struct ActiveAnimation {
|
|
|
69
74
|
// False while the animation is latched. Two ways in: built before any host
|
|
70
75
|
// view could display (pre-registration, or a host in a detached subtree), or
|
|
71
76
|
// re-latched by unregisterView when the last DISPLAYABLE host left
|
|
72
|
-
// mid-flight. Started by whichever
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
77
|
+
// mid-flight. Started by whichever registration/displayability update or
|
|
78
|
+
// joinActiveAnimation first finds a host that can produce a frame; each
|
|
79
|
+
// rebases startedAt through startLatchedAnimation. startedAt is only
|
|
80
|
+
// meaningful once started.
|
|
76
81
|
bool started = false;
|
|
77
82
|
};
|
|
78
83
|
|
|
@@ -98,6 +103,41 @@ std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
|
98
103
|
return value;
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
// The transitions must be observed for the whole process lifetime, not per
|
|
107
|
+
// view: the state below is process-global, and a notification delivered while
|
|
108
|
+
// no SmoothClipView happens to be alive (the last host unmounted during
|
|
109
|
+
// inactivity, or the app backgrounded between screens) must still land.
|
|
110
|
+
// View-held observers die with their views and left the flag permanently
|
|
111
|
+
// stale in exactly those windows.
|
|
112
|
+
void installApplicationStateObservers() {
|
|
113
|
+
static dispatch_once_t once;
|
|
114
|
+
dispatch_once(&once, ^{
|
|
115
|
+
NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
|
|
116
|
+
[center addObserverForName:UIApplicationWillResignActiveNotification
|
|
117
|
+
object:nil
|
|
118
|
+
queue:nil
|
|
119
|
+
usingBlock:^(NSNotification *) {
|
|
120
|
+
applicationWillResignActive();
|
|
121
|
+
}];
|
|
122
|
+
[center addObserverForName:UIApplicationDidBecomeActiveNotification
|
|
123
|
+
object:nil
|
|
124
|
+
queue:nil
|
|
125
|
+
usingBlock:^(NSNotification *) {
|
|
126
|
+
applicationDidBecomeActive();
|
|
127
|
+
}];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
bool &applicationActiveState() {
|
|
132
|
+
installApplicationStateObservers();
|
|
133
|
+
// The first SmoothClipView can be created after the app already entered the
|
|
134
|
+
// background, too late to observe WillResignActive. Seed from UIKit rather
|
|
135
|
+
// than assuming foreground so that first registration cannot start offscreen.
|
|
136
|
+
static bool active = UIApplication.sharedApplication.applicationState ==
|
|
137
|
+
UIApplicationStateActive;
|
|
138
|
+
return active;
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
CompletionSink &completionSink() {
|
|
102
142
|
static CompletionSink sink;
|
|
103
143
|
return sink;
|
|
@@ -176,7 +216,9 @@ void emitStandaloneCompletion(
|
|
|
176
216
|
#endif
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
Presentation canonicalFrozenPresentation(
|
|
219
|
+
Presentation canonicalFrozenPresentation(
|
|
220
|
+
DriverState &state,
|
|
221
|
+
SmoothClipView *preferred = nil) {
|
|
180
222
|
// A latched animation never rendered, so state.latest already holds its
|
|
181
223
|
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
182
224
|
Presentation canonical =
|
|
@@ -184,17 +226,35 @@ Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
|
184
226
|
? state.animation->start
|
|
185
227
|
: state.latest;
|
|
186
228
|
bool hasCanonical = false;
|
|
229
|
+
bool hasParticipantFallback = false;
|
|
230
|
+
Presentation participantFallback{{0, 0, 0, 0, 0}, 0, 0};
|
|
187
231
|
for (const ViewKey key : state.views) {
|
|
188
232
|
// Freeze every view (the teardown side effect must reach all of them),
|
|
189
233
|
// but only a laid-out view may define the canonical geometry — an
|
|
190
234
|
// unlaid-out one reports {0,0,0,0} and would collapse the clip.
|
|
191
235
|
SmoothClipView *candidate = viewForKey(key);
|
|
192
236
|
const Presentation frozen = [candidate smoothClipFreezePresentation];
|
|
193
|
-
if (
|
|
237
|
+
if (candidate == preferred && [candidate smoothClipIsJoinable]) {
|
|
238
|
+
canonical = frozen;
|
|
239
|
+
hasCanonical = true;
|
|
240
|
+
} else if (!hasCanonical && [candidate smoothClipIsJoinable]) {
|
|
194
241
|
canonical = frozen;
|
|
195
242
|
hasCanonical = true;
|
|
243
|
+
} else if (!hasParticipantFallback && state.animation.has_value() &&
|
|
244
|
+
(state.animation->participants.count(key) > 0 ||
|
|
245
|
+
state.animation->suspendedParticipants.count(key) > 0)) {
|
|
246
|
+
// An installed participant whose HOST just lost its size still holds
|
|
247
|
+
// real mid-flight geometry on its clip layer (host bounds do not feed
|
|
248
|
+
// the clip container). Without this fallback, a sole running host
|
|
249
|
+
// resized to zero froze the re-latch at state.latest — the target —
|
|
250
|
+
// and the resumed animation snapped to its endpoint.
|
|
251
|
+
hasParticipantFallback = true;
|
|
252
|
+
participantFallback = frozen;
|
|
196
253
|
}
|
|
197
254
|
}
|
|
255
|
+
if (!hasCanonical && hasParticipantFallback) {
|
|
256
|
+
canonical = participantFallback;
|
|
257
|
+
}
|
|
198
258
|
return canonical;
|
|
199
259
|
}
|
|
200
260
|
|
|
@@ -219,7 +279,8 @@ void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
|
219
279
|
return;
|
|
220
280
|
}
|
|
221
281
|
const int32_t animationId = state.animation->animationId;
|
|
222
|
-
const bool finished = state.animation->finished
|
|
282
|
+
const bool finished = state.animation->finished &&
|
|
283
|
+
state.animation->suspendedParticipants.empty();
|
|
223
284
|
// A detached/unlaid-out view can have a requested animation without any CA
|
|
224
285
|
// delegate capable of completing it. Once every installed animation ends,
|
|
225
286
|
// make those deferred views statically correct and invalidate their pending
|
|
@@ -231,12 +292,16 @@ void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
|
231
292
|
emitCompletion(driverId, state, animationId, finished);
|
|
232
293
|
}
|
|
233
294
|
|
|
234
|
-
void applyPresentation(
|
|
295
|
+
void applyPresentation(
|
|
296
|
+
DriverState &state,
|
|
297
|
+
Presentation presentation,
|
|
298
|
+
bool recordVelocitySample = true) {
|
|
235
299
|
state.latest = presentation;
|
|
236
300
|
state.hasLatest = true;
|
|
237
301
|
state.ownership = Ownership::Interactive;
|
|
238
302
|
for (const ViewKey key : state.views) {
|
|
239
|
-
[viewForKey(key) smoothClipApplyPresentation:presentation
|
|
303
|
+
[viewForKey(key) smoothClipApplyPresentation:presentation
|
|
304
|
+
recordVelocitySample:recordVelocitySample];
|
|
240
305
|
}
|
|
241
306
|
}
|
|
242
307
|
|
|
@@ -324,7 +389,10 @@ void dispatchActiveAnimationJoin(
|
|
|
324
389
|
durationMs:remainingMs
|
|
325
390
|
animationId:active.animationId];
|
|
326
391
|
}
|
|
327
|
-
if (installed)
|
|
392
|
+
if (installed) {
|
|
393
|
+
active.participants.insert(key);
|
|
394
|
+
active.suspendedParticipants.erase(key);
|
|
395
|
+
}
|
|
328
396
|
}
|
|
329
397
|
|
|
330
398
|
// An animation may only start once a registered view can actually produce a
|
|
@@ -339,10 +407,83 @@ bool anyDisplayableView(const DriverState &state) {
|
|
|
339
407
|
return false;
|
|
340
408
|
}
|
|
341
409
|
|
|
410
|
+
void relatchActiveAnimation(
|
|
411
|
+
uint64_t driverId,
|
|
412
|
+
DriverState &state,
|
|
413
|
+
SmoothClipView *preferred = nil) {
|
|
414
|
+
if (!state.animation.has_value() || !state.animation->started) return;
|
|
415
|
+
auto &active = *state.animation;
|
|
416
|
+
double springVelocity = 0;
|
|
417
|
+
if (active.kind == AnimationKind::Spring) {
|
|
418
|
+
SmoothClipView *source = preferred;
|
|
419
|
+
if (source == nil ||
|
|
420
|
+
active.participants.count(keyForView(source)) == 0) {
|
|
421
|
+
source = active.participants.empty()
|
|
422
|
+
? nil
|
|
423
|
+
: viewForKey(*active.participants.begin());
|
|
424
|
+
}
|
|
425
|
+
if (source != nil) {
|
|
426
|
+
springVelocity = [source smoothClipSpringContinuationVelocity];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const Presentation frozen = canonicalFrozenPresentation(state, preferred);
|
|
430
|
+
// Captured before the migration below: only hosts that were ALREADY
|
|
431
|
+
// suspended are unresolved. The active participants being frozen right now
|
|
432
|
+
// rendered every frame up to this instant.
|
|
433
|
+
const bool hadUnresolvedSuspended = !active.suspendedParticipants.empty();
|
|
434
|
+
active.suspendedParticipants.insert(
|
|
435
|
+
active.participants.begin(), active.participants.end());
|
|
436
|
+
active.participants.clear();
|
|
437
|
+
|
|
438
|
+
const double elapsedMs = MAX(
|
|
439
|
+
0, (CACurrentMediaTime() - active.startedAt) * 1000.0);
|
|
440
|
+
const double progress = active.durationMs <= 0
|
|
441
|
+
? 1
|
|
442
|
+
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
443
|
+
if (active.kind == AnimationKind::Timing) {
|
|
444
|
+
active.timing = timingRemainder(active.timing, progress).animation;
|
|
445
|
+
active.durationMs = active.timing.durationMs;
|
|
446
|
+
} else if (active.kind == AnimationKind::Keyframes) {
|
|
447
|
+
const KeyframeContinuation continuation = keyframeContinuation(
|
|
448
|
+
active.keyframes,
|
|
449
|
+
frozen,
|
|
450
|
+
state.latest,
|
|
451
|
+
active.durationMs,
|
|
452
|
+
progress);
|
|
453
|
+
active.keyframes = continuation.frames;
|
|
454
|
+
active.durationMs = continuation.durationMs;
|
|
455
|
+
} else {
|
|
456
|
+
active.spring.inheritVelocity = false;
|
|
457
|
+
active.spring.initialVelocity = springVelocity;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (state.destroyed ||
|
|
461
|
+
(active.kind != AnimationKind::Spring && active.durationMs <= 0)) {
|
|
462
|
+
// A residual trimmed to zero means the curve fully elapsed while a host
|
|
463
|
+
// displayed every frame — only the asynchronous didStop was outstanding
|
|
464
|
+
// when this freeze arrived. Report that run honestly instead of stamping
|
|
465
|
+
// the freeze-path false; hosts that were suspended before the freeze are
|
|
466
|
+
// unresolved and still poison it.
|
|
467
|
+
const bool ranToEnd = !state.destroyed && progress >= 1 &&
|
|
468
|
+
active.finished && !hadUnresolvedSuspended;
|
|
469
|
+
// Deferred peers hold pending installs for this id; invalidate them and
|
|
470
|
+
// finalize statically at the target, exactly as the didStop completion
|
|
471
|
+
// path does. Frozen participants already have no animation to cancel.
|
|
472
|
+
for (const ViewKey key : state.views) {
|
|
473
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
474
|
+
}
|
|
475
|
+
state.ownership = Ownership::Interactive;
|
|
476
|
+
emitCompletion(driverId, state, active.animationId, ranToEnd);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
active.start = frozen;
|
|
480
|
+
active.started = false;
|
|
481
|
+
}
|
|
482
|
+
|
|
342
483
|
// Starts a latched animation: rebases the clock so no progress was burned
|
|
343
484
|
// while no view could display, then dispatches the join to every registered
|
|
344
485
|
// view. Views that still cannot display defer their install per-view and
|
|
345
|
-
// resume via
|
|
486
|
+
// resume via a later positive displayability update.
|
|
346
487
|
void startLatchedAnimation(DriverState &state) {
|
|
347
488
|
state.animation->startedAt = CACurrentMediaTime();
|
|
348
489
|
state.animation->started = true;
|
|
@@ -352,13 +493,39 @@ void startLatchedAnimation(DriverState &state) {
|
|
|
352
493
|
const Presentation start = state.animation->start;
|
|
353
494
|
for (const ViewKey key : state.views) {
|
|
354
495
|
SmoothClipView *participant = viewForKey(key);
|
|
355
|
-
[participant smoothClipApplyPresentation:start];
|
|
496
|
+
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
356
497
|
dispatchActiveAnimationJoin(state, participant, key, start);
|
|
357
498
|
}
|
|
358
499
|
}
|
|
359
500
|
|
|
360
501
|
} // namespace
|
|
361
502
|
|
|
503
|
+
bool applicationIsActive() {
|
|
504
|
+
return applicationActiveState();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
void applicationWillResignActive() {
|
|
508
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
509
|
+
if (!applicationActiveState()) return;
|
|
510
|
+
applicationActiveState() = false;
|
|
511
|
+
for (auto &[driverId, state] : registry()) {
|
|
512
|
+
relatchActiveAnimation(driverId, state);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
void applicationDidBecomeActive() {
|
|
517
|
+
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
518
|
+
if (applicationActiveState()) return;
|
|
519
|
+
applicationActiveState() = true;
|
|
520
|
+
for (auto &[driverId, state] : registry()) {
|
|
521
|
+
(void)driverId;
|
|
522
|
+
if (state.animation.has_value() && !state.animation->started &&
|
|
523
|
+
anyDisplayableView(state)) {
|
|
524
|
+
startLatchedAnimation(state);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
362
529
|
void setCompletionCallback(
|
|
363
530
|
const void *owner,
|
|
364
531
|
CompletionCallback callback) {
|
|
@@ -394,8 +561,8 @@ void registerView(
|
|
|
394
561
|
const bool hasLatch = hasAnimation && !state.animation->started;
|
|
395
562
|
// A latch may only start once this view can produce a visible frame; a
|
|
396
563
|
// registration from a detached subtree (transparentModal before its view
|
|
397
|
-
// controller is presented) keeps the latch held
|
|
398
|
-
// starts it inside the attach commit.
|
|
564
|
+
// controller is presented) keeps the latch held; the positive
|
|
565
|
+
// displayability update starts it inside the attach commit.
|
|
399
566
|
const bool startsLatch = hasLatch && [view smoothClipCanDisplay];
|
|
400
567
|
const bool shouldReplay =
|
|
401
568
|
hasAnimation && state.animation->started && !state.views.empty();
|
|
@@ -425,7 +592,7 @@ void registerView(
|
|
|
425
592
|
state.views.end()) {
|
|
426
593
|
state.views.push_back(key);
|
|
427
594
|
}
|
|
428
|
-
[view smoothClipApplyPresentation:visible];
|
|
595
|
+
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
429
596
|
if (startsLatch) {
|
|
430
597
|
// Rebase the join clock and dispatch to every registered view. Only views
|
|
431
598
|
// that actually install their CA groups become completion participants.
|
|
@@ -436,7 +603,7 @@ void registerView(
|
|
|
436
603
|
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
437
604
|
}
|
|
438
605
|
|
|
439
|
-
void
|
|
606
|
+
void viewDisplayabilityChanged(uint64_t driverId, SmoothClipView *view) {
|
|
440
607
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
441
608
|
auto iterator = registry().find(driverId);
|
|
442
609
|
if (iterator == registry().end()) return;
|
|
@@ -447,15 +614,25 @@ void viewBecameDisplayable(uint64_t driverId, SmoothClipView *view) {
|
|
|
447
614
|
state.views.end()) {
|
|
448
615
|
return;
|
|
449
616
|
}
|
|
617
|
+
if (![view smoothClipCanDisplay]) {
|
|
618
|
+
if (!state.animation->started) return;
|
|
619
|
+
if (state.animation->participants.count(key) == 0) return;
|
|
620
|
+
if (!anyDisplayableView(state)) {
|
|
621
|
+
relatchActiveAnimation(driverId, state, view);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
[view smoothClipFreezePresentation];
|
|
625
|
+
state.animation->participants.erase(key);
|
|
626
|
+
state.animation->suspendedParticipants.insert(key);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
450
629
|
if (!state.animation->started) {
|
|
451
|
-
if (![view smoothClipCanDisplay]) return;
|
|
452
630
|
startLatchedAnimation(state);
|
|
453
631
|
return;
|
|
454
632
|
}
|
|
455
|
-
// A
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if ([view smoothClipHasPendingInstall]) {
|
|
633
|
+
// A deferred or suspended host joins from the registry's rebased remainder.
|
|
634
|
+
if ([view smoothClipHasPendingInstall] ||
|
|
635
|
+
state.animation->suspendedParticipants.count(key) > 0) {
|
|
459
636
|
joinActiveAnimation(driverId, view);
|
|
460
637
|
}
|
|
461
638
|
}
|
|
@@ -503,7 +680,7 @@ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
|
503
680
|
const Presentation visible = reference != nil
|
|
504
681
|
? [reference smoothClipCurrentPresentation]
|
|
505
682
|
: state.animation->start;
|
|
506
|
-
[view smoothClipApplyPresentation:visible];
|
|
683
|
+
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
507
684
|
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
508
685
|
return true;
|
|
509
686
|
}
|
|
@@ -515,117 +692,33 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
515
692
|
|
|
516
693
|
auto &state = iterator->second;
|
|
517
694
|
const ViewKey key = keyForView(view);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
}
|
|
695
|
+
bool installed = false;
|
|
696
|
+
if (state.animation.has_value()) {
|
|
697
|
+
installed = state.animation->participants.count(key) > 0 ||
|
|
698
|
+
state.animation->suspendedParticipants.count(key) > 0;
|
|
699
|
+
if (state.animation->started &&
|
|
700
|
+
state.animation->participants.count(key) > 0) {
|
|
701
|
+
bool otherDisplayable = false;
|
|
702
|
+
for (const ViewKey candidate : state.views) {
|
|
703
|
+
if (candidate != key && [viewForKey(candidate) smoothClipCanDisplay]) {
|
|
704
|
+
otherDisplayable = true;
|
|
558
705
|
break;
|
|
559
706
|
}
|
|
560
707
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
564
|
-
const bool canResume = !state.destroyed &&
|
|
565
|
-
(active.kind == AnimationKind::Spring || remainingMs > 0);
|
|
566
|
-
if (!canResume) {
|
|
567
|
-
// The animation ends here; without releasing ownership a host that
|
|
568
|
-
// remounts on this driver later would drop every interactive update.
|
|
569
|
-
state.ownership = Ownership::Interactive;
|
|
570
|
-
emitCompletion(driverId, state, active.animationId, false);
|
|
571
|
-
} else {
|
|
572
|
-
// The last displayable host left mid-flight. A registered detached or
|
|
573
|
-
// unlaid-out peer must not let the animation finish invisibly.
|
|
574
|
-
// Completing here would leave `latest` at the target and statically
|
|
575
|
-
// snap any re-registering host straight to it. Re-latch instead:
|
|
576
|
-
// freeze the remaining animation at the visible geometry resolved
|
|
577
|
-
// above so the next displayable host resumes it with the true
|
|
578
|
-
// remaining time. Ownership stays Native — the pending animation
|
|
579
|
-
// still owns rendering intent; destroyDriver cancels a latch that
|
|
580
|
-
// never finds a host, so the completion cannot hang.
|
|
581
|
-
if (active.kind == AnimationKind::Timing) {
|
|
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;
|
|
587
|
-
} else if (active.kind == AnimationKind::Keyframes) {
|
|
588
|
-
const double progress = active.durationMs <= 0
|
|
589
|
-
? 1
|
|
590
|
-
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
591
|
-
std::vector<Keyframe> remaining;
|
|
592
|
-
remaining.push_back({0, visible});
|
|
593
|
-
for (const Keyframe &frame : active.keyframes) {
|
|
594
|
-
if (frame.offset <= progress) continue;
|
|
595
|
-
const double offset = (frame.offset - progress) / (1 - progress);
|
|
596
|
-
remaining.push_back({offset, frame.presentation});
|
|
597
|
-
}
|
|
598
|
-
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
599
|
-
active.keyframes = std::move(remaining);
|
|
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;
|
|
608
|
-
}
|
|
609
|
-
active.start = visible;
|
|
610
|
-
active.started = false;
|
|
708
|
+
if (!otherDisplayable) {
|
|
709
|
+
relatchActiveAnimation(driverId, state, view);
|
|
611
710
|
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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.
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
state.views.erase(
|
|
715
|
+
std::remove(state.views.begin(), state.views.end(), key),
|
|
716
|
+
state.views.end());
|
|
717
|
+
if (state.animation.has_value()) {
|
|
718
|
+
state.animation->participants.erase(key);
|
|
719
|
+
state.animation->suspendedParticipants.erase(key);
|
|
720
|
+
if (installed) state.animation->finished = false;
|
|
721
|
+
if (state.animation->started && state.animation->participants.empty()) {
|
|
629
722
|
finishIfNoInstalledParticipants(driverId, state);
|
|
630
723
|
}
|
|
631
724
|
}
|
|
@@ -753,7 +846,7 @@ int32_t animateTiming(
|
|
|
753
846
|
prepareAnimation(driverId, state, start, presentation);
|
|
754
847
|
|
|
755
848
|
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
756
|
-
applyPresentation(state, presentation);
|
|
849
|
+
applyPresentation(state, presentation, false);
|
|
757
850
|
emitCompletion(driverId, state, animationId, true);
|
|
758
851
|
return animationId;
|
|
759
852
|
}
|
|
@@ -815,7 +908,7 @@ int32_t animateSpring(
|
|
|
815
908
|
prepareAnimation(driverId, state, start, presentation);
|
|
816
909
|
|
|
817
910
|
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
818
|
-
applyPresentation(state, presentation);
|
|
911
|
+
applyPresentation(state, presentation, false);
|
|
819
912
|
emitCompletion(driverId, state, animationId, true);
|
|
820
913
|
return animationId;
|
|
821
914
|
}
|
|
@@ -872,7 +965,7 @@ int32_t animateKeyframes(
|
|
|
872
965
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
873
966
|
prepareAnimation(driverId, state, start, presentation);
|
|
874
967
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
|
|
875
|
-
applyPresentation(state, presentation);
|
|
968
|
+
applyPresentation(state, presentation, false);
|
|
876
969
|
emitCompletion(driverId, state, animationId, true);
|
|
877
970
|
return animationId;
|
|
878
971
|
}
|
|
@@ -977,6 +1070,12 @@ void destroyDriver(uint64_t driverId) {
|
|
|
977
1070
|
if (iterator == registry().end()) return;
|
|
978
1071
|
auto &state = iterator->second;
|
|
979
1072
|
cancelActive(driverId, state, false);
|
|
1073
|
+
// A destroyed driver's interaction history must not seed a revived
|
|
1074
|
+
// incarnation: the revival seed would otherwise pair with these samples and
|
|
1075
|
+
// refresh the staleness clock with motion no finger produced.
|
|
1076
|
+
for (const ViewKey key : state.views) {
|
|
1077
|
+
[viewForKey(key) smoothClipClearVelocitySamples];
|
|
1078
|
+
}
|
|
980
1079
|
if (state.views.empty()) {
|
|
981
1080
|
registry().erase(iterator);
|
|
982
1081
|
} else {
|