react-native-smooth-clip-view 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -21
- package/android/CMakeLists.txt +0 -1
- package/android/src/main/cpp/SmoothClipAndroid.h +5 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +28 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +192 -191
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +20 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +105 -34
- package/cpp/SmoothClipAnimationCurve.h +468 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +27 -5
- package/ios/SmoothClipRegistry.mm +246 -97
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +73 -16
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/drivers.native.js +98 -29
- package/lib/module/drivers.native.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/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/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverTypes.ts +3 -2
- package/src/drivers.native.ts +132 -25
|
@@ -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>
|
|
@@ -26,6 +27,20 @@ using facebook::jni::global_ref;
|
|
|
26
27
|
enum class Ownership { Interactive, Native };
|
|
27
28
|
enum class AnimationKind { Timing, Spring, Keyframes };
|
|
28
29
|
|
|
30
|
+
// Load-bearing invariant, not just a convenience clock: advance()'s frame-clock
|
|
31
|
+
// anchor compares this value ABSOLUTELY against a choreographer frame stamp
|
|
32
|
+
// (std::min), so the two must share an epoch. They do — bionic's steady_clock,
|
|
33
|
+
// System.nanoTime() behind Choreographer#frameTimeNanos, and the
|
|
34
|
+
// SystemClock.uptimeMillis() Reanimated stamps its own animations with are all
|
|
35
|
+
// CLOCK_MONOTONIC since boot. (The anchor this replaced subtracted two samples
|
|
36
|
+
// of this clock and was epoch-independent; the min() is not.)
|
|
37
|
+
//
|
|
38
|
+
// Breaking that invariant fails silently and asymmetrically. A frame axis
|
|
39
|
+
// running BEHIND the wall axis degrades quietly: min() always adopts the frame
|
|
40
|
+
// stamp and animations lose only their sub-frame start offset. A frame axis
|
|
41
|
+
// running AHEAD is fatal: min() always keeps startedAtS, the first fraction
|
|
42
|
+
// (frame - startedAtS)/duration clamps to 1, and every animation completes on
|
|
43
|
+
// its first frame.
|
|
29
44
|
double nowSeconds() {
|
|
30
45
|
return std::chrono::duration<double>(
|
|
31
46
|
std::chrono::steady_clock::now().time_since_epoch())
|
|
@@ -40,26 +55,34 @@ struct ActiveAnimation {
|
|
|
40
55
|
Presentation current{{0, 0, 0, 0, 0}, 0, 0};
|
|
41
56
|
TimingAnimation timing{};
|
|
42
57
|
SpringAnimation spring{};
|
|
43
|
-
|
|
58
|
+
// Owns the keyframes and their precomputed monotone-cubic tangents; the
|
|
59
|
+
// tangent solve happens on reset(), never per frame.
|
|
60
|
+
KeyframeCurve keyframes;
|
|
44
61
|
double durationS = 0;
|
|
45
62
|
double startedAtS = 0;
|
|
46
|
-
// Wall-clock twin of startedAtS: stamped together with it, NEVER
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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.
|
|
50
67
|
double wallStartedAtS = 0;
|
|
51
68
|
// Integrated spring state (channels: x, y, width, height, radius, tx, ty).
|
|
52
69
|
std::array<double, 7> springPosition{};
|
|
53
70
|
std::array<double, 7> springVelocity{};
|
|
54
71
|
double lastFrameS = 0;
|
|
55
72
|
bool finished = true;
|
|
56
|
-
// False while the animation is latched
|
|
57
|
-
//
|
|
58
|
-
//
|
|
73
|
+
// False while the animation is latched, and held out of animatingDrivers()
|
|
74
|
+
// so it cannot tick. Two ways in: built before any host view could display
|
|
75
|
+
// (pre-registration, or a detached/unsized host), or re-latched by
|
|
76
|
+
// unregisterViewAndroid when the last DISPLAYABLE host left mid-flight.
|
|
77
|
+
// Started by whichever of registerViewAndroid, setViewHostGeometryAndroid or
|
|
78
|
+
// viewBecameDisplayableAndroid first sees a displayable entry; each rebases
|
|
79
|
+
// the clock through startLatchedAnimation.
|
|
59
80
|
bool started = false;
|
|
60
81
|
// False until the first advance() translates the wall-clock start stamp
|
|
61
82
|
// onto the choreographer frame-time axis (elapsed-preserving). Every
|
|
62
|
-
// startedAtS stamp clears it so a re-latched resume re-anchors too
|
|
83
|
+
// startedAtS stamp clears it so a re-latched resume re-anchors too — except
|
|
84
|
+
// a start that carried the JS-captured Reanimated stamp, which arrives
|
|
85
|
+
// pre-anchored (see startAnimation) and must not be min()'d again.
|
|
63
86
|
bool frameClockAnchored = false;
|
|
64
87
|
};
|
|
65
88
|
|
|
@@ -79,7 +102,6 @@ struct DriverState {
|
|
|
79
102
|
Ownership ownership = Ownership::Interactive;
|
|
80
103
|
std::vector<ViewEntry> views;
|
|
81
104
|
std::optional<ActiveAnimation> animation;
|
|
82
|
-
int32_t nextAnimationId = 0;
|
|
83
105
|
// Set by destroyDriver while views are still registered (StrictMode effect
|
|
84
106
|
// replay, hosts mounted in another subtree). The entry is erased when the
|
|
85
107
|
// last view leaves and revived by a take-ownership setPresentation.
|
|
@@ -131,7 +153,7 @@ struct JLooper : facebook::jni::JavaClass<JLooper> {
|
|
|
131
153
|
std::atomic<pid_t> gMainThreadId{0};
|
|
132
154
|
|
|
133
155
|
// The registry, the frame loop, and every view mutation are main-thread only:
|
|
134
|
-
//
|
|
156
|
+
// Choreographer.getInstance() is per-thread, and binding the loop to the JS
|
|
135
157
|
// thread would race Fabric mounting and mutate views off-main. The worklets
|
|
136
158
|
// UI runtime executes on the main thread, so supported usage always passes;
|
|
137
159
|
// direct off-main calls fail defined instead of racing.
|
|
@@ -155,72 +177,47 @@ bool isOnMainThread() {
|
|
|
155
177
|
|
|
156
178
|
// --- Frame loop -----------------------------------------------------------
|
|
157
179
|
|
|
180
|
+
// fbjni handle for the Kotlin frame-loop bridge. The loop posts through the
|
|
181
|
+
// Java Choreographer (CALLBACK_ANIMATION) rather than the NDK AChoreographer:
|
|
182
|
+
// the NDK instance is a separate frame source on the main Looper whose
|
|
183
|
+
// ordering against the Java Choreographer's traversal (draw) phase is
|
|
184
|
+
// undefined, so advances landing after the traversal presented one vsync
|
|
185
|
+
// late — a randomly flipping ±1-frame phase between the clip and Reanimated
|
|
186
|
+
// content. Posting into the same doFrame pass as Reanimated makes every
|
|
187
|
+
// advance precede the draw by construction. It also retires the 32-bit
|
|
188
|
+
// truncated-timestamp fallback: Java frameTimeNanos is a jlong everywhere.
|
|
189
|
+
//
|
|
190
|
+
// doCallbacks() extracts by wall-now rather than by frame time, so a post
|
|
191
|
+
// issued from an earlier phase of a frame already in flight (CALLBACK_INPUT —
|
|
192
|
+
// a gesture end that starts an animation) runs in that same doFrame instead
|
|
193
|
+
// of the next one. Harmless (the fraction is ~0 and the write dedupes in
|
|
194
|
+
// Kotlin) and one frame cheaper at the handoff than the NDK path allowed.
|
|
195
|
+
struct JSmoothClipBindings : facebook::jni::JavaClass<JSmoothClipBindings> {
|
|
196
|
+
static constexpr auto kJavaDescriptor =
|
|
197
|
+
"Lcom/smoothclipview/SmoothClipBindings;";
|
|
198
|
+
};
|
|
199
|
+
|
|
158
200
|
bool gFrameScheduled = false;
|
|
159
201
|
void onFrameImpl(double now);
|
|
160
|
-
void onFrameLegacy(long frameTimeNanos, void *data);
|
|
161
202
|
|
|
162
203
|
void scheduleFrame() {
|
|
163
204
|
if (gFrameScheduled) return;
|
|
164
205
|
// Defensive: never bind the per-thread choreographer to a non-main thread.
|
|
165
206
|
if (!isOnMainThread()) return;
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
static const auto scheduleMethod =
|
|
208
|
+
JSmoothClipBindings::javaClassStatic()->getStaticMethod<void()>(
|
|
209
|
+
"scheduleFrame");
|
|
210
|
+
scheduleMethod(JSmoothClipBindings::javaClassStatic());
|
|
211
|
+
// Set only after a successful post: if the up-call ever threw with the flag
|
|
212
|
+
// already true, the loop would be dead until process restart.
|
|
168
213
|
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
214
|
}
|
|
177
215
|
|
|
178
216
|
// --- 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
|
-
}
|
|
217
|
+
//
|
|
218
|
+
// clamp01, toChannels/fromChannels, interpolate, the frame-clock anchor and the
|
|
219
|
+
// keyframe curve now live in cpp/SmoothClipAnimationCurve.h so a test binary can
|
|
220
|
+
// reach them without linking fbjni; ios/tests pins their behavior.
|
|
224
221
|
|
|
225
222
|
// Springs settle below these per-channel thresholds (DIP and DIP/s).
|
|
226
223
|
constexpr double kSpringSettleDisplacement = 0.05;
|
|
@@ -228,13 +225,6 @@ constexpr double kSpringSettleVelocity = 1.0;
|
|
|
228
225
|
// Settle-based termination is backed by a hard cap so a pathological
|
|
229
226
|
// configuration cannot run the frame loop forever.
|
|
230
227
|
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
228
|
|
|
239
229
|
// Semi-implicit Euler per channel, substepped: one frame-sized step is only
|
|
240
230
|
// stable for omega*dt < 2, and accepted stiffness/mass ratios exceed that at
|
|
@@ -285,37 +275,6 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
|
|
|
285
275
|
return settled || now - animation.startedAtS >= kSpringMaxDurationS;
|
|
286
276
|
}
|
|
287
277
|
|
|
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
278
|
bool systemAnimatorsEnabled() {
|
|
320
279
|
static const auto valueAnimatorClass =
|
|
321
280
|
facebook::jni::findClassStatic("android/animation/ValueAnimator");
|
|
@@ -339,14 +298,6 @@ void emitCompletion(uint64_t driverId, int32_t animationId, bool finished) {
|
|
|
339
298
|
}
|
|
340
299
|
}
|
|
341
300
|
|
|
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
301
|
// Terminal per-frame delivery: scale DIP -> px and normalize against the
|
|
351
302
|
// pushed host metrics in C++, so the JNI call carries final pixel floats and
|
|
352
303
|
// the Kotlin side reduces to field stores + invalidateOutline().
|
|
@@ -470,11 +421,25 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
470
421
|
int32_t startAnimation(
|
|
471
422
|
uint64_t driverId,
|
|
472
423
|
DriverState &state,
|
|
473
|
-
ActiveAnimation animation
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
424
|
+
ActiveAnimation animation,
|
|
425
|
+
double startedAtHintS) {
|
|
426
|
+
// With a JS-captured hint the start stamp is Reanimated's own
|
|
427
|
+
// (`__frameTimestamp || _getAnimationTimestamp()`, read in the worklet that
|
|
428
|
+
// issued this call), already on the axis min() only approximates — so the
|
|
429
|
+
// animation arrives pre-anchored and advance() must not min() it against
|
|
430
|
+
// the dispatching frame, which for a CALLBACK_INPUT start is EARLIER than
|
|
431
|
+
// the call and would re-open the intra-frame lead the hint closes. Without
|
|
432
|
+
// a hint (NaN: latch-less legacy callers, tests, iOS ignoring the field)
|
|
433
|
+
// this reduces exactly to the old nowSeconds() + min() anchor path.
|
|
434
|
+
const double wallNow = nowSeconds();
|
|
435
|
+
const StartStamp stamp = resolveStartStamp(startedAtHintS, wallNow);
|
|
436
|
+
animation.startedAtS = stamp.startedAtS;
|
|
437
|
+
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
|
+
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
|
|
@@ -503,35 +468,29 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
503
468
|
ActiveAnimation &animation = *state.animation;
|
|
504
469
|
|
|
505
470
|
if (!animation.frameClockAnchored) {
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
// the
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
471
|
+
// This is the stamp-less/latch fallback. Worklet-issued animations carry
|
|
472
|
+
// Reanimated's exact `__frameTimestamp || _getAnimationTimestamp()` value
|
|
473
|
+
// and arrive pre-anchored, so they skip this approximation — including the
|
|
474
|
+
// CALLBACK_INPUT case where the current frame stamp predates the call.
|
|
475
|
+
// Here startedAtS/lastFrameS hold nowSeconds() sampled at native latch
|
|
476
|
+
// attach or by an older caller, while `now` is the frame's vsync stamp on
|
|
477
|
+
// the same CLOCK_MONOTONIC timebase. Taking the earlier stamp prevents a
|
|
478
|
+
// mid-frame native start from duplicating fraction zero.
|
|
479
|
+
//
|
|
480
|
+
// Deliberately NOT a wall-elapsed rebase (`now - (nowSeconds() -
|
|
481
|
+
// startedAtS)`): that bakes THIS frame's dispatch latency into the curve
|
|
482
|
+
// for the animation's whole duration, de-phasing the clip from parallel
|
|
483
|
+
// Reanimated content by however much main-thread work happened to run
|
|
484
|
+
// before us on frame one — worst on the first frame of a heavy
|
|
485
|
+
// transition, which is exactly when it is sampled. Catching up after a
|
|
486
|
+
// stall needs no help here either: Choreographer#doFrame snaps a late
|
|
487
|
+
// frameTimeNanos forward to within one frame interval of now (jitter
|
|
488
|
+
// correction), for every CALLBACK_ANIMATION client at once, so the clip
|
|
489
|
+
// and the content stall and catch up together.
|
|
490
|
+
animation.startedAtS = anchorStartTime(animation.startedAtS, now);
|
|
517
491
|
animation.lastFrameS = animation.startedAtS;
|
|
518
492
|
animation.frameClockAnchored = true;
|
|
519
|
-
// wallStartedAtS deliberately keeps the wall axis
|
|
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;
|
|
493
|
+
// wallStartedAtS deliberately keeps the wall axis (re-latch remainder).
|
|
535
494
|
}
|
|
536
495
|
|
|
537
496
|
bool done = false;
|
|
@@ -539,11 +498,9 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
539
498
|
done = advanceSpring(animation, now);
|
|
540
499
|
} else {
|
|
541
500
|
const double fraction =
|
|
542
|
-
animation.durationS
|
|
543
|
-
? 1.0
|
|
544
|
-
: clamp01((now - animation.startedAtS) / animation.durationS);
|
|
501
|
+
timingFraction(now, animation.startedAtS, animation.durationS);
|
|
545
502
|
if (animation.kind == AnimationKind::Keyframes) {
|
|
546
|
-
animation.current =
|
|
503
|
+
animation.current = animation.keyframes.evaluate(fraction);
|
|
547
504
|
} else {
|
|
548
505
|
const double eased = cubicBezier(
|
|
549
506
|
animation.timing.controlPoint1X,
|
|
@@ -593,26 +550,27 @@ void onFrameImpl(double now) {
|
|
|
593
550
|
if (!animatingDrivers().empty()) scheduleFrame();
|
|
594
551
|
}
|
|
595
552
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
553
|
+
} // namespace
|
|
554
|
+
|
|
555
|
+
// Advances the frame loop; reached from SmoothClipBindings.nativeOnFrame
|
|
556
|
+
// inside Choreographer#doFrame. Nothing may unwind back into doFrame — fbjni
|
|
557
|
+
// would rethrow it as a Java exception and take down the main thread. Clear
|
|
558
|
+
// any pending JNI exception, keep the loop scheduled, and let the next frame
|
|
559
|
+
// re-scan.
|
|
560
|
+
void onFrameAndroid(double frameTimeS) {
|
|
599
561
|
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
|
|
562
|
+
onFrameImpl(frameTimeS);
|
|
607
563
|
} catch (...) {
|
|
608
564
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
609
565
|
if (env != nullptr && env->ExceptionCheck()) env->ExceptionClear();
|
|
610
|
-
|
|
566
|
+
try {
|
|
567
|
+
scheduleFrame();
|
|
568
|
+
} catch (...) {
|
|
569
|
+
// gFrameScheduled stays false; the next animation start re-schedules.
|
|
570
|
+
}
|
|
611
571
|
}
|
|
612
572
|
}
|
|
613
573
|
|
|
614
|
-
} // namespace
|
|
615
|
-
|
|
616
574
|
// --- Public smoothclip:: interface (shared with iOS via SmoothClipRegistry.h)
|
|
617
575
|
|
|
618
576
|
void setCompletionCallback(const void *owner, CompletionCallback callback) {
|
|
@@ -631,7 +589,8 @@ void clearCompletionCallback(const void *owner) {
|
|
|
631
589
|
void setPresentation(
|
|
632
590
|
uint64_t driverId,
|
|
633
591
|
Presentation presentation,
|
|
634
|
-
bool takeOwnership
|
|
592
|
+
bool takeOwnership,
|
|
593
|
+
bool overridePendingAnimation) {
|
|
635
594
|
if (!isOnMainThread()) return;
|
|
636
595
|
auto iterator = registry().find(driverId);
|
|
637
596
|
if (!takeOwnership) {
|
|
@@ -647,13 +606,11 @@ void setPresentation(
|
|
|
647
606
|
if (!takeOwnership && state.ownership != Ownership::Interactive) return;
|
|
648
607
|
if (takeOwnership) {
|
|
649
608
|
state.destroyed = false;
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// cancel it explicitly first.
|
|
656
|
-
if (state.animation.has_value() && !state.animation->started) {
|
|
609
|
+
// Passive seeds and public setters must not displace newer pending intent.
|
|
610
|
+
// The fused animation.from write is different: it is the caller's newest,
|
|
611
|
+
// authoritative start and explicitly opts in to cancelling the old latch.
|
|
612
|
+
if (state.animation.has_value() && !state.animation->started &&
|
|
613
|
+
!overridePendingAnimation) {
|
|
657
614
|
return;
|
|
658
615
|
}
|
|
659
616
|
finishActive(driverId, state, false);
|
|
@@ -699,10 +656,15 @@ int32_t animateTiming(
|
|
|
699
656
|
TimingAnimation animation) {
|
|
700
657
|
if (!isOnMainThread()) return 0;
|
|
701
658
|
auto iterator = registry().find(driverId);
|
|
702
|
-
if (iterator == registry().end())
|
|
659
|
+
if (iterator == registry().end()) {
|
|
660
|
+
if (!start.hasInteractiveStart) return 0;
|
|
661
|
+
iterator = registry().try_emplace(driverId).first;
|
|
662
|
+
iterator->second.latest = start.interactiveStart;
|
|
663
|
+
iterator->second.hasLatest = true;
|
|
664
|
+
}
|
|
703
665
|
auto &state = iterator->second;
|
|
704
666
|
state.destroyed = false;
|
|
705
|
-
const int32_t animationId = allocateAnimationId(
|
|
667
|
+
const int32_t animationId = allocateAnimationId();
|
|
706
668
|
const Presentation resolvedStart =
|
|
707
669
|
prepareAnimation(driverId, state, start, presentation);
|
|
708
670
|
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
@@ -717,7 +679,7 @@ int32_t animateTiming(
|
|
|
717
679
|
active.start = resolvedStart;
|
|
718
680
|
active.target = presentation;
|
|
719
681
|
active.durationS = animation.durationMs / 1000.0;
|
|
720
|
-
return startAnimation(driverId, state, std::move(active));
|
|
682
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
721
683
|
}
|
|
722
684
|
|
|
723
685
|
int32_t animateSpring(
|
|
@@ -727,10 +689,15 @@ int32_t animateSpring(
|
|
|
727
689
|
SpringAnimation animation) {
|
|
728
690
|
if (!isOnMainThread()) return 0;
|
|
729
691
|
auto iterator = registry().find(driverId);
|
|
730
|
-
if (iterator == registry().end())
|
|
692
|
+
if (iterator == registry().end()) {
|
|
693
|
+
if (!start.hasInteractiveStart) return 0;
|
|
694
|
+
iterator = registry().try_emplace(driverId).first;
|
|
695
|
+
iterator->second.latest = start.interactiveStart;
|
|
696
|
+
iterator->second.hasLatest = true;
|
|
697
|
+
}
|
|
731
698
|
auto &state = iterator->second;
|
|
732
699
|
state.destroyed = false;
|
|
733
|
-
const int32_t animationId = allocateAnimationId(
|
|
700
|
+
const int32_t animationId = allocateAnimationId();
|
|
734
701
|
// Scalar velocity along the current-to-target trajectory; each channel is
|
|
735
702
|
// seeded with velocity·displacement to match the iOS CASpringAnimation
|
|
736
703
|
// per-keypath behavior.
|
|
@@ -760,7 +727,7 @@ int32_t animateSpring(
|
|
|
760
727
|
active.springVelocity[index] =
|
|
761
728
|
velocity * (targetChannels[index] - startChannels[index]);
|
|
762
729
|
}
|
|
763
|
-
return startAnimation(driverId, state, std::move(active));
|
|
730
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
764
731
|
}
|
|
765
732
|
|
|
766
733
|
int32_t animateKeyframes(
|
|
@@ -772,10 +739,15 @@ int32_t animateKeyframes(
|
|
|
772
739
|
int32_t reduceMotion) {
|
|
773
740
|
if (!isOnMainThread()) return 0;
|
|
774
741
|
auto iterator = registry().find(driverId);
|
|
775
|
-
if (iterator == registry().end())
|
|
742
|
+
if (iterator == registry().end()) {
|
|
743
|
+
if (!start.hasInteractiveStart) return 0;
|
|
744
|
+
iterator = registry().try_emplace(driverId).first;
|
|
745
|
+
iterator->second.latest = start.interactiveStart;
|
|
746
|
+
iterator->second.hasLatest = true;
|
|
747
|
+
}
|
|
776
748
|
auto &state = iterator->second;
|
|
777
749
|
state.destroyed = false;
|
|
778
|
-
const int32_t animationId = allocateAnimationId(
|
|
750
|
+
const int32_t animationId = allocateAnimationId();
|
|
779
751
|
const Presentation resolvedStart =
|
|
780
752
|
prepareAnimation(driverId, state, start, presentation);
|
|
781
753
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0 ||
|
|
@@ -787,18 +759,18 @@ int32_t animateKeyframes(
|
|
|
787
759
|
ActiveAnimation active;
|
|
788
760
|
active.id = animationId;
|
|
789
761
|
active.kind = AnimationKind::Keyframes;
|
|
790
|
-
active.keyframes
|
|
762
|
+
active.keyframes.reset(std::move(keyframes));
|
|
791
763
|
active.start = resolvedStart;
|
|
792
764
|
active.target = presentation;
|
|
793
765
|
active.durationS = durationMs / 1000.0;
|
|
794
|
-
return startAnimation(driverId, state, std::move(active));
|
|
766
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
795
767
|
}
|
|
796
768
|
|
|
797
769
|
int32_t rejectAnimation(uint64_t driverId) {
|
|
798
770
|
if (!isOnMainThread()) return 0;
|
|
799
771
|
auto iterator = registry().find(driverId);
|
|
800
772
|
if (iterator == registry().end()) return 0;
|
|
801
|
-
const int32_t animationId = allocateAnimationId(
|
|
773
|
+
const int32_t animationId = allocateAnimationId();
|
|
802
774
|
emitCompletion(driverId, animationId, false);
|
|
803
775
|
return animationId;
|
|
804
776
|
}
|
|
@@ -1009,12 +981,28 @@ void unregisterViewAndroid(
|
|
|
1009
981
|
}
|
|
1010
982
|
}
|
|
1011
983
|
if (removed && state.animation.has_value()) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
984
|
+
// Participant-gated like iOS: only a started run has rendered frames a
|
|
985
|
+
// departing view can leave unfinished. A held latch has installed nothing,
|
|
986
|
+
// so a view leaving while it is held must not poison the eventual natural
|
|
987
|
+
// completion with finished:false.
|
|
988
|
+
if (state.animation->started) state.animation->finished = false;
|
|
989
|
+
if (!isOnMainThread()) {
|
|
990
|
+
// Blast-radius reduction, NOT a synchronization fix — be clear about
|
|
991
|
+
// which. Both call sites (SmoothClipViewManager.onAfterUpdateTransaction
|
|
992
|
+
// and onDropViewInstance) are Fabric mount operations, so this branch is
|
|
993
|
+
// unreachable in supported usage; the erase above and the started-gated
|
|
994
|
+
// `finished` write are still unsynchronized if that ever stops holding. The erase
|
|
995
|
+
// cannot be skipped — dropping it would leak this view's global_ref for
|
|
996
|
+
// the process lifetime — but everything below can be, and should be: it
|
|
997
|
+
// crosses JNI to read view attachment (anyDisplayableView) and mutates
|
|
998
|
+
// animatingDrivers(), the vector the choreographer callback walks on the
|
|
999
|
+
// main thread. Record the unfinished flag (started runs only) and leave
|
|
1000
|
+
// the animation for the next main-thread event (attach, host geometry,
|
|
1001
|
+
// destroy) to resolve.
|
|
1002
|
+
} else if (!state.animation->started) {
|
|
1003
|
+
// A held latch stays pending; the next displayable registration or
|
|
1004
|
+
// attach starts it (or destroyDriver cancels it).
|
|
1005
|
+
} else if (!anyDisplayableView(state)) {
|
|
1018
1006
|
auto &animation = *state.animation;
|
|
1019
1007
|
// wallStartedAtS, not startedAtS: post-anchor the latter lives on the
|
|
1020
1008
|
// shifted frame axis and would overstate elapsed by the first
|
|
@@ -1030,7 +1018,8 @@ void unregisterViewAndroid(
|
|
|
1030
1018
|
state.ownership = Ownership::Interactive;
|
|
1031
1019
|
finishActive(driverId, state, false);
|
|
1032
1020
|
} else {
|
|
1033
|
-
// The last
|
|
1021
|
+
// The last displayable host left mid-flight. Registered detached or
|
|
1022
|
+
// unsized peers do not keep the clock running invisibly.
|
|
1034
1023
|
// Completing here would leave `latest` at the target and statically
|
|
1035
1024
|
// snap any re-registering host straight to it. Re-latch instead:
|
|
1036
1025
|
// freeze the remaining animation at its current geometry so the
|
|
@@ -1038,14 +1027,24 @@ void unregisterViewAndroid(
|
|
|
1038
1027
|
// destroyDriver cancels a latch that never finds a host, so the
|
|
1039
1028
|
// completion cannot hang.
|
|
1040
1029
|
if (animation.kind == AnimationKind::Timing) {
|
|
1041
|
-
animation.durationS
|
|
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;
|
|
1042
1041
|
} else if (animation.kind == AnimationKind::Keyframes) {
|
|
1043
1042
|
const double progress = animation.durationS <= 0
|
|
1044
1043
|
? 1.0
|
|
1045
1044
|
: clamp01(elapsedS / animation.durationS);
|
|
1046
1045
|
std::vector<Keyframe> remaining;
|
|
1047
1046
|
remaining.push_back({0, animation.current});
|
|
1048
|
-
for (const Keyframe &frame : animation.keyframes) {
|
|
1047
|
+
for (const Keyframe &frame : animation.keyframes.frames()) {
|
|
1049
1048
|
if (frame.offset <= progress) continue;
|
|
1050
1049
|
remaining.push_back(
|
|
1051
1050
|
{(frame.offset - progress) / (1 - progress),
|
|
@@ -1054,7 +1053,9 @@ void unregisterViewAndroid(
|
|
|
1054
1053
|
if (remaining.size() == 1) {
|
|
1055
1054
|
remaining.push_back({1, animation.target});
|
|
1056
1055
|
}
|
|
1057
|
-
|
|
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));
|
|
1058
1059
|
animation.durationS = remainingS;
|
|
1059
1060
|
}
|
|
1060
1061
|
animation.start = animation.current;
|