react-native-smooth-clip-view 0.2.5 → 0.2.7
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 +68 -26
- package/android/CMakeLists.txt +0 -1
- package/android/build.gradle +15 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +12 -8
- package/android/src/main/cpp/SmoothClipBindings.cpp +39 -15
- package/android/src/main/cpp/SmoothClipRegistry.cpp +288 -285
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +28 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +180 -42
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
- package/cpp/SmoothClipAnimationCurve.h +560 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +30 -10
- package/ios/SmoothClipRegistry.mm +398 -150
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +85 -140
- package/ios/SmoothClipViewRegistry.h +4 -1
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/driverState.js +8 -0
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.native.js +122 -32
- 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/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +3 -2
- package/lib/typescript/src/driverTypes.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/NativeSmoothClipModule.ts +2 -1
- package/src/driverState.ts +8 -0
- package/src/driverTypes.ts +3 -2
- package/src/drivers.native.ts +160 -29
- package/src/reactRequests.ts +14 -2
|
@@ -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,8 @@
|
|
|
37
42
|
- (BOOL)smoothClipIsJoinable;
|
|
38
43
|
- (BOOL)smoothClipCanDisplay;
|
|
39
44
|
- (BOOL)smoothClipHasPendingInstall;
|
|
45
|
+
- (double)smoothClipSpringContinuationVelocity;
|
|
46
|
+
- (void)smoothClipClearVelocitySamples;
|
|
40
47
|
@end
|
|
41
48
|
|
|
42
49
|
namespace smoothclip {
|
|
@@ -50,6 +57,10 @@ enum class AnimationKind { Timing, Spring, Keyframes };
|
|
|
50
57
|
struct ActiveAnimation {
|
|
51
58
|
int32_t animationId;
|
|
52
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;
|
|
53
64
|
bool finished = true;
|
|
54
65
|
AnimationKind kind = AnimationKind::Timing;
|
|
55
66
|
// Geometry the transition started from. Used when a view must join the
|
|
@@ -60,9 +71,13 @@ struct ActiveAnimation {
|
|
|
60
71
|
std::vector<Keyframe> keyframes;
|
|
61
72
|
double durationMs = 0;
|
|
62
73
|
CFTimeInterval startedAt = 0;
|
|
63
|
-
// False while the animation is latched: built before any host
|
|
64
|
-
//
|
|
65
|
-
//
|
|
74
|
+
// False while the animation is latched. Two ways in: built before any host
|
|
75
|
+
// view could display (pre-registration, or a host in a detached subtree), or
|
|
76
|
+
// re-latched by unregisterView when the last DISPLAYABLE host left
|
|
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.
|
|
66
81
|
bool started = false;
|
|
67
82
|
};
|
|
68
83
|
|
|
@@ -72,7 +87,6 @@ struct DriverState {
|
|
|
72
87
|
Ownership ownership = Ownership::Interactive;
|
|
73
88
|
std::vector<ViewKey> views;
|
|
74
89
|
std::optional<ActiveAnimation> animation;
|
|
75
|
-
int32_t nextAnimationId = 0;
|
|
76
90
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
77
91
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
78
92
|
// last view leaves and revived by a take-ownership setPresentation.
|
|
@@ -89,6 +103,41 @@ std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
|
89
103
|
return value;
|
|
90
104
|
}
|
|
91
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
|
+
|
|
92
141
|
CompletionSink &completionSink() {
|
|
93
142
|
static CompletionSink sink;
|
|
94
143
|
return sink;
|
|
@@ -167,15 +216,9 @@ void emitStandaloneCompletion(
|
|
|
167
216
|
#endif
|
|
168
217
|
}
|
|
169
218
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
? 1
|
|
174
|
-
: state.nextAnimationId + 1;
|
|
175
|
-
return state.nextAnimationId;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
219
|
+
Presentation canonicalFrozenPresentation(
|
|
220
|
+
DriverState &state,
|
|
221
|
+
SmoothClipView *preferred = nil) {
|
|
179
222
|
// A latched animation never rendered, so state.latest already holds its
|
|
180
223
|
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
181
224
|
Presentation canonical =
|
|
@@ -183,17 +226,35 @@ Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
|
183
226
|
? state.animation->start
|
|
184
227
|
: state.latest;
|
|
185
228
|
bool hasCanonical = false;
|
|
229
|
+
bool hasParticipantFallback = false;
|
|
230
|
+
Presentation participantFallback{{0, 0, 0, 0, 0}, 0, 0};
|
|
186
231
|
for (const ViewKey key : state.views) {
|
|
187
232
|
// Freeze every view (the teardown side effect must reach all of them),
|
|
188
233
|
// but only a laid-out view may define the canonical geometry — an
|
|
189
234
|
// unlaid-out one reports {0,0,0,0} and would collapse the clip.
|
|
190
235
|
SmoothClipView *candidate = viewForKey(key);
|
|
191
236
|
const Presentation frozen = [candidate smoothClipFreezePresentation];
|
|
192
|
-
if (
|
|
237
|
+
if (candidate == preferred && [candidate smoothClipIsJoinable]) {
|
|
238
|
+
canonical = frozen;
|
|
239
|
+
hasCanonical = true;
|
|
240
|
+
} else if (!hasCanonical && [candidate smoothClipIsJoinable]) {
|
|
193
241
|
canonical = frozen;
|
|
194
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;
|
|
195
253
|
}
|
|
196
254
|
}
|
|
255
|
+
if (!hasCanonical && hasParticipantFallback) {
|
|
256
|
+
canonical = participantFallback;
|
|
257
|
+
}
|
|
197
258
|
return canonical;
|
|
198
259
|
}
|
|
199
260
|
|
|
@@ -203,18 +264,44 @@ void cancelActive(
|
|
|
203
264
|
bool useTarget) {
|
|
204
265
|
if (!state.animation.has_value()) return;
|
|
205
266
|
const int32_t animationId = state.animation->animationId;
|
|
206
|
-
|
|
267
|
+
// Deferred views are deliberately not completion participants, but they do
|
|
268
|
+
// hold a pending install and requested target. Cancellation must still reach
|
|
269
|
+
// every registered view so a later attach cannot resurrect the old id.
|
|
270
|
+
for (const ViewKey key : state.views) {
|
|
207
271
|
[viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
|
|
208
272
|
}
|
|
209
273
|
emitCompletion(driverId, state, animationId, false);
|
|
210
274
|
}
|
|
211
275
|
|
|
212
|
-
void
|
|
276
|
+
void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
|
|
277
|
+
if (!state.animation.has_value() || !state.animation->started ||
|
|
278
|
+
!state.animation->participants.empty()) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const int32_t animationId = state.animation->animationId;
|
|
282
|
+
const bool finished = state.animation->finished &&
|
|
283
|
+
state.animation->suspendedParticipants.empty();
|
|
284
|
+
// A detached/unlaid-out view can have a requested animation without any CA
|
|
285
|
+
// delegate capable of completing it. Once every installed animation ends,
|
|
286
|
+
// make those deferred views statically correct and invalidate their pending
|
|
287
|
+
// ids before releasing the driver-level completion.
|
|
288
|
+
for (const ViewKey key : state.views) {
|
|
289
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
290
|
+
}
|
|
291
|
+
state.ownership = Ownership::Interactive;
|
|
292
|
+
emitCompletion(driverId, state, animationId, finished);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
void applyPresentation(
|
|
296
|
+
DriverState &state,
|
|
297
|
+
Presentation presentation,
|
|
298
|
+
bool recordVelocitySample = true) {
|
|
213
299
|
state.latest = presentation;
|
|
214
300
|
state.hasLatest = true;
|
|
215
301
|
state.ownership = Ownership::Interactive;
|
|
216
302
|
for (const ViewKey key : state.views) {
|
|
217
|
-
[viewForKey(key) smoothClipApplyPresentation:presentation
|
|
303
|
+
[viewForKey(key) smoothClipApplyPresentation:presentation
|
|
304
|
+
recordVelocitySample:recordVelocitySample];
|
|
218
305
|
}
|
|
219
306
|
}
|
|
220
307
|
|
|
@@ -258,21 +345,33 @@ void dispatchActiveAnimationJoin(
|
|
|
258
345
|
DriverState &state,
|
|
259
346
|
SmoothClipView *view,
|
|
260
347
|
ViewKey key,
|
|
261
|
-
const Presentation &visible
|
|
348
|
+
const Presentation &visible,
|
|
349
|
+
SmoothClipView *reference = nil) {
|
|
262
350
|
auto &active = *state.animation;
|
|
263
|
-
active.participants.insert(key);
|
|
264
351
|
const double elapsedMs = (CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
265
352
|
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
353
|
+
BOOL installed = NO;
|
|
266
354
|
if (active.kind == AnimationKind::Timing) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
355
|
+
const double progress = active.durationMs <= 0
|
|
356
|
+
? 1
|
|
357
|
+
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
358
|
+
TimingAnimation timing = timingRemainder(active.timing, progress).animation;
|
|
359
|
+
installed = [view smoothClipAnimateTiming:state.latest
|
|
360
|
+
animation:timing
|
|
361
|
+
animationId:active.animationId];
|
|
272
362
|
} else if (active.kind == AnimationKind::Spring) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
363
|
+
SpringAnimation spring = active.spring;
|
|
364
|
+
if (reference != nil) {
|
|
365
|
+
// A late host must join the spring's CURRENT physical state. Reusing the
|
|
366
|
+
// original launch velocity (or resolving `inherit` from the joining
|
|
367
|
+
// host's unrelated samples) creates a different spring at the seam.
|
|
368
|
+
spring.inheritVelocity = false;
|
|
369
|
+
spring.initialVelocity =
|
|
370
|
+
[reference smoothClipSpringContinuationVelocity];
|
|
371
|
+
}
|
|
372
|
+
installed = [view smoothClipAnimateSpring:state.latest
|
|
373
|
+
animation:spring
|
|
374
|
+
animationId:active.animationId];
|
|
276
375
|
} else {
|
|
277
376
|
const double progress = active.durationMs <= 0
|
|
278
377
|
? 1
|
|
@@ -285,10 +384,14 @@ void dispatchActiveAnimationJoin(
|
|
|
285
384
|
remaining.push_back({offset, frame.presentation});
|
|
286
385
|
}
|
|
287
386
|
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
288
|
-
[view smoothClipAnimateKeyframes:state.latest
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
387
|
+
installed = [view smoothClipAnimateKeyframes:state.latest
|
|
388
|
+
keyframes:remaining
|
|
389
|
+
durationMs:remainingMs
|
|
390
|
+
animationId:active.animationId];
|
|
391
|
+
}
|
|
392
|
+
if (installed) {
|
|
393
|
+
active.participants.insert(key);
|
|
394
|
+
active.suspendedParticipants.erase(key);
|
|
292
395
|
}
|
|
293
396
|
}
|
|
294
397
|
|
|
@@ -304,23 +407,125 @@ bool anyDisplayableView(const DriverState &state) {
|
|
|
304
407
|
return false;
|
|
305
408
|
}
|
|
306
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
|
+
|
|
307
483
|
// Starts a latched animation: rebases the clock so no progress was burned
|
|
308
484
|
// while no view could display, then dispatches the join to every registered
|
|
309
485
|
// view. Views that still cannot display defer their install per-view and
|
|
310
|
-
// resume via
|
|
486
|
+
// resume via a later positive displayability update.
|
|
311
487
|
void startLatchedAnimation(DriverState &state) {
|
|
312
488
|
state.animation->startedAt = CACurrentMediaTime();
|
|
313
489
|
state.animation->started = true;
|
|
490
|
+
// Participants describe live CA delegates, not registered views. Anything
|
|
491
|
+
// left from the frozen run can only produce a stale callback for that run.
|
|
492
|
+
state.animation->participants.clear();
|
|
314
493
|
const Presentation start = state.animation->start;
|
|
315
494
|
for (const ViewKey key : state.views) {
|
|
316
495
|
SmoothClipView *participant = viewForKey(key);
|
|
317
|
-
[participant smoothClipApplyPresentation:start];
|
|
496
|
+
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
318
497
|
dispatchActiveAnimationJoin(state, participant, key, start);
|
|
319
498
|
}
|
|
320
499
|
}
|
|
321
500
|
|
|
322
501
|
} // namespace
|
|
323
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
|
+
|
|
324
529
|
void setCompletionCallback(
|
|
325
530
|
const void *owner,
|
|
326
531
|
CompletionCallback callback) {
|
|
@@ -356,18 +561,18 @@ void registerView(
|
|
|
356
561
|
const bool hasLatch = hasAnimation && !state.animation->started;
|
|
357
562
|
// A latch may only start once this view can produce a visible frame; a
|
|
358
563
|
// registration from a detached subtree (transparentModal before its view
|
|
359
|
-
// controller is presented) keeps the latch held
|
|
360
|
-
// 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.
|
|
361
566
|
const bool startsLatch = hasLatch && [view smoothClipCanDisplay];
|
|
362
567
|
const bool shouldReplay =
|
|
363
568
|
hasAnimation && state.animation->started && !state.views.empty();
|
|
364
569
|
Presentation visible = state.latest;
|
|
570
|
+
SmoothClipView *reference = nil;
|
|
365
571
|
if (shouldReplay) {
|
|
366
572
|
// A registered peer that never laid out reports zero geometry; joining
|
|
367
573
|
// from it would visibly collapse the clip. Prefer the first laid-out
|
|
368
574
|
// peer and fall back to the animation's start.
|
|
369
|
-
|
|
370
|
-
for (const ViewKey candidate : state.views) {
|
|
575
|
+
for (const ViewKey candidate : state.animation->participants) {
|
|
371
576
|
SmoothClipView *candidateView = viewForKey(candidate);
|
|
372
577
|
if ([candidateView smoothClipIsJoinable]) {
|
|
373
578
|
reference = candidateView;
|
|
@@ -387,19 +592,18 @@ void registerView(
|
|
|
387
592
|
state.views.end()) {
|
|
388
593
|
state.views.push_back(key);
|
|
389
594
|
}
|
|
390
|
-
[view smoothClipApplyPresentation:visible];
|
|
595
|
+
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
391
596
|
if (startsLatch) {
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// orphan-safe for a mount-then-unmount before first layout).
|
|
597
|
+
// Rebase the join clock and dispatch to every registered view. Only views
|
|
598
|
+
// that actually install their CA groups become completion participants.
|
|
395
599
|
startLatchedAnimation(state);
|
|
396
600
|
return;
|
|
397
601
|
}
|
|
398
602
|
if (!shouldReplay) return;
|
|
399
|
-
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
603
|
+
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
400
604
|
}
|
|
401
605
|
|
|
402
|
-
void
|
|
606
|
+
void viewDisplayabilityChanged(uint64_t driverId, SmoothClipView *view) {
|
|
403
607
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
404
608
|
auto iterator = registry().find(driverId);
|
|
405
609
|
if (iterator == registry().end()) return;
|
|
@@ -410,15 +614,25 @@ void viewBecameDisplayable(uint64_t driverId, SmoothClipView *view) {
|
|
|
410
614
|
state.views.end()) {
|
|
411
615
|
return;
|
|
412
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
|
+
}
|
|
413
629
|
if (!state.animation->started) {
|
|
414
|
-
if (![view smoothClipCanDisplay]) return;
|
|
415
630
|
startLatchedAnimation(state);
|
|
416
631
|
return;
|
|
417
632
|
}
|
|
418
|
-
// A
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
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) {
|
|
422
636
|
joinActiveAnimation(driverId, view);
|
|
423
637
|
}
|
|
424
638
|
}
|
|
@@ -433,33 +647,41 @@ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
|
433
647
|
if (!state.animation.has_value() || state.views.empty()) {
|
|
434
648
|
return false;
|
|
435
649
|
}
|
|
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
650
|
const ViewKey key = keyForView(view);
|
|
443
651
|
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
444
652
|
state.views.end()) {
|
|
445
653
|
return false;
|
|
446
654
|
}
|
|
655
|
+
// A pending install used to imply the animation was dispatched, which
|
|
656
|
+
// implied started. Re-latching on displayability broke that: when the last
|
|
657
|
+
// displayable host leaves, a peer that deferred its install keeps
|
|
658
|
+
// `_pendingAnimationInstall` while the animation goes back to un-started, and
|
|
659
|
+
// its first layout lands here. Start the latch — returning false would make
|
|
660
|
+
// the caller clear `_activeAnimationId` and apply the requested geometry,
|
|
661
|
+
// snapping this view straight to the target. A *join* must still never
|
|
662
|
+
// compute elapsed time against an un-rebased latch clock, so this returns
|
|
663
|
+
// instead of falling through.
|
|
664
|
+
if (!state.animation->started) {
|
|
665
|
+
if (![view smoothClipCanDisplay]) return false;
|
|
666
|
+
startLatchedAnimation(state);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
447
669
|
// Join from a laid-out peer's live presentation, exactly as a view that had
|
|
448
670
|
// registered at this moment would. When the joining view is the only
|
|
449
671
|
// registered host, its own layer never received geometry (the install was
|
|
450
672
|
// deferred for lack of layout), so fall back to the animation's start.
|
|
451
|
-
|
|
452
|
-
for (const ViewKey candidate : state.
|
|
673
|
+
SmoothClipView *reference = nil;
|
|
674
|
+
for (const ViewKey candidate : state.animation->participants) {
|
|
453
675
|
if (candidate != key && [viewForKey(candidate) smoothClipIsJoinable]) {
|
|
454
|
-
|
|
676
|
+
reference = viewForKey(candidate);
|
|
455
677
|
break;
|
|
456
678
|
}
|
|
457
679
|
}
|
|
458
|
-
const Presentation visible =
|
|
459
|
-
? [
|
|
680
|
+
const Presentation visible = reference != nil
|
|
681
|
+
? [reference smoothClipCurrentPresentation]
|
|
460
682
|
: state.animation->start;
|
|
461
|
-
[view smoothClipApplyPresentation:visible];
|
|
462
|
-
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
683
|
+
[view smoothClipApplyPresentation:visible recordVelocitySample:NO];
|
|
684
|
+
dispatchActiveAnimationJoin(state, view, key, visible, reference);
|
|
463
685
|
return true;
|
|
464
686
|
}
|
|
465
687
|
|
|
@@ -470,59 +692,36 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
470
692
|
|
|
471
693
|
auto &state = iterator->second;
|
|
472
694
|
const ViewKey key = keyForView(view);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
state.animation->
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// The animation ends here; without releasing ownership a host that
|
|
485
|
-
// remounts on this driver later would drop every interactive update.
|
|
486
|
-
state.ownership = Ownership::Interactive;
|
|
487
|
-
emitCompletion(driverId, state, active.animationId, false);
|
|
488
|
-
} else {
|
|
489
|
-
// The last rendering host left mid-flight (remount, screen swap).
|
|
490
|
-
// Completing here would leave `latest` at the target and statically
|
|
491
|
-
// snap any re-registering host straight to it. Re-latch instead:
|
|
492
|
-
// freeze the remaining animation at the departing view's visible
|
|
493
|
-
// geometry so the next displayable host resumes it with the true
|
|
494
|
-
// remaining time. Ownership stays Native — the pending animation
|
|
495
|
-
// still owns rendering intent; destroyDriver cancels a latch that
|
|
496
|
-
// never finds a host, so the completion cannot hang.
|
|
497
|
-
const Presentation visible = [view smoothClipIsJoinable]
|
|
498
|
-
? [view smoothClipCurrentPresentation]
|
|
499
|
-
: active.start;
|
|
500
|
-
if (active.kind == AnimationKind::Timing) {
|
|
501
|
-
active.durationMs = remainingMs;
|
|
502
|
-
active.timing.durationMs = remainingMs;
|
|
503
|
-
} else if (active.kind == AnimationKind::Keyframes) {
|
|
504
|
-
const double progress = active.durationMs <= 0
|
|
505
|
-
? 1
|
|
506
|
-
: MIN(1, MAX(0, elapsedMs / active.durationMs));
|
|
507
|
-
std::vector<Keyframe> remaining;
|
|
508
|
-
remaining.push_back({0, visible});
|
|
509
|
-
for (const Keyframe &frame : active.keyframes) {
|
|
510
|
-
if (frame.offset <= progress) continue;
|
|
511
|
-
const double offset = (frame.offset - progress) / (1 - progress);
|
|
512
|
-
remaining.push_back({offset, frame.presentation});
|
|
513
|
-
}
|
|
514
|
-
if (remaining.size() == 1) remaining.push_back({1, state.latest});
|
|
515
|
-
active.keyframes = std::move(remaining);
|
|
516
|
-
active.durationMs = remainingMs;
|
|
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;
|
|
705
|
+
break;
|
|
517
706
|
}
|
|
518
|
-
|
|
519
|
-
|
|
707
|
+
}
|
|
708
|
+
if (!otherDisplayable) {
|
|
709
|
+
relatchActiveAnimation(driverId, state, view);
|
|
520
710
|
}
|
|
521
711
|
}
|
|
522
712
|
}
|
|
713
|
+
|
|
523
714
|
state.views.erase(
|
|
524
715
|
std::remove(state.views.begin(), state.views.end(), key),
|
|
525
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()) {
|
|
722
|
+
finishIfNoInstalledParticipants(driverId, state);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
526
725
|
if (state.destroyed && state.views.empty()) {
|
|
527
726
|
registry().erase(iterator);
|
|
528
727
|
}
|
|
@@ -531,7 +730,8 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
531
730
|
void setPresentation(
|
|
532
731
|
uint64_t driverId,
|
|
533
732
|
Presentation presentation,
|
|
534
|
-
bool takeOwnership
|
|
733
|
+
bool takeOwnership,
|
|
734
|
+
bool overridePendingAnimation) {
|
|
535
735
|
// CALayer writes require the main thread (not main-queue drain context).
|
|
536
736
|
if (!NSThread.isMainThread) {
|
|
537
737
|
// Never block an off-main caller: a synchronous hop can deadlock against
|
|
@@ -539,7 +739,8 @@ void setPresentation(
|
|
|
539
739
|
// thread while holding it, and the main thread routinely waits for the
|
|
540
740
|
// same mutex when draining scheduled worklets).
|
|
541
741
|
RCTExecuteOnMainQueue(^{
|
|
542
|
-
setPresentation(
|
|
742
|
+
setPresentation(
|
|
743
|
+
driverId, presentation, takeOwnership, overridePendingAnimation);
|
|
543
744
|
});
|
|
544
745
|
return;
|
|
545
746
|
}
|
|
@@ -574,13 +775,10 @@ void setPresentation(
|
|
|
574
775
|
}
|
|
575
776
|
if (takeOwnership) {
|
|
576
777
|
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) {
|
|
778
|
+
// Passive seeds and public setters must not displace newer pending intent.
|
|
779
|
+
// The fused animation.from write is authoritative and opts in explicitly.
|
|
780
|
+
if (state.animation.has_value() && !state.animation->started &&
|
|
781
|
+
!overridePendingAnimation) {
|
|
584
782
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
585
783
|
if (signpostsEnabled) {
|
|
586
784
|
os_signpost_interval_end(
|
|
@@ -633,18 +831,22 @@ int32_t animateTiming(
|
|
|
633
831
|
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
634
832
|
// documented rejection sentinel.
|
|
635
833
|
return 0;
|
|
636
|
-
}
|
|
834
|
+
}
|
|
835
|
+
auto iterator = registry().find(driverId);
|
|
637
836
|
if (iterator == registry().end()) {
|
|
638
|
-
return 0;
|
|
837
|
+
if (!start.hasInteractiveStart) return 0;
|
|
838
|
+
iterator = registry().try_emplace(driverId).first;
|
|
839
|
+
iterator->second.latest = start.interactiveStart;
|
|
840
|
+
iterator->second.hasLatest = true;
|
|
639
841
|
}
|
|
640
842
|
auto &state = iterator->second;
|
|
641
843
|
state.destroyed = false;
|
|
642
|
-
const int32_t animationId = allocateAnimationId(
|
|
844
|
+
const int32_t animationId = allocateAnimationId();
|
|
643
845
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
644
846
|
prepareAnimation(driverId, state, start, presentation);
|
|
645
847
|
|
|
646
848
|
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
647
|
-
applyPresentation(state, presentation);
|
|
849
|
+
applyPresentation(state, presentation, false);
|
|
648
850
|
emitCompletion(driverId, state, animationId, true);
|
|
649
851
|
return animationId;
|
|
650
852
|
}
|
|
@@ -655,7 +857,6 @@ int32_t animateTiming(
|
|
|
655
857
|
active.timing = animation;
|
|
656
858
|
active.durationMs = animation.durationMs;
|
|
657
859
|
active.startedAt = CACurrentMediaTime();
|
|
658
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
659
860
|
active.started = anyDisplayableView(state);
|
|
660
861
|
state.animation = std::move(active);
|
|
661
862
|
if (!state.animation->started) {
|
|
@@ -675,9 +876,11 @@ int32_t animateTiming(
|
|
|
675
876
|
static_cast<unsigned long>(state.views.size()));
|
|
676
877
|
#endif
|
|
677
878
|
for (const ViewKey key : state.views) {
|
|
678
|
-
[viewForKey(key) smoothClipAnimateTiming:presentation
|
|
679
|
-
|
|
680
|
-
|
|
879
|
+
if ([viewForKey(key) smoothClipAnimateTiming:presentation
|
|
880
|
+
animation:animation
|
|
881
|
+
animationId:animationId]) {
|
|
882
|
+
state.animation->participants.insert(key);
|
|
883
|
+
}
|
|
681
884
|
}
|
|
682
885
|
return animationId;
|
|
683
886
|
}
|
|
@@ -690,16 +893,22 @@ int32_t animateSpring(
|
|
|
690
893
|
if (!NSThread.isMainThread) {
|
|
691
894
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
692
895
|
return 0;
|
|
693
|
-
}
|
|
694
|
-
|
|
896
|
+
}
|
|
897
|
+
auto iterator = registry().find(driverId);
|
|
898
|
+
if (iterator == registry().end()) {
|
|
899
|
+
if (!start.hasInteractiveStart) return 0;
|
|
900
|
+
iterator = registry().try_emplace(driverId).first;
|
|
901
|
+
iterator->second.latest = start.interactiveStart;
|
|
902
|
+
iterator->second.hasLatest = true;
|
|
903
|
+
}
|
|
695
904
|
auto &state = iterator->second;
|
|
696
905
|
state.destroyed = false;
|
|
697
|
-
const int32_t animationId = allocateAnimationId(
|
|
906
|
+
const int32_t animationId = allocateAnimationId();
|
|
698
907
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
699
908
|
prepareAnimation(driverId, state, start, presentation);
|
|
700
909
|
|
|
701
910
|
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
702
|
-
applyPresentation(state, presentation);
|
|
911
|
+
applyPresentation(state, presentation, false);
|
|
703
912
|
emitCompletion(driverId, state, animationId, true);
|
|
704
913
|
return animationId;
|
|
705
914
|
}
|
|
@@ -709,7 +918,6 @@ int32_t animateSpring(
|
|
|
709
918
|
active.start = resolvedStart;
|
|
710
919
|
active.spring = animation;
|
|
711
920
|
active.startedAt = CACurrentMediaTime();
|
|
712
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
713
921
|
active.started = anyDisplayableView(state);
|
|
714
922
|
state.animation = std::move(active);
|
|
715
923
|
if (!state.animation->started) {
|
|
@@ -724,9 +932,11 @@ int32_t animateSpring(
|
|
|
724
932
|
static_cast<unsigned long>(state.views.size()));
|
|
725
933
|
#endif
|
|
726
934
|
for (const ViewKey key : state.views) {
|
|
727
|
-
[viewForKey(key) smoothClipAnimateSpring:presentation
|
|
728
|
-
|
|
729
|
-
|
|
935
|
+
if ([viewForKey(key) smoothClipAnimateSpring:presentation
|
|
936
|
+
animation:animation
|
|
937
|
+
animationId:animationId]) {
|
|
938
|
+
state.animation->participants.insert(key);
|
|
939
|
+
}
|
|
730
940
|
}
|
|
731
941
|
return animationId;
|
|
732
942
|
}
|
|
@@ -741,15 +951,21 @@ int32_t animateKeyframes(
|
|
|
741
951
|
if (!NSThread.isMainThread) {
|
|
742
952
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
743
953
|
return 0;
|
|
744
|
-
}
|
|
745
|
-
|
|
954
|
+
}
|
|
955
|
+
auto iterator = registry().find(driverId);
|
|
956
|
+
if (iterator == registry().end()) {
|
|
957
|
+
if (!start.hasInteractiveStart) return 0;
|
|
958
|
+
iterator = registry().try_emplace(driverId).first;
|
|
959
|
+
iterator->second.latest = start.interactiveStart;
|
|
960
|
+
iterator->second.hasLatest = true;
|
|
961
|
+
}
|
|
746
962
|
auto &state = iterator->second;
|
|
747
963
|
state.destroyed = false;
|
|
748
|
-
const int32_t animationId = allocateAnimationId(
|
|
964
|
+
const int32_t animationId = allocateAnimationId();
|
|
749
965
|
const Presentation resolvedStart = resolvedAnimationStart(state, start);
|
|
750
966
|
prepareAnimation(driverId, state, start, presentation);
|
|
751
967
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
|
|
752
|
-
applyPresentation(state, presentation);
|
|
968
|
+
applyPresentation(state, presentation, false);
|
|
753
969
|
emitCompletion(driverId, state, animationId, true);
|
|
754
970
|
return animationId;
|
|
755
971
|
}
|
|
@@ -760,7 +976,6 @@ int32_t animateKeyframes(
|
|
|
760
976
|
active.keyframes = keyframes;
|
|
761
977
|
active.durationMs = durationMs;
|
|
762
978
|
active.startedAt = CACurrentMediaTime();
|
|
763
|
-
active.participants.insert(state.views.begin(), state.views.end());
|
|
764
979
|
active.started = anyDisplayableView(state);
|
|
765
980
|
state.animation = std::move(active);
|
|
766
981
|
if (!state.animation->started) {
|
|
@@ -768,10 +983,12 @@ int32_t animateKeyframes(
|
|
|
768
983
|
return animationId;
|
|
769
984
|
}
|
|
770
985
|
for (const ViewKey key : state.views) {
|
|
771
|
-
[viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
986
|
+
if ([viewForKey(key) smoothClipAnimateKeyframes:presentation
|
|
987
|
+
keyframes:keyframes
|
|
988
|
+
durationMs:durationMs
|
|
989
|
+
animationId:animationId]) {
|
|
990
|
+
state.animation->participants.insert(key);
|
|
991
|
+
}
|
|
775
992
|
}
|
|
776
993
|
return animationId;
|
|
777
994
|
}
|
|
@@ -782,7 +999,7 @@ int32_t rejectAnimation(uint64_t driverId) {
|
|
|
782
999
|
return 0;
|
|
783
1000
|
} auto iterator = registry().find(driverId);
|
|
784
1001
|
if (iterator == registry().end()) return 0;
|
|
785
|
-
const int32_t animationId = allocateAnimationId(
|
|
1002
|
+
const int32_t animationId = allocateAnimationId();
|
|
786
1003
|
emitStandaloneCompletion(driverId, animationId, false);
|
|
787
1004
|
return animationId;
|
|
788
1005
|
}
|
|
@@ -812,7 +1029,30 @@ CancelResult cancelAnimation(
|
|
|
812
1029
|
#endif
|
|
813
1030
|
if (useTarget) {
|
|
814
1031
|
const Presentation target = state.latest;
|
|
1032
|
+
// A latch never dispatched to its views, so every participant still has
|
|
1033
|
+
// _activeAnimationId == 0 and smoothClipCancelAnimationUsingTarget
|
|
1034
|
+
// early-returns without writing anything: state.latest would report the
|
|
1035
|
+
// target while every layer still showed the pre-animation geometry, and
|
|
1036
|
+
// nothing else re-applies it. Android's cancelAnimation fans the result out
|
|
1037
|
+
// unconditionally — match that. Read `started` before cancelActive, which
|
|
1038
|
+
// resets state.animation.
|
|
1039
|
+
//
|
|
1040
|
+
// Gated on the latch case so a started animation is not written twice
|
|
1041
|
+
// (smoothClipCancelAnimationUsingTarget already applied the target there),
|
|
1042
|
+
// and applied WITHOUT a velocity sample: a jump to the target is not
|
|
1043
|
+
// interactive motion, and the cancel-to-target path deliberately keeps it
|
|
1044
|
+
// out of the 'inherit' history on both platforms — Android's cancel
|
|
1045
|
+
// fan-out records nothing either.
|
|
1046
|
+
const bool wasLatched = !state.animation->started;
|
|
815
1047
|
cancelActive(driverId, state, true);
|
|
1048
|
+
if (wasLatched) {
|
|
1049
|
+
// state.latest already holds the target (set at animate time); only the
|
|
1050
|
+
// view fan-out is missing.
|
|
1051
|
+
for (const ViewKey key : state.views) {
|
|
1052
|
+
[viewForKey(key) smoothClipApplyPresentation:target
|
|
1053
|
+
recordVelocitySample:NO];
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
816
1056
|
state.ownership = Ownership::Interactive;
|
|
817
1057
|
return {true, target};
|
|
818
1058
|
}
|
|
@@ -830,6 +1070,12 @@ void destroyDriver(uint64_t driverId) {
|
|
|
830
1070
|
if (iterator == registry().end()) return;
|
|
831
1071
|
auto &state = iterator->second;
|
|
832
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
|
+
}
|
|
833
1079
|
if (state.views.empty()) {
|
|
834
1080
|
registry().erase(iterator);
|
|
835
1081
|
} else {
|
|
@@ -853,15 +1099,17 @@ void viewAnimationDidStop(
|
|
|
853
1099
|
auto &state = iterator->second;
|
|
854
1100
|
if (!state.animation.has_value() ||
|
|
855
1101
|
state.animation->animationId != animationId) return;
|
|
1102
|
+
// A delegate from the frozen run can still report a stop for this id after
|
|
1103
|
+
// the animation has been re-latched. Draining participants there could
|
|
1104
|
+
// complete an animation that is waiting to resume. Only a running animation
|
|
1105
|
+
// may be completed by its installed delegates; a latch is completed by
|
|
1106
|
+
// replacement, cancel or destroy.
|
|
1107
|
+
if (!state.animation->started) return;
|
|
856
1108
|
|
|
857
1109
|
const ViewKey key = keyForView(view);
|
|
858
1110
|
if (state.animation->participants.erase(key) == 0) return;
|
|
859
1111
|
state.animation->finished = state.animation->finished && finished;
|
|
860
|
-
|
|
861
|
-
state.ownership = Ownership::Interactive;
|
|
862
|
-
emitCompletion(
|
|
863
|
-
driverId, state, animationId, state.animation->finished);
|
|
864
|
-
}
|
|
1112
|
+
finishIfNoInstalledParticipants(driverId, state);
|
|
865
1113
|
}
|
|
866
1114
|
|
|
867
1115
|
size_t registeredViewCount(uint64_t driverId) {
|