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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#include "SmoothClipAndroid.h"
|
|
2
|
+
#include "SmoothClipAnimationId.h"
|
|
3
|
+
#include "SmoothClipAnimationCurve.h"
|
|
2
4
|
#include "SmoothClipVelocityTracker.h"
|
|
3
5
|
|
|
4
|
-
#include <android/choreographer.h>
|
|
5
6
|
#include <fbjni/fbjni.h>
|
|
6
7
|
|
|
7
8
|
#include <algorithm>
|
|
@@ -25,7 +26,22 @@ using facebook::jni::global_ref;
|
|
|
25
26
|
|
|
26
27
|
enum class Ownership { Interactive, Native };
|
|
27
28
|
enum class AnimationKind { Timing, Spring, Keyframes };
|
|
28
|
-
|
|
29
|
+
enum class ViewParticipation { Deferred, Active, Suspended };
|
|
30
|
+
|
|
31
|
+
// Load-bearing invariant, not just a convenience clock: advance()'s frame-clock
|
|
32
|
+
// anchor compares this value ABSOLUTELY against a choreographer frame stamp
|
|
33
|
+
// (std::min), so the two must share an epoch. They do — bionic's steady_clock,
|
|
34
|
+
// System.nanoTime() behind Choreographer#frameTimeNanos, and the
|
|
35
|
+
// SystemClock.uptimeMillis() Reanimated stamps its own animations with are all
|
|
36
|
+
// CLOCK_MONOTONIC since boot. (The anchor this replaced subtracted two samples
|
|
37
|
+
// of this clock and was epoch-independent; the min() is not.)
|
|
38
|
+
//
|
|
39
|
+
// Breaking that invariant fails silently and asymmetrically. A frame axis
|
|
40
|
+
// running BEHIND the wall axis degrades quietly: min() always adopts the frame
|
|
41
|
+
// stamp and animations lose only their sub-frame start offset. A frame axis
|
|
42
|
+
// running AHEAD is fatal: min() always keeps startedAtS, the first fraction
|
|
43
|
+
// (frame - startedAtS)/duration clamps to 1, and every animation completes on
|
|
44
|
+
// its first frame.
|
|
29
45
|
double nowSeconds() {
|
|
30
46
|
return std::chrono::duration<double>(
|
|
31
47
|
std::chrono::steady_clock::now().time_since_epoch())
|
|
@@ -40,26 +56,28 @@ struct ActiveAnimation {
|
|
|
40
56
|
Presentation current{{0, 0, 0, 0, 0}, 0, 0};
|
|
41
57
|
TimingAnimation timing{};
|
|
42
58
|
SpringAnimation spring{};
|
|
43
|
-
|
|
59
|
+
// Owns the keyframes and their precomputed monotone-cubic tangents; the
|
|
60
|
+
// tangent solve happens on reset(), never per frame.
|
|
61
|
+
KeyframeCurve keyframes;
|
|
44
62
|
double durationS = 0;
|
|
45
63
|
double startedAtS = 0;
|
|
46
|
-
// Wall-clock twin of startedAtS: stamped together with it, NEVER rebased
|
|
47
|
-
// by the frame-clock anchor. Readers that need honest wall elapsed (the
|
|
48
|
-
// unregister re-latch remainder, the run-ahead guard) use this; the
|
|
49
|
-
// animation curve itself paces on the anchored startedAtS frame axis.
|
|
50
|
-
double wallStartedAtS = 0;
|
|
51
64
|
// Integrated spring state (channels: x, y, width, height, radius, tx, ty).
|
|
52
65
|
std::array<double, 7> springPosition{};
|
|
53
66
|
std::array<double, 7> springVelocity{};
|
|
54
67
|
double lastFrameS = 0;
|
|
55
68
|
bool finished = true;
|
|
56
|
-
// False while the animation is latched
|
|
57
|
-
//
|
|
58
|
-
//
|
|
69
|
+
// False while the animation is latched, and held out of animatingDrivers()
|
|
70
|
+
// so it cannot tick. Two ways in: built before any host view could display
|
|
71
|
+
// (pre-registration, or a detached/unsized host), or re-latched by
|
|
72
|
+
// unregisterViewAndroid when the last DISPLAYABLE host left mid-flight.
|
|
73
|
+
// Started by whichever lifecycle/geometry update first sees a displayable
|
|
74
|
+
// entry; each rebases the clock through startLatchedAnimation.
|
|
59
75
|
bool started = false;
|
|
60
76
|
// False until the first advance() translates the wall-clock start stamp
|
|
61
77
|
// onto the choreographer frame-time axis (elapsed-preserving). Every
|
|
62
|
-
// startedAtS stamp clears it so a re-latched resume re-anchors too
|
|
78
|
+
// startedAtS stamp clears it so a re-latched resume re-anchors too — except
|
|
79
|
+
// a start that carried the JS-captured Reanimated stamp, which arrives
|
|
80
|
+
// pre-anchored (see startAnimation) and must not be min()'d again.
|
|
63
81
|
bool frameClockAnchored = false;
|
|
64
82
|
};
|
|
65
83
|
|
|
@@ -71,6 +89,8 @@ struct ViewEntry {
|
|
|
71
89
|
double density = 0;
|
|
72
90
|
double hostWidthPx = 0;
|
|
73
91
|
double hostHeightPx = 0;
|
|
92
|
+
bool lifecycleVisible = false;
|
|
93
|
+
ViewParticipation participation = ViewParticipation::Deferred;
|
|
74
94
|
};
|
|
75
95
|
|
|
76
96
|
struct DriverState {
|
|
@@ -79,7 +99,6 @@ struct DriverState {
|
|
|
79
99
|
Ownership ownership = Ownership::Interactive;
|
|
80
100
|
std::vector<ViewEntry> views;
|
|
81
101
|
std::optional<ActiveAnimation> animation;
|
|
82
|
-
int32_t nextAnimationId = 0;
|
|
83
102
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
84
103
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
85
104
|
// last view leaves and revived by a take-ownership setPresentation.
|
|
@@ -131,7 +150,7 @@ struct JLooper : facebook::jni::JavaClass<JLooper> {
|
|
|
131
150
|
std::atomic<pid_t> gMainThreadId{0};
|
|
132
151
|
|
|
133
152
|
// The registry, the frame loop, and every view mutation are main-thread only:
|
|
134
|
-
//
|
|
153
|
+
// Choreographer.getInstance() is per-thread, and binding the loop to the JS
|
|
135
154
|
// thread would race Fabric mounting and mutate views off-main. The worklets
|
|
136
155
|
// UI runtime executes on the main thread, so supported usage always passes;
|
|
137
156
|
// direct off-main calls fail defined instead of racing.
|
|
@@ -155,72 +174,47 @@ bool isOnMainThread() {
|
|
|
155
174
|
|
|
156
175
|
// --- Frame loop -----------------------------------------------------------
|
|
157
176
|
|
|
177
|
+
// fbjni handle for the Kotlin frame-loop bridge. The loop posts through the
|
|
178
|
+
// Java Choreographer (CALLBACK_ANIMATION) rather than the NDK AChoreographer:
|
|
179
|
+
// the NDK instance is a separate frame source on the main Looper whose
|
|
180
|
+
// ordering against the Java Choreographer's traversal (draw) phase is
|
|
181
|
+
// undefined, so advances landing after the traversal presented one vsync
|
|
182
|
+
// late — a randomly flipping ±1-frame phase between the clip and Reanimated
|
|
183
|
+
// content. Posting into the same doFrame pass as Reanimated makes every
|
|
184
|
+
// advance precede the draw by construction. It also retires the 32-bit
|
|
185
|
+
// truncated-timestamp fallback: Java frameTimeNanos is a jlong everywhere.
|
|
186
|
+
//
|
|
187
|
+
// doCallbacks() extracts by wall-now rather than by frame time, so a post
|
|
188
|
+
// issued from an earlier phase of a frame already in flight (CALLBACK_INPUT —
|
|
189
|
+
// a gesture end that starts an animation) runs in that same doFrame instead
|
|
190
|
+
// of the next one. Harmless (the fraction is ~0 and the write dedupes in
|
|
191
|
+
// Kotlin) and one frame cheaper at the handoff than the NDK path allowed.
|
|
192
|
+
struct JSmoothClipBindings : facebook::jni::JavaClass<JSmoothClipBindings> {
|
|
193
|
+
static constexpr auto kJavaDescriptor =
|
|
194
|
+
"Lcom/smoothclipview/SmoothClipBindings;";
|
|
195
|
+
};
|
|
196
|
+
|
|
158
197
|
bool gFrameScheduled = false;
|
|
159
198
|
void onFrameImpl(double now);
|
|
160
|
-
void onFrameLegacy(long frameTimeNanos, void *data);
|
|
161
199
|
|
|
162
200
|
void scheduleFrame() {
|
|
163
201
|
if (gFrameScheduled) return;
|
|
164
202
|
// Defensive: never bind the per-thread choreographer to a non-main thread.
|
|
165
203
|
if (!isOnMainThread()) return;
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
static const auto scheduleMethod =
|
|
205
|
+
JSmoothClipBindings::javaClassStatic()->getStaticMethod<void()>(
|
|
206
|
+
"scheduleFrame");
|
|
207
|
+
scheduleMethod(JSmoothClipBindings::javaClassStatic());
|
|
208
|
+
// Set only after a successful post: if the up-call ever threw with the flag
|
|
209
|
+
// already true, the loop would be dead until process restart.
|
|
168
210
|
gFrameScheduled = true;
|
|
169
|
-
// AChoreographer_postFrameCallback64 (API 29+) only matters on 32-bit ABIs,
|
|
170
|
-
// where `long` truncates the timestamp; onFrameLegacy falls back to the
|
|
171
|
-
// clock there, so the legacy entry point covers every case.
|
|
172
|
-
#pragma clang diagnostic push
|
|
173
|
-
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
174
|
-
AChoreographer_postFrameCallback(choreographer, &onFrameLegacy, nullptr);
|
|
175
|
-
#pragma clang diagnostic pop
|
|
176
211
|
}
|
|
177
212
|
|
|
178
213
|
// --- Math (ported from SmoothClipRegistry.kt so both platforms match) -----
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
double x1,
|
|
184
|
-
double y1,
|
|
185
|
-
double x2,
|
|
186
|
-
double y2,
|
|
187
|
-
double input) {
|
|
188
|
-
double low = 0;
|
|
189
|
-
double high = 1;
|
|
190
|
-
for (int iteration = 0; iteration < 14; iteration += 1) {
|
|
191
|
-
const double t = (low + high) / 2;
|
|
192
|
-
const double inverse = 1 - t;
|
|
193
|
-
const double x = 3 * inverse * inverse * t * x1 +
|
|
194
|
-
3 * inverse * t * t * x2 + t * t * t;
|
|
195
|
-
if (x < input) {
|
|
196
|
-
low = t;
|
|
197
|
-
} else {
|
|
198
|
-
high = t;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const double t = (low + high) / 2;
|
|
202
|
-
const double inverse = 1 - t;
|
|
203
|
-
return 3 * inverse * inverse * t * y1 + 3 * inverse * t * t * y2 +
|
|
204
|
-
t * t * t;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Presentation <-> per-channel array (x, y, width, height, radius, tx, ty).
|
|
208
|
-
std::array<double, 7> toChannels(const Presentation &presentation) {
|
|
209
|
-
return {presentation.clip.x,
|
|
210
|
-
presentation.clip.y,
|
|
211
|
-
presentation.clip.width,
|
|
212
|
-
presentation.clip.height,
|
|
213
|
-
presentation.clip.radius,
|
|
214
|
-
presentation.contentTranslateX,
|
|
215
|
-
presentation.contentTranslateY};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
Presentation fromChannels(const std::array<double, 7> &channels) {
|
|
219
|
-
return Presentation{
|
|
220
|
-
{channels[0], channels[1], channels[2], channels[3], channels[4]},
|
|
221
|
-
channels[5],
|
|
222
|
-
channels[6]};
|
|
223
|
-
}
|
|
214
|
+
//
|
|
215
|
+
// clamp01, toChannels/fromChannels, interpolate, the frame-clock anchor and the
|
|
216
|
+
// keyframe curve now live in cpp/SmoothClipAnimationCurve.h so a test binary can
|
|
217
|
+
// reach them without linking fbjni; ios/tests pins their behavior.
|
|
224
218
|
|
|
225
219
|
// Springs settle below these per-channel thresholds (DIP and DIP/s).
|
|
226
220
|
constexpr double kSpringSettleDisplacement = 0.05;
|
|
@@ -228,13 +222,6 @@ constexpr double kSpringSettleVelocity = 1.0;
|
|
|
228
222
|
// Settle-based termination is backed by a hard cap so a pathological
|
|
229
223
|
// configuration cannot run the frame loop forever.
|
|
230
224
|
constexpr double kSpringMaxDurationS = 10.0;
|
|
231
|
-
// Fixed slack for the post-stall run-ahead guard in advance(). The lead it
|
|
232
|
-
// bounds is a latency DIFFERENTIAL — the anchor frame's dispatch latency
|
|
233
|
-
// minus the current frame's — so the constant is refresh-rate independent
|
|
234
|
-
// (it is not one vsync period; at 120 Hz it spans two). 17 ms keeps the
|
|
235
|
-
// guard silent through normal callback jitter while capping post-stall
|
|
236
|
-
// run-ahead at one 60 Hz frame of curve time.
|
|
237
|
-
constexpr double kMaxFrameLeadS = 0.017;
|
|
238
225
|
|
|
239
226
|
// Semi-implicit Euler per channel, substepped: one frame-sized step is only
|
|
240
227
|
// stable for omega*dt < 2, and accepted stiffness/mass ratios exceed that at
|
|
@@ -285,37 +272,6 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
|
|
|
285
272
|
return settled || now - animation.startedAtS >= kSpringMaxDurationS;
|
|
286
273
|
}
|
|
287
274
|
|
|
288
|
-
Presentation interpolate(
|
|
289
|
-
const Presentation &from,
|
|
290
|
-
const Presentation &to,
|
|
291
|
-
double progress) {
|
|
292
|
-
const auto mix = [progress](double start, double end) {
|
|
293
|
-
return start + (end - start) * progress;
|
|
294
|
-
};
|
|
295
|
-
return Presentation{
|
|
296
|
-
{mix(from.clip.x, to.clip.x),
|
|
297
|
-
mix(from.clip.y, to.clip.y),
|
|
298
|
-
mix(from.clip.width, to.clip.width),
|
|
299
|
-
mix(from.clip.height, to.clip.height),
|
|
300
|
-
mix(from.clip.radius, to.clip.radius)},
|
|
301
|
-
mix(from.contentTranslateX, to.contentTranslateX),
|
|
302
|
-
mix(from.contentTranslateY, to.contentTranslateY)};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
Presentation interpolateKeyframes(
|
|
306
|
-
const std::vector<Keyframe> &frames,
|
|
307
|
-
double progress) {
|
|
308
|
-
size_t upper = 1;
|
|
309
|
-
while (upper < frames.size() - 1 && progress > frames[upper].offset) {
|
|
310
|
-
upper += 1;
|
|
311
|
-
}
|
|
312
|
-
const Keyframe &lower = frames[upper - 1];
|
|
313
|
-
const Keyframe &higher = frames[upper];
|
|
314
|
-
const double span = higher.offset - lower.offset;
|
|
315
|
-
const double local = span <= 0 ? 1.0 : (progress - lower.offset) / span;
|
|
316
|
-
return interpolate(lower.presentation, higher.presentation, clamp01(local));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
275
|
bool systemAnimatorsEnabled() {
|
|
320
276
|
static const auto valueAnimatorClass =
|
|
321
277
|
facebook::jni::findClassStatic("android/animation/ValueAnimator");
|
|
@@ -339,14 +295,6 @@ void emitCompletion(uint64_t driverId, int32_t animationId, bool finished) {
|
|
|
339
295
|
}
|
|
340
296
|
}
|
|
341
297
|
|
|
342
|
-
int32_t allocateAnimationId(DriverState &state) {
|
|
343
|
-
state.nextAnimationId =
|
|
344
|
-
state.nextAnimationId == std::numeric_limits<int32_t>::max()
|
|
345
|
-
? 1
|
|
346
|
-
: state.nextAnimationId + 1;
|
|
347
|
-
return state.nextAnimationId;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
298
|
// Terminal per-frame delivery: scale DIP -> px and normalize against the
|
|
351
299
|
// pushed host metrics in C++, so the JNI call carries final pixel floats and
|
|
352
300
|
// the Kotlin side reduces to field stores + invalidateOutline().
|
|
@@ -398,6 +346,9 @@ std::vector<uint64_t> &animatingDrivers() {
|
|
|
398
346
|
// erase/recreate cycle would let one frame integrate the animation twice.
|
|
399
347
|
void clearActiveAnimation(uint64_t driverId, DriverState &state) {
|
|
400
348
|
state.animation.reset();
|
|
349
|
+
for (ViewEntry &entry : state.views) {
|
|
350
|
+
entry.participation = ViewParticipation::Deferred;
|
|
351
|
+
}
|
|
401
352
|
auto &active = animatingDrivers();
|
|
402
353
|
active.erase(
|
|
403
354
|
std::remove(active.begin(), active.end(), driverId), active.end());
|
|
@@ -437,7 +388,7 @@ Presentation prepareAnimation(
|
|
|
437
388
|
// no one can see (the transparentModal mount pattern).
|
|
438
389
|
bool entryDisplayable(const ViewEntry &entry) {
|
|
439
390
|
return entry.hostWidthPx > 0 && entry.hostHeightPx > 0 &&
|
|
440
|
-
entry.
|
|
391
|
+
entry.lifecycleVisible;
|
|
441
392
|
}
|
|
442
393
|
|
|
443
394
|
bool anyDisplayableView(const DriverState &state) {
|
|
@@ -454,7 +405,6 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
454
405
|
animation.started = true;
|
|
455
406
|
animation.startedAtS = nowSeconds();
|
|
456
407
|
animation.lastFrameS = animation.startedAtS;
|
|
457
|
-
animation.wallStartedAtS = animation.startedAtS;
|
|
458
408
|
// Load-bearing for re-latch resumes: unregisterViewAndroid rewrites this
|
|
459
409
|
// ActiveAnimation in place, so a stale anchor would replay the fraction-0
|
|
460
410
|
// first frame the anchor exists to remove.
|
|
@@ -463,6 +413,11 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
463
413
|
if (std::find(active.begin(), active.end(), driverId) == active.end()) {
|
|
464
414
|
active.push_back(driverId);
|
|
465
415
|
}
|
|
416
|
+
for (ViewEntry &entry : state.views) {
|
|
417
|
+
if (entryDisplayable(entry)) {
|
|
418
|
+
entry.participation = ViewParticipation::Active;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
466
421
|
applyToViews(state, animation.start);
|
|
467
422
|
scheduleFrame();
|
|
468
423
|
}
|
|
@@ -470,11 +425,21 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
470
425
|
int32_t startAnimation(
|
|
471
426
|
uint64_t driverId,
|
|
472
427
|
DriverState &state,
|
|
473
|
-
ActiveAnimation animation
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
428
|
+
ActiveAnimation animation,
|
|
429
|
+
double startedAtHintS) {
|
|
430
|
+
// With a JS-captured hint the start stamp is Reanimated's own
|
|
431
|
+
// (`__frameTimestamp || _getAnimationTimestamp()`, read in the worklet that
|
|
432
|
+
// issued this call), already on the axis min() only approximates — so the
|
|
433
|
+
// animation arrives pre-anchored and advance() must not min() it against
|
|
434
|
+
// the dispatching frame, which for a CALLBACK_INPUT start is EARLIER than
|
|
435
|
+
// the call and would re-open the intra-frame lead the hint closes. Without
|
|
436
|
+
// a hint (NaN: latch-less legacy callers, tests, iOS ignoring the field)
|
|
437
|
+
// this reduces exactly to the old nowSeconds() + min() anchor path.
|
|
438
|
+
const double wallNow = nowSeconds();
|
|
439
|
+
const StartStamp stamp = resolveStartStamp(startedAtHintS, wallNow);
|
|
440
|
+
animation.startedAtS = stamp.startedAtS;
|
|
441
|
+
animation.lastFrameS = stamp.startedAtS;
|
|
442
|
+
animation.frameClockAnchored = stamp.frameClockAnchored;
|
|
478
443
|
// current = start even while latched is load-bearing: cancelAnimation,
|
|
479
444
|
// beginInteraction, prepareAnimation's visibleBefore and registerView's
|
|
480
445
|
// visible all read animation->current, giving a never-rendered latch
|
|
@@ -482,6 +447,11 @@ int32_t startAnimation(
|
|
|
482
447
|
animation.current = animation.start;
|
|
483
448
|
animation.started = anyDisplayableView(state);
|
|
484
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
|
+
}
|
|
485
455
|
if (!state.animation->started) {
|
|
486
456
|
// Latch: no host view can display yet (animateTo raced the mount, or
|
|
487
457
|
// every host is detached/unsized). The first displayable registration,
|
|
@@ -499,51 +469,117 @@ int32_t startAnimation(
|
|
|
499
469
|
return state.animation->id;
|
|
500
470
|
}
|
|
501
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
|
+
|
|
502
544
|
void advance(uint64_t driverId, DriverState &state, double now) {
|
|
503
545
|
ActiveAnimation &animation = *state.animation;
|
|
504
546
|
|
|
505
547
|
if (!animation.frameClockAnchored) {
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
// the
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
548
|
+
// This is the stamp-less/latch fallback. Worklet-issued animations carry
|
|
549
|
+
// Reanimated's exact `__frameTimestamp || _getAnimationTimestamp()` value
|
|
550
|
+
// and arrive pre-anchored, so they skip this approximation — including the
|
|
551
|
+
// CALLBACK_INPUT case where the current frame stamp predates the call.
|
|
552
|
+
// Here startedAtS/lastFrameS hold nowSeconds() sampled at native latch
|
|
553
|
+
// attach or by an older caller, while `now` is the frame's vsync stamp on
|
|
554
|
+
// the same CLOCK_MONOTONIC timebase. Taking the earlier stamp prevents a
|
|
555
|
+
// mid-frame native start from duplicating fraction zero.
|
|
556
|
+
//
|
|
557
|
+
// Deliberately NOT a wall-elapsed rebase (`now - (nowSeconds() -
|
|
558
|
+
// startedAtS)`): that bakes THIS frame's dispatch latency into the curve
|
|
559
|
+
// for the animation's whole duration, de-phasing the clip from parallel
|
|
560
|
+
// Reanimated content by however much main-thread work happened to run
|
|
561
|
+
// before us on frame one — worst on the first frame of a heavy
|
|
562
|
+
// transition, which is exactly when it is sampled. Catching up after a
|
|
563
|
+
// stall needs no help here either: Choreographer#doFrame snaps a late
|
|
564
|
+
// frameTimeNanos forward to within one frame interval of now (jitter
|
|
565
|
+
// correction), for every CALLBACK_ANIMATION client at once, so the clip
|
|
566
|
+
// and the content stall and catch up together.
|
|
567
|
+
animation.startedAtS = anchorStartTime(animation.startedAtS, now);
|
|
517
568
|
animation.lastFrameS = animation.startedAtS;
|
|
518
569
|
animation.frameClockAnchored = true;
|
|
519
|
-
// wallStartedAtS deliberately keeps the wall axis through this rebase.
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Run-ahead guard: the anchor's one-time shift bakes the first callback's
|
|
523
|
-
// dispatch latency into the frame axis. If a stalled handoff anchored
|
|
524
|
-
// against a stale frame stamp and later stamps catch up to real time, the
|
|
525
|
-
// frame axis leads wall time by up to the stall length and the animation
|
|
526
|
-
// completes early. Re-anchor forward when the lead exceeds one vsync of
|
|
527
|
-
// slack; on calm frames the condition (first callback's latency exceeding
|
|
528
|
-
// this frame's by > 17 ms) cannot hold, so the hot path is unchanged, and
|
|
529
|
-
// the 32-bit path (frame axis == wall axis) never fires it. lastFrameS is
|
|
530
|
-
// deliberately untouched: springs pace on dt, already clamped to 64 ms.
|
|
531
|
-
const double wallElapsedNowS =
|
|
532
|
-
std::max(0.0, nowSeconds() - animation.wallStartedAtS);
|
|
533
|
-
if (now - animation.startedAtS > wallElapsedNowS + kMaxFrameLeadS) {
|
|
534
|
-
animation.startedAtS = now - wallElapsedNowS;
|
|
535
570
|
}
|
|
536
571
|
|
|
537
572
|
bool done = false;
|
|
538
573
|
if (animation.kind == AnimationKind::Spring) {
|
|
539
574
|
done = advanceSpring(animation, now);
|
|
540
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;
|
|
541
579
|
const double fraction =
|
|
542
|
-
animation.durationS
|
|
543
|
-
? 1.0
|
|
544
|
-
: clamp01((now - animation.startedAtS) / animation.durationS);
|
|
580
|
+
timingFraction(now, animation.startedAtS, animation.durationS);
|
|
545
581
|
if (animation.kind == AnimationKind::Keyframes) {
|
|
546
|
-
animation.current =
|
|
582
|
+
animation.current = animation.keyframes.evaluate(fraction);
|
|
547
583
|
} else {
|
|
548
584
|
const double eased = cubicBezier(
|
|
549
585
|
animation.timing.controlPoint1X,
|
|
@@ -561,7 +597,13 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
561
597
|
|
|
562
598
|
if (done) {
|
|
563
599
|
const int32_t animationId = animation.id;
|
|
564
|
-
|
|
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
|
+
}
|
|
565
607
|
const Presentation target = animation.target;
|
|
566
608
|
clearActiveAnimation(driverId, state);
|
|
567
609
|
state.latest = target;
|
|
@@ -593,26 +635,27 @@ void onFrameImpl(double now) {
|
|
|
593
635
|
if (!animatingDrivers().empty()) scheduleFrame();
|
|
594
636
|
}
|
|
595
637
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
638
|
+
} // namespace
|
|
639
|
+
|
|
640
|
+
// Advances the frame loop; reached from SmoothClipBindings.nativeOnFrame
|
|
641
|
+
// inside Choreographer#doFrame. Nothing may unwind back into doFrame — fbjni
|
|
642
|
+
// would rethrow it as a Java exception and take down the main thread. Clear
|
|
643
|
+
// any pending JNI exception, keep the loop scheduled, and let the next frame
|
|
644
|
+
// re-scan.
|
|
645
|
+
void onFrameAndroid(double frameTimeS) {
|
|
599
646
|
try {
|
|
600
|
-
|
|
601
|
-
onFrameImpl(static_cast<double>(frameTimeNanos) / 1e9);
|
|
602
|
-
#else
|
|
603
|
-
// 32-bit `long` truncates the nanosecond timestamp; fall back to the
|
|
604
|
-
// clock.
|
|
605
|
-
onFrameImpl(nowSeconds());
|
|
606
|
-
#endif
|
|
647
|
+
onFrameImpl(frameTimeS);
|
|
607
648
|
} catch (...) {
|
|
608
649
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
609
650
|
if (env != nullptr && env->ExceptionCheck()) env->ExceptionClear();
|
|
610
|
-
|
|
651
|
+
try {
|
|
652
|
+
scheduleFrame();
|
|
653
|
+
} catch (...) {
|
|
654
|
+
// gFrameScheduled stays false; the next animation start re-schedules.
|
|
655
|
+
}
|
|
611
656
|
}
|
|
612
657
|
}
|
|
613
658
|
|
|
614
|
-
} // namespace
|
|
615
|
-
|
|
616
659
|
// --- Public smoothclip:: interface (shared with iOS via SmoothClipRegistry.h)
|
|
617
660
|
|
|
618
661
|
void setCompletionCallback(const void *owner, CompletionCallback callback) {
|
|
@@ -631,7 +674,8 @@ void clearCompletionCallback(const void *owner) {
|
|
|
631
674
|
void setPresentation(
|
|
632
675
|
uint64_t driverId,
|
|
633
676
|
Presentation presentation,
|
|
634
|
-
bool takeOwnership
|
|
677
|
+
bool takeOwnership,
|
|
678
|
+
bool overridePendingAnimation) {
|
|
635
679
|
if (!isOnMainThread()) return;
|
|
636
680
|
auto iterator = registry().find(driverId);
|
|
637
681
|
if (!takeOwnership) {
|
|
@@ -647,13 +691,11 @@ void setPresentation(
|
|
|
647
691
|
if (!takeOwnership && state.ownership != Ownership::Interactive) return;
|
|
648
692
|
if (takeOwnership) {
|
|
649
693
|
state.destroyed = false;
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// cancel it explicitly first.
|
|
656
|
-
if (state.animation.has_value() && !state.animation->started) {
|
|
694
|
+
// Passive seeds and public setters must not displace newer pending intent.
|
|
695
|
+
// The fused animation.from write is different: it is the caller's newest,
|
|
696
|
+
// authoritative start and explicitly opts in to cancelling the old latch.
|
|
697
|
+
if (state.animation.has_value() && !state.animation->started &&
|
|
698
|
+
!overridePendingAnimation) {
|
|
657
699
|
return;
|
|
658
700
|
}
|
|
659
701
|
finishActive(driverId, state, false);
|
|
@@ -681,12 +723,8 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
681
723
|
state.latest = current;
|
|
682
724
|
state.hasLatest = true;
|
|
683
725
|
state.ownership = Ownership::Interactive;
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
// with the last real sample lets a grab-and-instant-refling inherit
|
|
687
|
-
// bounded recent motion instead of launching dead; an unchanged value
|
|
688
|
-
// dedupes to a no-op and ages out through the staleness guard.
|
|
689
|
-
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.
|
|
690
728
|
applyToViews(state, current);
|
|
691
729
|
emitCompletion(driverId, animationId, false);
|
|
692
730
|
return current;
|
|
@@ -699,10 +737,15 @@ int32_t animateTiming(
|
|
|
699
737
|
TimingAnimation animation) {
|
|
700
738
|
if (!isOnMainThread()) return 0;
|
|
701
739
|
auto iterator = registry().find(driverId);
|
|
702
|
-
if (iterator == registry().end())
|
|
740
|
+
if (iterator == registry().end()) {
|
|
741
|
+
if (!start.hasInteractiveStart) return 0;
|
|
742
|
+
iterator = registry().try_emplace(driverId).first;
|
|
743
|
+
iterator->second.latest = start.interactiveStart;
|
|
744
|
+
iterator->second.hasLatest = true;
|
|
745
|
+
}
|
|
703
746
|
auto &state = iterator->second;
|
|
704
747
|
state.destroyed = false;
|
|
705
|
-
const int32_t animationId = allocateAnimationId(
|
|
748
|
+
const int32_t animationId = allocateAnimationId();
|
|
706
749
|
const Presentation resolvedStart =
|
|
707
750
|
prepareAnimation(driverId, state, start, presentation);
|
|
708
751
|
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
@@ -717,7 +760,7 @@ int32_t animateTiming(
|
|
|
717
760
|
active.start = resolvedStart;
|
|
718
761
|
active.target = presentation;
|
|
719
762
|
active.durationS = animation.durationMs / 1000.0;
|
|
720
|
-
return startAnimation(driverId, state, std::move(active));
|
|
763
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
721
764
|
}
|
|
722
765
|
|
|
723
766
|
int32_t animateSpring(
|
|
@@ -727,10 +770,15 @@ int32_t animateSpring(
|
|
|
727
770
|
SpringAnimation animation) {
|
|
728
771
|
if (!isOnMainThread()) return 0;
|
|
729
772
|
auto iterator = registry().find(driverId);
|
|
730
|
-
if (iterator == registry().end())
|
|
773
|
+
if (iterator == registry().end()) {
|
|
774
|
+
if (!start.hasInteractiveStart) return 0;
|
|
775
|
+
iterator = registry().try_emplace(driverId).first;
|
|
776
|
+
iterator->second.latest = start.interactiveStart;
|
|
777
|
+
iterator->second.hasLatest = true;
|
|
778
|
+
}
|
|
731
779
|
auto &state = iterator->second;
|
|
732
780
|
state.destroyed = false;
|
|
733
|
-
const int32_t animationId = allocateAnimationId(
|
|
781
|
+
const int32_t animationId = allocateAnimationId();
|
|
734
782
|
// Scalar velocity along the current-to-target trajectory; each channel is
|
|
735
783
|
// seeded with velocity·displacement to match the iOS CASpringAnimation
|
|
736
784
|
// per-keypath behavior.
|
|
@@ -760,7 +808,7 @@ int32_t animateSpring(
|
|
|
760
808
|
active.springVelocity[index] =
|
|
761
809
|
velocity * (targetChannels[index] - startChannels[index]);
|
|
762
810
|
}
|
|
763
|
-
return startAnimation(driverId, state, std::move(active));
|
|
811
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
764
812
|
}
|
|
765
813
|
|
|
766
814
|
int32_t animateKeyframes(
|
|
@@ -772,10 +820,15 @@ int32_t animateKeyframes(
|
|
|
772
820
|
int32_t reduceMotion) {
|
|
773
821
|
if (!isOnMainThread()) return 0;
|
|
774
822
|
auto iterator = registry().find(driverId);
|
|
775
|
-
if (iterator == registry().end())
|
|
823
|
+
if (iterator == registry().end()) {
|
|
824
|
+
if (!start.hasInteractiveStart) return 0;
|
|
825
|
+
iterator = registry().try_emplace(driverId).first;
|
|
826
|
+
iterator->second.latest = start.interactiveStart;
|
|
827
|
+
iterator->second.hasLatest = true;
|
|
828
|
+
}
|
|
776
829
|
auto &state = iterator->second;
|
|
777
830
|
state.destroyed = false;
|
|
778
|
-
const int32_t animationId = allocateAnimationId(
|
|
831
|
+
const int32_t animationId = allocateAnimationId();
|
|
779
832
|
const Presentation resolvedStart =
|
|
780
833
|
prepareAnimation(driverId, state, start, presentation);
|
|
781
834
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0 ||
|
|
@@ -787,18 +840,18 @@ int32_t animateKeyframes(
|
|
|
787
840
|
ActiveAnimation active;
|
|
788
841
|
active.id = animationId;
|
|
789
842
|
active.kind = AnimationKind::Keyframes;
|
|
790
|
-
active.keyframes
|
|
843
|
+
active.keyframes.reset(std::move(keyframes));
|
|
791
844
|
active.start = resolvedStart;
|
|
792
845
|
active.target = presentation;
|
|
793
846
|
active.durationS = durationMs / 1000.0;
|
|
794
|
-
return startAnimation(driverId, state, std::move(active));
|
|
847
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
795
848
|
}
|
|
796
849
|
|
|
797
850
|
int32_t rejectAnimation(uint64_t driverId) {
|
|
798
851
|
if (!isOnMainThread()) return 0;
|
|
799
852
|
auto iterator = registry().find(driverId);
|
|
800
853
|
if (iterator == registry().end()) return 0;
|
|
801
|
-
const int32_t animationId = allocateAnimationId(
|
|
854
|
+
const int32_t animationId = allocateAnimationId();
|
|
802
855
|
emitCompletion(driverId, animationId, false);
|
|
803
856
|
return animationId;
|
|
804
857
|
}
|
|
@@ -837,6 +890,11 @@ void destroyDriver(uint64_t driverId) {
|
|
|
837
890
|
auto &state = iterator->second;
|
|
838
891
|
state.ownership = Ownership::Interactive;
|
|
839
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{};
|
|
840
898
|
if (state.views.empty()) {
|
|
841
899
|
registry().erase(iterator);
|
|
842
900
|
} else {
|
|
@@ -850,12 +908,6 @@ void destroyDriver(uint64_t driverId) {
|
|
|
850
908
|
|
|
851
909
|
// --- Android view lifecycle ----------------------------------------------
|
|
852
910
|
|
|
853
|
-
bool JSmoothClipView::isViewAttachedToWindow() const {
|
|
854
|
-
static const auto method =
|
|
855
|
-
javaClassStatic()->getMethod<jboolean()>("isAttachedToWindow");
|
|
856
|
-
return method(self());
|
|
857
|
-
}
|
|
858
|
-
|
|
859
911
|
void JSmoothClipView::applyClip(const Presentation &presentation) const {
|
|
860
912
|
static const auto method =
|
|
861
913
|
javaClassStatic()
|
|
@@ -899,7 +951,8 @@ void registerViewAndroid(
|
|
|
899
951
|
Presentation initialPresentation,
|
|
900
952
|
double density,
|
|
901
953
|
double hostWidthPx,
|
|
902
|
-
double hostHeightPx
|
|
954
|
+
double hostHeightPx,
|
|
955
|
+
bool lifecycleVisible) {
|
|
903
956
|
auto &state = registry()[driverId];
|
|
904
957
|
state.destroyed = false;
|
|
905
958
|
if (!state.hasLatest) {
|
|
@@ -915,32 +968,24 @@ void registerViewAndroid(
|
|
|
915
968
|
existing.density = density;
|
|
916
969
|
existing.hostWidthPx = hostWidthPx;
|
|
917
970
|
existing.hostHeightPx = hostHeightPx;
|
|
971
|
+
existing.lifecycleVisible = lifecycleVisible;
|
|
918
972
|
deliverToView(existing, visible);
|
|
919
|
-
|
|
920
|
-
entryDisplayable(existing)) {
|
|
921
|
-
startLatchedAnimation(driverId, state);
|
|
922
|
-
}
|
|
973
|
+
reconcileViewDisplayability(driverId, state, existing);
|
|
923
974
|
return;
|
|
924
975
|
}
|
|
925
976
|
}
|
|
926
977
|
ViewEntry entry{
|
|
927
|
-
facebook::jni::make_global(view),
|
|
978
|
+
facebook::jni::make_global(view),
|
|
979
|
+
density,
|
|
980
|
+
hostWidthPx,
|
|
981
|
+
hostHeightPx,
|
|
982
|
+
lifecycleVisible,
|
|
983
|
+
ViewParticipation::Deferred};
|
|
928
984
|
// `visible` above already delivered animation->current (= start) to the
|
|
929
985
|
// registering view — the correct first frame for a latched animation.
|
|
930
986
|
deliverToView(entry, visible);
|
|
931
987
|
state.views.push_back(std::move(entry));
|
|
932
|
-
|
|
933
|
-
entryDisplayable(state.views.back())) {
|
|
934
|
-
// Start the latched animation: rebase the clock so the first frame
|
|
935
|
-
// integrates from now, not from the pre-mount animateTo call. Gated on
|
|
936
|
-
// displayability — a mount-time registration from a detached subtree
|
|
937
|
-
// holds the latch until window attach / first host geometry.
|
|
938
|
-
// Note this function has no isOnMainThread() guard (Kotlin calls it from
|
|
939
|
-
// the view's main-thread attach path); scheduleFrame() is itself
|
|
940
|
-
// main-thread-gated, so an off-main register cannot bind the
|
|
941
|
-
// choreographer to the wrong thread.
|
|
942
|
-
startLatchedAnimation(driverId, state);
|
|
943
|
-
}
|
|
988
|
+
reconcileViewDisplayability(driverId, state, state.views.back());
|
|
944
989
|
}
|
|
945
990
|
|
|
946
991
|
void setViewHostGeometryAndroid(
|
|
@@ -965,29 +1010,24 @@ void setViewHostGeometryAndroid(
|
|
|
965
1010
|
state.animation.has_value() ? state.animation->current
|
|
966
1011
|
: state.latest);
|
|
967
1012
|
}
|
|
968
|
-
|
|
969
|
-
entryDisplayable(entry)) {
|
|
970
|
-
// First real host geometry made this view displayable.
|
|
971
|
-
startLatchedAnimation(driverId, state);
|
|
972
|
-
}
|
|
1013
|
+
reconcileViewDisplayability(driverId, state, entry);
|
|
973
1014
|
return;
|
|
974
1015
|
}
|
|
975
1016
|
}
|
|
976
1017
|
|
|
977
|
-
void
|
|
1018
|
+
void setViewLifecycleVisibilityAndroid(
|
|
978
1019
|
uint64_t driverId,
|
|
979
|
-
alias_ref<JSmoothClipView> view
|
|
1020
|
+
alias_ref<JSmoothClipView> view,
|
|
1021
|
+
bool lifecycleVisible) {
|
|
980
1022
|
if (!isOnMainThread()) return;
|
|
981
1023
|
auto iterator = registry().find(driverId);
|
|
982
1024
|
if (iterator == registry().end()) return;
|
|
983
1025
|
auto &state = iterator->second;
|
|
984
|
-
if (!state.animation.has_value() || state.animation->started) return;
|
|
985
1026
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
986
1027
|
for (auto &entry : state.views) {
|
|
987
1028
|
if (!env->IsSameObject(entry.view.get(), view.get())) continue;
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
1029
|
+
entry.lifecycleVisible = lifecycleVisible;
|
|
1030
|
+
reconcileViewDisplayability(driverId, state, entry);
|
|
991
1031
|
return;
|
|
992
1032
|
}
|
|
993
1033
|
}
|
|
@@ -999,9 +1039,12 @@ void unregisterViewAndroid(
|
|
|
999
1039
|
if (iterator == registry().end()) return;
|
|
1000
1040
|
auto &state = iterator->second;
|
|
1001
1041
|
bool removed = false;
|
|
1042
|
+
bool removedParticipant = false;
|
|
1002
1043
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
1003
1044
|
for (auto view_it = state.views.begin(); view_it != state.views.end();) {
|
|
1004
1045
|
if (env->IsSameObject(view_it->view.get(), view.get())) {
|
|
1046
|
+
removedParticipant = removedParticipant ||
|
|
1047
|
+
view_it->participation != ViewParticipation::Deferred;
|
|
1005
1048
|
view_it = state.views.erase(view_it);
|
|
1006
1049
|
removed = true;
|
|
1007
1050
|
} else {
|
|
@@ -1009,64 +1052,24 @@ void unregisterViewAndroid(
|
|
|
1009
1052
|
}
|
|
1010
1053
|
}
|
|
1011
1054
|
if (removed && state.animation.has_value()) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
state.ownership = Ownership::Interactive;
|
|
1031
|
-
finishActive(driverId, state, false);
|
|
1032
|
-
} else {
|
|
1033
|
-
// The last rendering host left mid-flight (remount, screen swap).
|
|
1034
|
-
// Completing here would leave `latest` at the target and statically
|
|
1035
|
-
// snap any re-registering host straight to it. Re-latch instead:
|
|
1036
|
-
// freeze the remaining animation at its current geometry so the
|
|
1037
|
-
// next displayable host resumes it with the true remaining time.
|
|
1038
|
-
// destroyDriver cancels a latch that never finds a host, so the
|
|
1039
|
-
// completion cannot hang.
|
|
1040
|
-
if (animation.kind == AnimationKind::Timing) {
|
|
1041
|
-
animation.durationS = remainingS;
|
|
1042
|
-
} else if (animation.kind == AnimationKind::Keyframes) {
|
|
1043
|
-
const double progress = animation.durationS <= 0
|
|
1044
|
-
? 1.0
|
|
1045
|
-
: clamp01(elapsedS / animation.durationS);
|
|
1046
|
-
std::vector<Keyframe> remaining;
|
|
1047
|
-
remaining.push_back({0, animation.current});
|
|
1048
|
-
for (const Keyframe &frame : animation.keyframes) {
|
|
1049
|
-
if (frame.offset <= progress) continue;
|
|
1050
|
-
remaining.push_back(
|
|
1051
|
-
{(frame.offset - progress) / (1 - progress),
|
|
1052
|
-
frame.presentation});
|
|
1053
|
-
}
|
|
1054
|
-
if (remaining.size() == 1) {
|
|
1055
|
-
remaining.push_back({1, animation.target});
|
|
1056
|
-
}
|
|
1057
|
-
animation.keyframes = std::move(remaining);
|
|
1058
|
-
animation.durationS = remainingS;
|
|
1059
|
-
}
|
|
1060
|
-
animation.start = animation.current;
|
|
1061
|
-
animation.started = false;
|
|
1062
|
-
// A held latch must not tick: leave animatingDrivers() without
|
|
1063
|
-
// resetting the animation (the eager-removal rule the choreographer
|
|
1064
|
-
// loop depends on).
|
|
1065
|
-
auto &active = animatingDrivers();
|
|
1066
|
-
active.erase(
|
|
1067
|
-
std::remove(active.begin(), active.end(), driverId),
|
|
1068
|
-
active.end());
|
|
1069
|
-
}
|
|
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;
|
|
1059
|
+
if (!isOnMainThread()) {
|
|
1060
|
+
// Blast-radius reduction, NOT a synchronization fix — be clear about
|
|
1061
|
+
// which. Both call sites (SmoothClipViewManager.onAfterUpdateTransaction
|
|
1062
|
+
// and onDropViewInstance) are Fabric mount operations, so this branch is
|
|
1063
|
+
// unreachable in supported usage; the erase above and participant-based
|
|
1064
|
+
// `finished` write are still unsynchronized if that ever stops holding. The erase
|
|
1065
|
+
// cannot be skipped — dropping it would leak this view's global_ref for
|
|
1066
|
+
// the process lifetime — but everything below can be, and should be: it
|
|
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);
|
|
1070
1073
|
}
|
|
1071
1074
|
}
|
|
1072
1075
|
if (state.destroyed && state.views.empty()) {
|