react-native-smooth-clip-view 0.2.4 → 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 +62 -25
- 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 +198 -189
- 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 +32 -15
- 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.android.js +3 -1
- package/lib/module/drivers.android.js.map +1 -1
- package/lib/module/drivers.ios.js +3 -391
- package/lib/module/drivers.ios.js.map +1 -1
- package/lib/module/drivers.native.js +470 -0
- package/lib/module/drivers.native.js.map +1 -0
- 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 +6 -3
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.android.d.ts +2 -2
- package/lib/typescript/src/drivers.android.d.ts.map +1 -1
- package/lib/typescript/src/drivers.ios.d.ts +2 -4
- package/lib/typescript/src/drivers.ios.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts +5 -0
- package/lib/typescript/src/drivers.native.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverTypes.ts +6 -3
- package/src/drivers.android.ts +4 -2
- package/src/drivers.ios.ts +4 -687
- package/src/drivers.native.ts +805 -0
|
@@ -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,10 +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
|
-
// One vsync of slack for the post-stall run-ahead guard in advance():
|
|
232
|
-
// frame-axis elapsed may lead wall elapsed by at most this much before the
|
|
233
|
-
// animation is re-anchored onto honest wall time.
|
|
234
|
-
constexpr double kMaxFrameLeadS = 0.017;
|
|
235
228
|
|
|
236
229
|
// Semi-implicit Euler per channel, substepped: one frame-sized step is only
|
|
237
230
|
// stable for omega*dt < 2, and accepted stiffness/mass ratios exceed that at
|
|
@@ -282,37 +275,6 @@ bool advanceSpring(ActiveAnimation &animation, double now) {
|
|
|
282
275
|
return settled || now - animation.startedAtS >= kSpringMaxDurationS;
|
|
283
276
|
}
|
|
284
277
|
|
|
285
|
-
Presentation interpolate(
|
|
286
|
-
const Presentation &from,
|
|
287
|
-
const Presentation &to,
|
|
288
|
-
double progress) {
|
|
289
|
-
const auto mix = [progress](double start, double end) {
|
|
290
|
-
return start + (end - start) * progress;
|
|
291
|
-
};
|
|
292
|
-
return Presentation{
|
|
293
|
-
{mix(from.clip.x, to.clip.x),
|
|
294
|
-
mix(from.clip.y, to.clip.y),
|
|
295
|
-
mix(from.clip.width, to.clip.width),
|
|
296
|
-
mix(from.clip.height, to.clip.height),
|
|
297
|
-
mix(from.clip.radius, to.clip.radius)},
|
|
298
|
-
mix(from.contentTranslateX, to.contentTranslateX),
|
|
299
|
-
mix(from.contentTranslateY, to.contentTranslateY)};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
Presentation interpolateKeyframes(
|
|
303
|
-
const std::vector<Keyframe> &frames,
|
|
304
|
-
double progress) {
|
|
305
|
-
size_t upper = 1;
|
|
306
|
-
while (upper < frames.size() - 1 && progress > frames[upper].offset) {
|
|
307
|
-
upper += 1;
|
|
308
|
-
}
|
|
309
|
-
const Keyframe &lower = frames[upper - 1];
|
|
310
|
-
const Keyframe &higher = frames[upper];
|
|
311
|
-
const double span = higher.offset - lower.offset;
|
|
312
|
-
const double local = span <= 0 ? 1.0 : (progress - lower.offset) / span;
|
|
313
|
-
return interpolate(lower.presentation, higher.presentation, clamp01(local));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
278
|
bool systemAnimatorsEnabled() {
|
|
317
279
|
static const auto valueAnimatorClass =
|
|
318
280
|
facebook::jni::findClassStatic("android/animation/ValueAnimator");
|
|
@@ -336,14 +298,6 @@ void emitCompletion(uint64_t driverId, int32_t animationId, bool finished) {
|
|
|
336
298
|
}
|
|
337
299
|
}
|
|
338
300
|
|
|
339
|
-
int32_t allocateAnimationId(DriverState &state) {
|
|
340
|
-
state.nextAnimationId =
|
|
341
|
-
state.nextAnimationId == std::numeric_limits<int32_t>::max()
|
|
342
|
-
? 1
|
|
343
|
-
: state.nextAnimationId + 1;
|
|
344
|
-
return state.nextAnimationId;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
301
|
// Terminal per-frame delivery: scale DIP -> px and normalize against the
|
|
348
302
|
// pushed host metrics in C++, so the JNI call carries final pixel floats and
|
|
349
303
|
// the Kotlin side reduces to field stores + invalidateOutline().
|
|
@@ -467,11 +421,25 @@ void startLatchedAnimation(uint64_t driverId, DriverState &state) {
|
|
|
467
421
|
int32_t startAnimation(
|
|
468
422
|
uint64_t driverId,
|
|
469
423
|
DriverState &state,
|
|
470
|
-
ActiveAnimation animation
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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;
|
|
475
443
|
// current = start even while latched is load-bearing: cancelAnimation,
|
|
476
444
|
// beginInteraction, prepareAnimation's visibleBefore and registerView's
|
|
477
445
|
// visible all read animation->current, giving a never-rendered latch
|
|
@@ -500,35 +468,29 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
500
468
|
ActiveAnimation &animation = *state.animation;
|
|
501
469
|
|
|
502
470
|
if (!animation.frameClockAnchored) {
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// the
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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);
|
|
514
491
|
animation.lastFrameS = animation.startedAtS;
|
|
515
492
|
animation.frameClockAnchored = true;
|
|
516
|
-
// wallStartedAtS deliberately keeps the wall axis
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Run-ahead guard: the anchor's one-time shift bakes the first callback's
|
|
520
|
-
// dispatch latency into the frame axis. If a stalled handoff anchored
|
|
521
|
-
// against a stale frame stamp and later stamps catch up to real time, the
|
|
522
|
-
// frame axis leads wall time by up to the stall length and the animation
|
|
523
|
-
// completes early. Re-anchor forward when the lead exceeds one vsync of
|
|
524
|
-
// slack; on calm frames the condition (first callback's latency exceeding
|
|
525
|
-
// this frame's by > 17 ms) cannot hold, so the hot path is unchanged, and
|
|
526
|
-
// the 32-bit path (frame axis == wall axis) never fires it. lastFrameS is
|
|
527
|
-
// deliberately untouched: springs pace on dt, already clamped to 64 ms.
|
|
528
|
-
const double wallElapsedNowS =
|
|
529
|
-
std::max(0.0, nowSeconds() - animation.wallStartedAtS);
|
|
530
|
-
if (now - animation.startedAtS > wallElapsedNowS + kMaxFrameLeadS) {
|
|
531
|
-
animation.startedAtS = now - wallElapsedNowS;
|
|
493
|
+
// wallStartedAtS deliberately keeps the wall axis (re-latch remainder).
|
|
532
494
|
}
|
|
533
495
|
|
|
534
496
|
bool done = false;
|
|
@@ -536,11 +498,9 @@ void advance(uint64_t driverId, DriverState &state, double now) {
|
|
|
536
498
|
done = advanceSpring(animation, now);
|
|
537
499
|
} else {
|
|
538
500
|
const double fraction =
|
|
539
|
-
animation.durationS
|
|
540
|
-
? 1.0
|
|
541
|
-
: clamp01((now - animation.startedAtS) / animation.durationS);
|
|
501
|
+
timingFraction(now, animation.startedAtS, animation.durationS);
|
|
542
502
|
if (animation.kind == AnimationKind::Keyframes) {
|
|
543
|
-
animation.current =
|
|
503
|
+
animation.current = animation.keyframes.evaluate(fraction);
|
|
544
504
|
} else {
|
|
545
505
|
const double eased = cubicBezier(
|
|
546
506
|
animation.timing.controlPoint1X,
|
|
@@ -590,26 +550,27 @@ void onFrameImpl(double now) {
|
|
|
590
550
|
if (!animatingDrivers().empty()) scheduleFrame();
|
|
591
551
|
}
|
|
592
552
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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) {
|
|
596
561
|
try {
|
|
597
|
-
|
|
598
|
-
onFrameImpl(static_cast<double>(frameTimeNanos) / 1e9);
|
|
599
|
-
#else
|
|
600
|
-
// 32-bit `long` truncates the nanosecond timestamp; fall back to the
|
|
601
|
-
// clock.
|
|
602
|
-
onFrameImpl(nowSeconds());
|
|
603
|
-
#endif
|
|
562
|
+
onFrameImpl(frameTimeS);
|
|
604
563
|
} catch (...) {
|
|
605
564
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
606
565
|
if (env != nullptr && env->ExceptionCheck()) env->ExceptionClear();
|
|
607
|
-
|
|
566
|
+
try {
|
|
567
|
+
scheduleFrame();
|
|
568
|
+
} catch (...) {
|
|
569
|
+
// gFrameScheduled stays false; the next animation start re-schedules.
|
|
570
|
+
}
|
|
608
571
|
}
|
|
609
572
|
}
|
|
610
573
|
|
|
611
|
-
} // namespace
|
|
612
|
-
|
|
613
574
|
// --- Public smoothclip:: interface (shared with iOS via SmoothClipRegistry.h)
|
|
614
575
|
|
|
615
576
|
void setCompletionCallback(const void *owner, CompletionCallback callback) {
|
|
@@ -628,7 +589,8 @@ void clearCompletionCallback(const void *owner) {
|
|
|
628
589
|
void setPresentation(
|
|
629
590
|
uint64_t driverId,
|
|
630
591
|
Presentation presentation,
|
|
631
|
-
bool takeOwnership
|
|
592
|
+
bool takeOwnership,
|
|
593
|
+
bool overridePendingAnimation) {
|
|
632
594
|
if (!isOnMainThread()) return;
|
|
633
595
|
auto iterator = registry().find(driverId);
|
|
634
596
|
if (!takeOwnership) {
|
|
@@ -644,13 +606,11 @@ void setPresentation(
|
|
|
644
606
|
if (!takeOwnership && state.ownership != Ownership::Interactive) return;
|
|
645
607
|
if (takeOwnership) {
|
|
646
608
|
state.destroyed = false;
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// cancel it explicitly first.
|
|
653
|
-
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) {
|
|
654
614
|
return;
|
|
655
615
|
}
|
|
656
616
|
finishActive(driverId, state, false);
|
|
@@ -678,7 +638,12 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
678
638
|
state.latest = current;
|
|
679
639
|
state.hasLatest = true;
|
|
680
640
|
state.ownership = Ownership::Interactive;
|
|
681
|
-
|
|
641
|
+
// Plain record, not a reset — iOS parity (smoothClipFreezePresentation
|
|
642
|
+
// records the frozen value). Pairing the frozen mid-flight presentation
|
|
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());
|
|
682
647
|
applyToViews(state, current);
|
|
683
648
|
emitCompletion(driverId, animationId, false);
|
|
684
649
|
return current;
|
|
@@ -691,10 +656,15 @@ int32_t animateTiming(
|
|
|
691
656
|
TimingAnimation animation) {
|
|
692
657
|
if (!isOnMainThread()) return 0;
|
|
693
658
|
auto iterator = registry().find(driverId);
|
|
694
|
-
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
|
+
}
|
|
695
665
|
auto &state = iterator->second;
|
|
696
666
|
state.destroyed = false;
|
|
697
|
-
const int32_t animationId = allocateAnimationId(
|
|
667
|
+
const int32_t animationId = allocateAnimationId();
|
|
698
668
|
const Presentation resolvedStart =
|
|
699
669
|
prepareAnimation(driverId, state, start, presentation);
|
|
700
670
|
if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
|
|
@@ -709,7 +679,7 @@ int32_t animateTiming(
|
|
|
709
679
|
active.start = resolvedStart;
|
|
710
680
|
active.target = presentation;
|
|
711
681
|
active.durationS = animation.durationMs / 1000.0;
|
|
712
|
-
return startAnimation(driverId, state, std::move(active));
|
|
682
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
713
683
|
}
|
|
714
684
|
|
|
715
685
|
int32_t animateSpring(
|
|
@@ -719,10 +689,15 @@ int32_t animateSpring(
|
|
|
719
689
|
SpringAnimation animation) {
|
|
720
690
|
if (!isOnMainThread()) return 0;
|
|
721
691
|
auto iterator = registry().find(driverId);
|
|
722
|
-
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
|
+
}
|
|
723
698
|
auto &state = iterator->second;
|
|
724
699
|
state.destroyed = false;
|
|
725
|
-
const int32_t animationId = allocateAnimationId(
|
|
700
|
+
const int32_t animationId = allocateAnimationId();
|
|
726
701
|
// Scalar velocity along the current-to-target trajectory; each channel is
|
|
727
702
|
// seeded with velocity·displacement to match the iOS CASpringAnimation
|
|
728
703
|
// per-keypath behavior.
|
|
@@ -752,7 +727,7 @@ int32_t animateSpring(
|
|
|
752
727
|
active.springVelocity[index] =
|
|
753
728
|
velocity * (targetChannels[index] - startChannels[index]);
|
|
754
729
|
}
|
|
755
|
-
return startAnimation(driverId, state, std::move(active));
|
|
730
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
756
731
|
}
|
|
757
732
|
|
|
758
733
|
int32_t animateKeyframes(
|
|
@@ -764,10 +739,15 @@ int32_t animateKeyframes(
|
|
|
764
739
|
int32_t reduceMotion) {
|
|
765
740
|
if (!isOnMainThread()) return 0;
|
|
766
741
|
auto iterator = registry().find(driverId);
|
|
767
|
-
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
|
+
}
|
|
768
748
|
auto &state = iterator->second;
|
|
769
749
|
state.destroyed = false;
|
|
770
|
-
const int32_t animationId = allocateAnimationId(
|
|
750
|
+
const int32_t animationId = allocateAnimationId();
|
|
771
751
|
const Presentation resolvedStart =
|
|
772
752
|
prepareAnimation(driverId, state, start, presentation);
|
|
773
753
|
if (shouldReduceMotion(reduceMotion) || durationMs <= 0 ||
|
|
@@ -779,18 +759,18 @@ int32_t animateKeyframes(
|
|
|
779
759
|
ActiveAnimation active;
|
|
780
760
|
active.id = animationId;
|
|
781
761
|
active.kind = AnimationKind::Keyframes;
|
|
782
|
-
active.keyframes
|
|
762
|
+
active.keyframes.reset(std::move(keyframes));
|
|
783
763
|
active.start = resolvedStart;
|
|
784
764
|
active.target = presentation;
|
|
785
765
|
active.durationS = durationMs / 1000.0;
|
|
786
|
-
return startAnimation(driverId, state, std::move(active));
|
|
766
|
+
return startAnimation(driverId, state, std::move(active), start.startedAtHintS);
|
|
787
767
|
}
|
|
788
768
|
|
|
789
769
|
int32_t rejectAnimation(uint64_t driverId) {
|
|
790
770
|
if (!isOnMainThread()) return 0;
|
|
791
771
|
auto iterator = registry().find(driverId);
|
|
792
772
|
if (iterator == registry().end()) return 0;
|
|
793
|
-
const int32_t animationId = allocateAnimationId(
|
|
773
|
+
const int32_t animationId = allocateAnimationId();
|
|
794
774
|
emitCompletion(driverId, animationId, false);
|
|
795
775
|
return animationId;
|
|
796
776
|
}
|
|
@@ -1001,12 +981,28 @@ void unregisterViewAndroid(
|
|
|
1001
981
|
}
|
|
1002
982
|
}
|
|
1003
983
|
if (removed && state.animation.has_value()) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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)) {
|
|
1010
1006
|
auto &animation = *state.animation;
|
|
1011
1007
|
// wallStartedAtS, not startedAtS: post-anchor the latter lives on the
|
|
1012
1008
|
// shifted frame axis and would overstate elapsed by the first
|
|
@@ -1022,7 +1018,8 @@ void unregisterViewAndroid(
|
|
|
1022
1018
|
state.ownership = Ownership::Interactive;
|
|
1023
1019
|
finishActive(driverId, state, false);
|
|
1024
1020
|
} else {
|
|
1025
|
-
// The last
|
|
1021
|
+
// The last displayable host left mid-flight. Registered detached or
|
|
1022
|
+
// unsized peers do not keep the clock running invisibly.
|
|
1026
1023
|
// Completing here would leave `latest` at the target and statically
|
|
1027
1024
|
// snap any re-registering host straight to it. Re-latch instead:
|
|
1028
1025
|
// freeze the remaining animation at its current geometry so the
|
|
@@ -1030,14 +1027,24 @@ void unregisterViewAndroid(
|
|
|
1030
1027
|
// destroyDriver cancels a latch that never finds a host, so the
|
|
1031
1028
|
// completion cannot hang.
|
|
1032
1029
|
if (animation.kind == AnimationKind::Timing) {
|
|
1033
|
-
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;
|
|
1034
1041
|
} else if (animation.kind == AnimationKind::Keyframes) {
|
|
1035
1042
|
const double progress = animation.durationS <= 0
|
|
1036
1043
|
? 1.0
|
|
1037
1044
|
: clamp01(elapsedS / animation.durationS);
|
|
1038
1045
|
std::vector<Keyframe> remaining;
|
|
1039
1046
|
remaining.push_back({0, animation.current});
|
|
1040
|
-
for (const Keyframe &frame : animation.keyframes) {
|
|
1047
|
+
for (const Keyframe &frame : animation.keyframes.frames()) {
|
|
1041
1048
|
if (frame.offset <= progress) continue;
|
|
1042
1049
|
remaining.push_back(
|
|
1043
1050
|
{(frame.offset - progress) / (1 - progress),
|
|
@@ -1046,7 +1053,9 @@ void unregisterViewAndroid(
|
|
|
1046
1053
|
if (remaining.size() == 1) {
|
|
1047
1054
|
remaining.push_back({1, animation.target});
|
|
1048
1055
|
}
|
|
1049
|
-
|
|
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));
|
|
1050
1059
|
animation.durationS = remainingS;
|
|
1051
1060
|
}
|
|
1052
1061
|
animation.start = animation.current;
|