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
package/README.md
CHANGED
|
@@ -208,10 +208,10 @@ puts on screen, which is the property worth keeping identical.
|
|
|
208
208
|
(< 4 ms apart, e.g. a release-sample `from` seed right after the last drag
|
|
209
209
|
write) coalesce into one sample, and an identical re-write is ignored, so a
|
|
210
210
|
fused handoff can neither zero nor inflate the inherited velocity.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
Only interactive writes contribute samples. Internal freeze/join/resume and
|
|
212
|
+
static-finalization writes do not, so grabbing a native animation cannot
|
|
213
|
+
manufacture velocity for a later `'inherit'` spring; a subsequent drag (or
|
|
214
|
+
explicit `animation.from`) supplies the release samples instead.
|
|
215
215
|
- `driver.react` exposes `beginInteraction`, `set`, `animateTo`, and `cancel`
|
|
216
216
|
as Promises (`setScalars` is UI-worklet-only). React code never blocks
|
|
217
217
|
waiting for main/UI-thread work. An immediate animation request resolves
|
|
@@ -249,26 +249,40 @@ puts on screen, which is the property worth keeping identical.
|
|
|
249
249
|
authoritative interactive start, creates the missing driver state and
|
|
250
250
|
returns a real id. `0` — with no completion — is reserved for off-main,
|
|
251
251
|
invalid-id, or otherwise unsupported dispatch: a missing-state native request
|
|
252
|
-
with no authoritative start,
|
|
253
|
-
driver's hook has unmounted
|
|
252
|
+
with no authoritative start, any `driver.ui.animateTo` issued after the
|
|
253
|
+
driver's hook has unmounted, and an invalid-parameter request issued before
|
|
254
|
+
the driver's first seed reached native (validation rejections mint their
|
|
255
|
+
`finished: false` completion only once the native entry exists). (The
|
|
256
|
+
post-unmount case is decided on the UI runtime,
|
|
254
257
|
because a destroyed driver and a not-yet-seeded one are the same missing
|
|
255
258
|
registry entry to native — accepting it would build a latch nothing can start
|
|
256
|
-
and nothing can cancel.)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
259
|
+
and nothing can cancel.) A host is displayable only while it is attached,
|
|
260
|
+
foreground/window-visible, laid out, and positive-sized (the window's own
|
|
261
|
+
`hidden`/scene state is not consulted — RN's single always-visible window
|
|
262
|
+
makes it moot). Losing the last
|
|
263
|
+
displayable host mid-flight — including temporary detach, zero-size layout,
|
|
264
|
+
or app background — freezes and re-latches the exact remainder instead of
|
|
265
|
+
consuming duration offscreen. Foreground/reattach resumes the same animation
|
|
266
|
+
ID from that stored timing/keyframe phase or spring state.
|
|
267
|
+
Parallel Reanimated clocks do **not** pause with it: a `withTiming` started
|
|
268
|
+
beside `animateTo` keeps its wall-clock start, so after backgrounding it
|
|
269
|
+
completes on its first resumed frame while the native run still animates its
|
|
270
|
+
preserved remainder. Key teardown and state transitions off
|
|
271
|
+
`onAnimationComplete` (or re-synchronize on `AppState`) rather than off a
|
|
272
|
+
duration-matched Reanimated callback.
|
|
273
|
+
With multiple hosts on one driver, a host becomes an installed participant
|
|
274
|
+
only after its native animation starts. Temporary loss moves it to suspended;
|
|
275
|
+
a rejoin restores active participation. Unregistering an installed/suspended
|
|
276
|
+
host, or reaching completion while it remains suspended, makes the eventual
|
|
277
|
+
completion `finished: false`. A host that stayed deferred because it was
|
|
278
|
+
detached, unlaid-out, or zero-sized never poisons completion. `finished: true`
|
|
279
|
+
means every installed participant either ran to the end or rejoined and did so.
|
|
266
280
|
- An `animateTo` issued before any host view can produce a visible frame (for
|
|
267
281
|
example from an effect in the same commit that mounts the host, or inside a
|
|
268
282
|
modal route whose subtree attaches to its window late) is held pending and
|
|
269
283
|
starts with its full duration at the first moment a registered host can
|
|
270
|
-
produce a frame —
|
|
271
|
-
|
|
284
|
+
produce a frame — positive layout, window attach/visibility, and foreground
|
|
285
|
+
state must all be present. This also covers an animation worklet that runs before the
|
|
272
286
|
hook's seed worklet: the animation creates the state and the later passive
|
|
273
287
|
seed cannot reset its ownership or active id. A pending animation owns the
|
|
274
288
|
driver: ordinary take-ownership writes (`set`, `setScalars`, the hook's seed)
|
package/android/build.gradle
CHANGED
|
@@ -114,6 +114,19 @@ android {
|
|
|
114
114
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
testOptions {
|
|
118
|
+
unitTests {
|
|
119
|
+
includeAndroidResources = true
|
|
120
|
+
all {
|
|
121
|
+
// Robolectric on Java 17 reflects into these platform packages.
|
|
122
|
+
jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED",
|
|
123
|
+
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
|
124
|
+
"--add-opens=java.base/java.io=ALL-UNNAMED",
|
|
125
|
+
"--add-opens=java.base/java.net=ALL-UNNAMED"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
packagingOptions {
|
|
118
131
|
// Provided by the host application at runtime.
|
|
119
132
|
excludes += [
|
|
@@ -128,4 +141,6 @@ android {
|
|
|
128
141
|
dependencies {
|
|
129
142
|
implementation "com.facebook.react:react-android"
|
|
130
143
|
testImplementation "junit:junit:4.13.2"
|
|
144
|
+
testImplementation "org.mockito:mockito-core:5.14.2"
|
|
145
|
+
testImplementation "org.robolectric:robolectric:4.16"
|
|
131
146
|
}
|
|
@@ -22,7 +22,6 @@ struct JSmoothClipView : facebook::jni::JavaClass<JSmoothClipView> {
|
|
|
22
22
|
const NormalizedClip &clip,
|
|
23
23
|
double contentTranslateXPx,
|
|
24
24
|
double contentTranslateYPx) const;
|
|
25
|
-
bool isViewAttachedToWindow() const;
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
// Called from Kotlin (SmoothClipViewManager) on the UI thread when a Fabric
|
|
@@ -33,7 +32,8 @@ void registerViewAndroid(
|
|
|
33
32
|
Presentation initialPresentation,
|
|
34
33
|
double density,
|
|
35
34
|
double hostWidthPx,
|
|
36
|
-
double hostHeightPx
|
|
35
|
+
double hostHeightPx,
|
|
36
|
+
bool lifecycleVisible);
|
|
37
37
|
// Refreshes a registered view's density / host size (px) and synchronously
|
|
38
38
|
// redelivers the driver's visible value pre-normalized against them.
|
|
39
39
|
void setViewHostGeometryAndroid(
|
|
@@ -45,13 +45,12 @@ void setViewHostGeometryAndroid(
|
|
|
45
45
|
void unregisterViewAndroid(
|
|
46
46
|
uint64_t driverId,
|
|
47
47
|
facebook::jni::alias_ref<JSmoothClipView> view);
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// burn progress no one can see.
|
|
52
|
-
void viewBecameDisplayableAndroid(
|
|
48
|
+
// Pushes attachment/window visibility independently of host geometry. The
|
|
49
|
+
// registry combines both signals into its displayability/participant state.
|
|
50
|
+
void setViewLifecycleVisibilityAndroid(
|
|
53
51
|
uint64_t driverId,
|
|
54
|
-
facebook::jni::alias_ref<JSmoothClipView> view
|
|
52
|
+
facebook::jni::alias_ref<JSmoothClipView> view,
|
|
53
|
+
bool lifecycleVisible);
|
|
55
54
|
|
|
56
55
|
// Advances the registry frame loop. Called from Kotlin (SmoothClipBindings)
|
|
57
56
|
// inside Choreographer#doFrame with the frame's vsync timestamp converted to
|
|
@@ -389,7 +389,8 @@ void nativeRegisterView(
|
|
|
389
389
|
jdouble contentTranslateY,
|
|
390
390
|
jdouble density,
|
|
391
391
|
jdouble widthPx,
|
|
392
|
-
jdouble heightPx
|
|
392
|
+
jdouble heightPx,
|
|
393
|
+
jboolean lifecycleVisible) {
|
|
393
394
|
if (driverId <= 0 || !std::isfinite(driverId)) return;
|
|
394
395
|
smoothclip::registerViewAndroid(
|
|
395
396
|
static_cast<uint64_t>(driverId),
|
|
@@ -398,7 +399,8 @@ void nativeRegisterView(
|
|
|
398
399
|
{x, y, width, height, radius}, contentTranslateX, contentTranslateY},
|
|
399
400
|
density,
|
|
400
401
|
widthPx,
|
|
401
|
-
heightPx
|
|
402
|
+
heightPx,
|
|
403
|
+
lifecycleVisible != 0);
|
|
402
404
|
}
|
|
403
405
|
|
|
404
406
|
void nativeSetViewHostGeometry(
|
|
@@ -421,13 +423,14 @@ void nativeUnregisterView(
|
|
|
421
423
|
smoothclip::unregisterViewAndroid(static_cast<uint64_t>(driverId), view);
|
|
422
424
|
}
|
|
423
425
|
|
|
424
|
-
void
|
|
426
|
+
void nativeSetViewLifecycleVisibility(
|
|
425
427
|
jni::alias_ref<jni::JObject>,
|
|
426
428
|
jdouble driverId,
|
|
427
|
-
jni::alias_ref<smoothclip::JSmoothClipView> view
|
|
429
|
+
jni::alias_ref<smoothclip::JSmoothClipView> view,
|
|
430
|
+
jboolean lifecycleVisible) {
|
|
428
431
|
if (driverId <= 0 || !std::isfinite(driverId)) return;
|
|
429
|
-
smoothclip::
|
|
430
|
-
static_cast<uint64_t>(driverId), view);
|
|
432
|
+
smoothclip::setViewLifecycleVisibilityAndroid(
|
|
433
|
+
static_cast<uint64_t>(driverId), view, lifecycleVisible != 0);
|
|
431
434
|
}
|
|
432
435
|
|
|
433
436
|
void nativeInvalidate(jni::alias_ref<jni::JObject>) {
|
|
@@ -451,7 +454,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
|
451
454
|
"nativeSetViewHostGeometry", nativeSetViewHostGeometry),
|
|
452
455
|
makeNativeMethod("nativeUnregisterView", nativeUnregisterView),
|
|
453
456
|
makeNativeMethod(
|
|
454
|
-
"
|
|
457
|
+
"nativeSetViewLifecycleVisibility",
|
|
458
|
+
nativeSetViewLifecycleVisibility),
|
|
455
459
|
makeNativeMethod("nativeInvalidate", nativeInvalidate),
|
|
456
460
|
makeNativeMethod("nativeOnFrame", nativeOnFrame),
|
|
457
461
|
});
|
|
@@ -26,6 +26,7 @@ using facebook::jni::global_ref;
|
|
|
26
26
|
|
|
27
27
|
enum class Ownership { Interactive, Native };
|
|
28
28
|
enum class AnimationKind { Timing, Spring, Keyframes };
|
|
29
|
+
enum class ViewParticipation { Deferred, Active, Suspended };
|
|
29
30
|
|
|
30
31
|
// Load-bearing invariant, not just a convenience clock: advance()'s frame-clock
|
|
31
32
|
// anchor compares this value ABSOLUTELY against a choreographer frame stamp
|
|
@@ -60,11 +61,6 @@ struct ActiveAnimation {
|
|
|
60
61
|
KeyframeCurve keyframes;
|
|
61
62
|
double durationS = 0;
|
|
62
63
|
double startedAtS = 0;
|
|
63
|
-
// Wall-clock twin of startedAtS: stamped together with it, NEVER moved by
|
|
64
|
-
// the frame-clock anchor. The unregister re-latch remainder needs honest
|
|
65
|
-
// wall elapsed and reads this; the animation curve itself paces on the
|
|
66
|
-
// anchored startedAtS frame axis.
|
|
67
|
-
double wallStartedAtS = 0;
|
|
68
64
|
// Integrated spring state (channels: x, y, width, height, radius, tx, ty).
|
|
69
65
|
std::array<double, 7> springPosition{};
|
|
70
66
|
std::array<double, 7> springVelocity{};
|
|
@@ -74,9 +70,8 @@ struct ActiveAnimation {
|
|
|
74
70
|
// so it cannot tick. Two ways in: built before any host view could display
|
|
75
71
|
// (pre-registration, or a detached/unsized host), or re-latched by
|
|
76
72
|
// unregisterViewAndroid when the last DISPLAYABLE host left mid-flight.
|
|
77
|
-
// Started by whichever
|
|
78
|
-
//
|
|
79
|
-
// the clock through startLatchedAnimation.
|
|
73
|
+
// Started by whichever lifecycle/geometry update first sees a displayable
|
|
74
|
+
// entry; each rebases the clock through startLatchedAnimation.
|
|
80
75
|
bool started = false;
|
|
81
76
|
// False until the first advance() translates the wall-clock start stamp
|
|
82
77
|
// onto the choreographer frame-time axis (elapsed-preserving). Every
|
|
@@ -94,6 +89,8 @@ struct ViewEntry {
|
|
|
94
89
|
double density = 0;
|
|
95
90
|
double hostWidthPx = 0;
|
|
96
91
|
double hostHeightPx = 0;
|
|
92
|
+
bool lifecycleVisible = false;
|
|
93
|
+
ViewParticipation participation = ViewParticipation::Deferred;
|
|
97
94
|
};
|
|
98
95
|
|
|
99
96
|
struct DriverState {
|
|
@@ -349,6 +346,9 @@ std::vector<uint64_t> &animatingDrivers() {
|
|
|
349
346
|
// erase/recreate cycle would let one frame integrate the animation twice.
|
|
350
347
|
void clearActiveAnimation(uint64_t driverId, DriverState &state) {
|
|
351
348
|
state.animation.reset();
|
|
349
|
+
for (ViewEntry &entry : state.views) {
|
|
350
|
+
entry.participation = ViewParticipation::Deferred;
|
|
351
|
+
}
|
|
352
352
|
auto &active = animatingDrivers();
|
|
353
353
|
active.erase(
|
|
354
354
|
std::remove(active.begin(), active.end(), driverId), active.end());
|
|
@@ -388,7 +388,7 @@ Presentation prepareAnimation(
|
|
|
388
388
|
// no one can see (the transparentModal mount pattern).
|
|
389
389
|
bool entryDisplayable(const ViewEntry &entry) {
|
|
390
390
|
return entry.hostWidthPx > 0 && entry.hostHeightPx > 0 &&
|
|
391
|
-
entry.
|
|
391
|
+
entry.lifecycleVisible;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
bool anyDisplayableView(const DriverState &state) {
|
|
@@ -405,7 +405,6 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
405
405
|
animation.started = true;
|
|
406
406
|
animation.startedAtS = nowSeconds();
|
|
407
407
|
animation.lastFrameS = animation.startedAtS;
|
|
408
|
-
animation.wallStartedAtS = animation.startedAtS;
|
|
409
408
|
// Load-bearing for re-latch resumes: unregisterViewAndroid rewrites this
|
|
410
409
|
// ActiveAnimation in place, so a stale anchor would replay the fraction-0
|
|
411
410
|
// first frame the anchor exists to remove.
|
|
@@ -414,6 +413,11 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
414
413
|
if (std::find(active.begin(), active.end(), driverId) == active.end()) {
|
|
415
414
|
active.push_back(driverId);
|
|
416
415
|
}
|
|
416
|
+
for (ViewEntry &entry : state.views) {
|
|
417
|
+
if (entryDisplayable(entry)) {
|
|
418
|
+
entry.participation = ViewParticipation::Active;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
417
421
|
applyToViews(state, animation.start);
|
|
418
422
|
scheduleFrame();
|
|
419
423
|
}
|
|
@@ -435,10 +439,6 @@ int32_t startAnimation(
|
|
|
435
439
|
const StartStamp stamp = resolveStartStamp(startedAtHintS, wallNow);
|
|
436
440
|
animation.startedAtS = stamp.startedAtS;
|
|
437
441
|
animation.lastFrameS = stamp.startedAtS;
|
|
438
|
-
// Wall axis for the re-latch remainder keeps the native call stamp: the
|
|
439
|
-
// hint is at most a frame older and the remainder math wants honest wall
|
|
440
|
-
// elapsed from when the animation actually began integrating.
|
|
441
|
-
animation.wallStartedAtS = wallNow;
|
|
442
442
|
animation.frameClockAnchored = stamp.frameClockAnchored;
|
|
443
443
|
// current = start even while latched is load-bearing: cancelAnimation,
|
|
444
444
|
// beginInteraction, prepareAnimation's visibleBefore and registerView's
|
|
@@ -447,6 +447,11 @@ int32_t startAnimation(
|
|
|
447
447
|
animation.current = animation.start;
|
|
448
448
|
animation.started = anyDisplayableView(state);
|
|
449
449
|
state.animation = std::move(animation);
|
|
450
|
+
for (ViewEntry &entry : state.views) {
|
|
451
|
+
entry.participation = state.animation->started && entryDisplayable(entry)
|
|
452
|
+
? ViewParticipation::Active
|
|
453
|
+
: ViewParticipation::Deferred;
|
|
454
|
+
}
|
|
450
455
|
if (!state.animation->started) {
|
|
451
456
|
// Latch: no host view can display yet (animateTo raced the mount, or
|
|
452
457
|
// every host is detached/unsized). The first displayable registration,
|
|
@@ -464,6 +469,78 @@ int32_t startAnimation(
|
|
|
464
469
|
return state.animation->id;
|
|
465
470
|
}
|
|
466
471
|
|
|
472
|
+
void relatchIfNoDisplayable(uint64_t driverId, DriverState &state) {
|
|
473
|
+
if (!state.animation.has_value() || !state.animation->started ||
|
|
474
|
+
anyDisplayableView(state)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
auto &animation = *state.animation;
|
|
478
|
+
for (ViewEntry &entry : state.views) {
|
|
479
|
+
if (entry.participation == ViewParticipation::Active) {
|
|
480
|
+
entry.participation = ViewParticipation::Suspended;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (animation.kind == AnimationKind::Timing) {
|
|
485
|
+
const TimingContinuation continuation = timingContinuationAtFrame(
|
|
486
|
+
animation.timing,
|
|
487
|
+
animation.start,
|
|
488
|
+
animation.target,
|
|
489
|
+
animation.lastFrameS,
|
|
490
|
+
animation.startedAtS,
|
|
491
|
+
animation.durationS);
|
|
492
|
+
animation.current = continuation.start;
|
|
493
|
+
animation.timing = continuation.animation;
|
|
494
|
+
animation.durationS = continuation.animation.durationMs / 1000.0;
|
|
495
|
+
} else if (animation.kind == AnimationKind::Keyframes) {
|
|
496
|
+
const KeyframeContinuation continuation = keyframeContinuationAtFrame(
|
|
497
|
+
animation.keyframes.frames(),
|
|
498
|
+
animation.current,
|
|
499
|
+
animation.target,
|
|
500
|
+
animation.lastFrameS,
|
|
501
|
+
animation.startedAtS,
|
|
502
|
+
animation.durationS);
|
|
503
|
+
animation.current = continuation.start;
|
|
504
|
+
animation.keyframes.reset(continuation.frames);
|
|
505
|
+
animation.durationS = continuation.durationMs / 1000.0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (state.destroyed ||
|
|
509
|
+
(animation.kind != AnimationKind::Spring && animation.durationS <= 0)) {
|
|
510
|
+
state.ownership = Ownership::Interactive;
|
|
511
|
+
finishActive(driverId, state, false);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
animation.start = animation.current;
|
|
516
|
+
animation.started = false;
|
|
517
|
+
animation.frameClockAnchored = false;
|
|
518
|
+
auto &active = animatingDrivers();
|
|
519
|
+
active.erase(
|
|
520
|
+
std::remove(active.begin(), active.end(), driverId), active.end());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
void reconcileViewDisplayability(
|
|
524
|
+
uint64_t driverId,
|
|
525
|
+
DriverState &state,
|
|
526
|
+
ViewEntry &entry) {
|
|
527
|
+
if (!state.animation.has_value()) return;
|
|
528
|
+
if (!state.animation->started) {
|
|
529
|
+
if (entryDisplayable(entry)) startLatchedAnimation(driverId, state);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (entryDisplayable(entry)) {
|
|
534
|
+
if (entry.participation != ViewParticipation::Active) {
|
|
535
|
+
entry.participation = ViewParticipation::Active;
|
|
536
|
+
deliverToView(entry, state.animation->current);
|
|
537
|
+
}
|
|
538
|
+
} else if (entry.participation == ViewParticipation::Active) {
|
|
539
|
+
entry.participation = ViewParticipation::Suspended;
|
|
540
|
+
}
|
|
541
|
+
relatchIfNoDisplayable(driverId, state);
|
|
542
|
+
}
|
|
543
|
+
|
|
467
544
|
void advance(uint64_t driverId, DriverState &state, double now) {
|
|
468
545
|
ActiveAnimation &animation = *state.animation;
|
|
469
546
|
|
|
@@ -490,13 +567,15 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
490
567
|
animation.startedAtS = anchorStartTime(animation.startedAtS, now);
|
|
491
568
|
animation.lastFrameS = animation.startedAtS;
|
|
492
569
|
animation.frameClockAnchored = true;
|
|
493
|
-
// wallStartedAtS deliberately keeps the wall axis (re-latch remainder).
|
|
494
570
|
}
|
|
495
571
|
|
|
496
572
|
bool done = false;
|
|
497
573
|
if (animation.kind == AnimationKind::Spring) {
|
|
498
574
|
done = advanceSpring(animation, now);
|
|
499
575
|
} else {
|
|
576
|
+
// This is the frame whose presentation is delivered below. Lifecycle
|
|
577
|
+
// re-latch must trim from this timestamp, never from a later wall sample.
|
|
578
|
+
animation.lastFrameS = now;
|
|
500
579
|
const double fraction =
|
|
501
580
|
timingFraction(now, animation.startedAtS, animation.durationS);
|
|
502
581
|
if (animation.kind == AnimationKind::Keyframes) {
|
|
@@ -518,7 +597,13 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
518
597
|
|
|
519
598
|
if (done) {
|
|
520
599
|
const int32_t animationId = animation.id;
|
|
521
|
-
|
|
600
|
+
bool finished = animation.finished;
|
|
601
|
+
for (const ViewEntry &entry : state.views) {
|
|
602
|
+
if (entry.participation == ViewParticipation::Suspended) {
|
|
603
|
+
finished = false;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
522
607
|
const Presentation target = animation.target;
|
|
523
608
|
clearActiveAnimation(driverId, state);
|
|
524
609
|
state.latest = target;
|
|
@@ -638,12 +723,8 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
638
723
|
state.latest = current;
|
|
639
724
|
state.hasLatest = true;
|
|
640
725
|
state.ownership = Ownership::Interactive;
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// with the last real sample lets a grab-and-instant-refling inherit
|
|
644
|
-
// bounded recent motion instead of launching dead; an unchanged value
|
|
645
|
-
// dedupes to a no-op and ages out through the staleness guard.
|
|
646
|
-
recordVelocitySample(state.samples, toChannels(current), nowSeconds());
|
|
726
|
+
// The frozen frame came from native animation movement, not an interactive
|
|
727
|
+
// write. Do not let it become a later `initialVelocity: inherit` sample.
|
|
647
728
|
applyToViews(state, current);
|
|
648
729
|
emitCompletion(driverId, animationId, false);
|
|
649
730
|
return current;
|
|
@@ -809,6 +890,11 @@ void destroyDriver(uint64_t driverId) {
|
|
|
809
890
|
auto &state = iterator->second;
|
|
810
891
|
state.ownership = Ownership::Interactive;
|
|
811
892
|
finishActive(driverId, state, false);
|
|
893
|
+
// A destroyed driver's interaction history must not seed a revived
|
|
894
|
+
// incarnation: the revival seed would otherwise pair with these samples and
|
|
895
|
+
// refresh the staleness clock with motion no finger produced. (iOS clears
|
|
896
|
+
// its per-view histories at the same point.)
|
|
897
|
+
state.samples = VelocitySampleHistory{};
|
|
812
898
|
if (state.views.empty()) {
|
|
813
899
|
registry().erase(iterator);
|
|
814
900
|
} else {
|
|
@@ -822,12 +908,6 @@ void destroyDriver(uint64_t driverId) {
|
|
|
822
908
|
|
|
823
909
|
// --- Android view lifecycle ----------------------------------------------
|
|
824
910
|
|
|
825
|
-
bool JSmoothClipView::isViewAttachedToWindow() const {
|
|
826
|
-
static const auto method =
|
|
827
|
-
javaClassStatic()->getMethod<jboolean()>("isAttachedToWindow");
|
|
828
|
-
return method(self());
|
|
829
|
-
}
|
|
830
|
-
|
|
831
911
|
void JSmoothClipView::applyClip(const Presentation &presentation) const {
|
|
832
912
|
static const auto method =
|
|
833
913
|
javaClassStatic()
|
|
@@ -871,7 +951,8 @@ void registerViewAndroid(
|
|
|
871
951
|
Presentation initialPresentation,
|
|
872
952
|
double density,
|
|
873
953
|
double hostWidthPx,
|
|
874
|
-
double hostHeightPx
|
|
954
|
+
double hostHeightPx,
|
|
955
|
+
bool lifecycleVisible) {
|
|
875
956
|
auto &state = registry()[driverId];
|
|
876
957
|
state.destroyed = false;
|
|
877
958
|
if (!state.hasLatest) {
|
|
@@ -887,32 +968,24 @@ void registerViewAndroid(
|
|
|
887
968
|
existing.density = density;
|
|
888
969
|
existing.hostWidthPx = hostWidthPx;
|
|
889
970
|
existing.hostHeightPx = hostHeightPx;
|
|
971
|
+
existing.lifecycleVisible = lifecycleVisible;
|
|
890
972
|
deliverToView(existing, visible);
|
|
891
|
-
|
|
892
|
-
entryDisplayable(existing)) {
|
|
893
|
-
startLatchedAnimation(driverId, state);
|
|
894
|
-
}
|
|
973
|
+
reconcileViewDisplayability(driverId, state, existing);
|
|
895
974
|
return;
|
|
896
975
|
}
|
|
897
976
|
}
|
|
898
977
|
ViewEntry entry{
|
|
899
|
-
facebook::jni::make_global(view),
|
|
978
|
+
facebook::jni::make_global(view),
|
|
979
|
+
density,
|
|
980
|
+
hostWidthPx,
|
|
981
|
+
hostHeightPx,
|
|
982
|
+
lifecycleVisible,
|
|
983
|
+
ViewParticipation::Deferred};
|
|
900
984
|
// `visible` above already delivered animation->current (= start) to the
|
|
901
985
|
// registering view — the correct first frame for a latched animation.
|
|
902
986
|
deliverToView(entry, visible);
|
|
903
987
|
state.views.push_back(std::move(entry));
|
|
904
|
-
|
|
905
|
-
entryDisplayable(state.views.back())) {
|
|
906
|
-
// Start the latched animation: rebase the clock so the first frame
|
|
907
|
-
// integrates from now, not from the pre-mount animateTo call. Gated on
|
|
908
|
-
// displayability — a mount-time registration from a detached subtree
|
|
909
|
-
// holds the latch until window attach / first host geometry.
|
|
910
|
-
// Note this function has no isOnMainThread() guard (Kotlin calls it from
|
|
911
|
-
// the view's main-thread attach path); scheduleFrame() is itself
|
|
912
|
-
// main-thread-gated, so an off-main register cannot bind the
|
|
913
|
-
// choreographer to the wrong thread.
|
|
914
|
-
startLatchedAnimation(driverId, state);
|
|
915
|
-
}
|
|
988
|
+
reconcileViewDisplayability(driverId, state, state.views.back());
|
|
916
989
|
}
|
|
917
990
|
|
|
918
991
|
void setViewHostGeometryAndroid(
|
|
@@ -937,29 +1010,24 @@ void setViewHostGeometryAndroid(
|
|
|
937
1010
|
state.animation.has_value() ? state.animation->current
|
|
938
1011
|
: state.latest);
|
|
939
1012
|
}
|
|
940
|
-
|
|
941
|
-
entryDisplayable(entry)) {
|
|
942
|
-
// First real host geometry made this view displayable.
|
|
943
|
-
startLatchedAnimation(driverId, state);
|
|
944
|
-
}
|
|
1013
|
+
reconcileViewDisplayability(driverId, state, entry);
|
|
945
1014
|
return;
|
|
946
1015
|
}
|
|
947
1016
|
}
|
|
948
1017
|
|
|
949
|
-
void
|
|
1018
|
+
void setViewLifecycleVisibilityAndroid(
|
|
950
1019
|
uint64_t driverId,
|
|
951
|
-
alias_ref<JSmoothClipView> view
|
|
1020
|
+
alias_ref<JSmoothClipView> view,
|
|
1021
|
+
bool lifecycleVisible) {
|
|
952
1022
|
if (!isOnMainThread()) return;
|
|
953
1023
|
auto iterator = registry().find(driverId);
|
|
954
1024
|
if (iterator == registry().end()) return;
|
|
955
1025
|
auto &state = iterator->second;
|
|
956
|
-
if (!state.animation.has_value() || state.animation->started) return;
|
|
957
1026
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
958
1027
|
for (auto &entry : state.views) {
|
|
959
1028
|
if (!env->IsSameObject(entry.view.get(), view.get())) continue;
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
1029
|
+
entry.lifecycleVisible = lifecycleVisible;
|
|
1030
|
+
reconcileViewDisplayability(driverId, state, entry);
|
|
963
1031
|
return;
|
|
964
1032
|
}
|
|
965
1033
|
}
|
|
@@ -971,9 +1039,12 @@ void unregisterViewAndroid(
|
|
|
971
1039
|
if (iterator == registry().end()) return;
|
|
972
1040
|
auto &state = iterator->second;
|
|
973
1041
|
bool removed = false;
|
|
1042
|
+
bool removedParticipant = false;
|
|
974
1043
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
975
1044
|
for (auto view_it = state.views.begin(); view_it != state.views.end();) {
|
|
976
1045
|
if (env->IsSameObject(view_it->view.get(), view.get())) {
|
|
1046
|
+
removedParticipant = removedParticipant ||
|
|
1047
|
+
view_it->participation != ViewParticipation::Deferred;
|
|
977
1048
|
view_it = state.views.erase(view_it);
|
|
978
1049
|
removed = true;
|
|
979
1050
|
} else {
|
|
@@ -981,93 +1052,24 @@ void unregisterViewAndroid(
|
|
|
981
1052
|
}
|
|
982
1053
|
}
|
|
983
1054
|
if (removed && state.animation.has_value()) {
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
|
|
988
|
-
if (state.animation->started) state.animation->finished = false;
|
|
1055
|
+
// Only a host that actually joined (or is unresolved after suspension)
|
|
1056
|
+
// affects completion. A detached/unsized registration that stayed deferred
|
|
1057
|
+
// never installed or rendered this animation.
|
|
1058
|
+
if (removedParticipant) state.animation->finished = false;
|
|
989
1059
|
if (!isOnMainThread()) {
|
|
990
1060
|
// Blast-radius reduction, NOT a synchronization fix — be clear about
|
|
991
1061
|
// which. Both call sites (SmoothClipViewManager.onAfterUpdateTransaction
|
|
992
1062
|
// and onDropViewInstance) are Fabric mount operations, so this branch is
|
|
993
|
-
// unreachable in supported usage; the erase above and
|
|
1063
|
+
// unreachable in supported usage; the erase above and participant-based
|
|
994
1064
|
// `finished` write are still unsynchronized if that ever stops holding. The erase
|
|
995
1065
|
// cannot be skipped — dropping it would leak this view's global_ref for
|
|
996
1066
|
// the process lifetime — but everything below can be, and should be: it
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
// A held latch stays pending; the next displayable registration or
|
|
1004
|
-
// attach starts it (or destroyDriver cancels it).
|
|
1005
|
-
} else if (!anyDisplayableView(state)) {
|
|
1006
|
-
auto &animation = *state.animation;
|
|
1007
|
-
// wallStartedAtS, not startedAtS: post-anchor the latter lives on the
|
|
1008
|
-
// shifted frame axis and would overstate elapsed by the first
|
|
1009
|
-
// callback's dispatch latency, shortening the resumed remainder and
|
|
1010
|
-
// advancing the keyframe rebase past the rendered position.
|
|
1011
|
-
const double elapsedS = nowSeconds() - animation.wallStartedAtS;
|
|
1012
|
-
const double remainingS = std::max(0.0, animation.durationS - elapsedS);
|
|
1013
|
-
const bool canResume = !state.destroyed &&
|
|
1014
|
-
(animation.kind == AnimationKind::Spring || remainingS > 0);
|
|
1015
|
-
if (!canResume) {
|
|
1016
|
-
// The animation ends here; release ownership so a later remount's
|
|
1017
|
-
// interactive updates are not dropped.
|
|
1018
|
-
state.ownership = Ownership::Interactive;
|
|
1019
|
-
finishActive(driverId, state, false);
|
|
1020
|
-
} else {
|
|
1021
|
-
// The last displayable host left mid-flight. Registered detached or
|
|
1022
|
-
// unsized peers do not keep the clock running invisibly.
|
|
1023
|
-
// Completing here would leave `latest` at the target and statically
|
|
1024
|
-
// snap any re-registering host straight to it. Re-latch instead:
|
|
1025
|
-
// freeze the remaining animation at its current geometry so the
|
|
1026
|
-
// next displayable host resumes it with the true remaining time.
|
|
1027
|
-
// destroyDriver cancels a latch that never finds a host, so the
|
|
1028
|
-
// completion cannot hang.
|
|
1029
|
-
if (animation.kind == AnimationKind::Timing) {
|
|
1030
|
-
const double progress = animation.durationS <= 0
|
|
1031
|
-
? 1.0
|
|
1032
|
-
: clamp01(elapsedS / animation.durationS);
|
|
1033
|
-
const TimingRemainder remainder =
|
|
1034
|
-
timingRemainder(animation.timing, progress);
|
|
1035
|
-
// Keep the frozen start, cutoff, and shortened curve on one exact
|
|
1036
|
-
// phase. Reusing the old easing here would restart its slope.
|
|
1037
|
-
animation.current = interpolate(
|
|
1038
|
-
animation.start, animation.target, remainder.easedProgress);
|
|
1039
|
-
animation.timing = remainder.animation;
|
|
1040
|
-
animation.durationS = animation.timing.durationMs / 1000.0;
|
|
1041
|
-
} else if (animation.kind == AnimationKind::Keyframes) {
|
|
1042
|
-
const double progress = animation.durationS <= 0
|
|
1043
|
-
? 1.0
|
|
1044
|
-
: clamp01(elapsedS / animation.durationS);
|
|
1045
|
-
std::vector<Keyframe> remaining;
|
|
1046
|
-
remaining.push_back({0, animation.current});
|
|
1047
|
-
for (const Keyframe &frame : animation.keyframes.frames()) {
|
|
1048
|
-
if (frame.offset <= progress) continue;
|
|
1049
|
-
remaining.push_back(
|
|
1050
|
-
{(frame.offset - progress) / (1 - progress),
|
|
1051
|
-
frame.presentation});
|
|
1052
|
-
}
|
|
1053
|
-
if (remaining.size() == 1) {
|
|
1054
|
-
remaining.push_back({1, animation.target});
|
|
1055
|
-
}
|
|
1056
|
-
// reset() re-solves the tangents for the pruned curve and rewinds
|
|
1057
|
-
// the segment cursor; the resumed run must not inherit either.
|
|
1058
|
-
animation.keyframes.reset(std::move(remaining));
|
|
1059
|
-
animation.durationS = remainingS;
|
|
1060
|
-
}
|
|
1061
|
-
animation.start = animation.current;
|
|
1062
|
-
animation.started = false;
|
|
1063
|
-
// A held latch must not tick: leave animatingDrivers() without
|
|
1064
|
-
// resetting the animation (the eager-removal rule the choreographer
|
|
1065
|
-
// loop depends on).
|
|
1066
|
-
auto &active = animatingDrivers();
|
|
1067
|
-
active.erase(
|
|
1068
|
-
std::remove(active.begin(), active.end(), driverId),
|
|
1069
|
-
active.end());
|
|
1070
|
-
}
|
|
1067
|
+
// mutates animatingDrivers(), the vector the choreographer callback walks
|
|
1068
|
+
// on the main thread. Record the unfinished flag and leave the animation
|
|
1069
|
+
// for the next main-thread event (attach, host geometry, destroy) to
|
|
1070
|
+
// resolve.
|
|
1071
|
+
} else {
|
|
1072
|
+
relatchIfNoDisplayable(driverId, state);
|
|
1071
1073
|
}
|
|
1072
1074
|
}
|
|
1073
1075
|
if (state.destroyed && state.views.empty()) {
|