react-native-smooth-clip-view 0.2.0 → 0.2.1
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 +7 -0
- package/android/src/main/cpp/SmoothClipRegistry.cpp +42 -10
- package/ios/SmoothClipRegistry.mm +52 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,13 @@ const gesture = Gesture.Pan()
|
|
|
198
198
|
`finished` state, including cancellation, replacement, participant unmount,
|
|
199
199
|
and native-side rejection (`animateTo` then returns `0` and one
|
|
200
200
|
`finished: false` completion is delivered).
|
|
201
|
+
- An `animateTo` issued before any host view has registered (for example from
|
|
202
|
+
an effect in the same commit that mounts the host, or inside a modal route
|
|
203
|
+
that attaches late) is held pending and starts with its full duration when
|
|
204
|
+
the first view registers. If no view ever registers, the pending animation
|
|
205
|
+
survives until it is replaced, cancelled, overridden by a take-ownership
|
|
206
|
+
write, or the driver is destroyed — at which point its single
|
|
207
|
+
`finished: false` completion is delivered.
|
|
201
208
|
|
|
202
209
|
Do not call `driver.ui` from React code — it throws on the React runtime; use
|
|
203
210
|
`driver.react` there. During a native transition, `driver.presentation.value`
|
|
@@ -47,6 +47,10 @@ struct ActiveAnimation {
|
|
|
47
47
|
std::array<double, 7> springVelocity{};
|
|
48
48
|
double lastFrameS = 0;
|
|
49
49
|
bool finished = true;
|
|
50
|
+
// False while the animation is latched: built before any host view
|
|
51
|
+
// registered, held un-rendered (and out of animatingDrivers()) until the
|
|
52
|
+
// first registerViewAndroid rebases the clock and starts it.
|
|
53
|
+
bool started = false;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
// Per-view fanout state. Density and host metrics are pushed from Kotlin at
|
|
@@ -457,17 +461,23 @@ Presentation prepareAnimation(
|
|
|
457
461
|
int32_t startAnimation(
|
|
458
462
|
uint64_t driverId,
|
|
459
463
|
DriverState &state,
|
|
460
|
-
ActiveAnimation animation
|
|
461
|
-
const Presentation &target) {
|
|
462
|
-
if (state.views.empty()) {
|
|
463
|
-
applyPresentation(state, target);
|
|
464
|
-
emitCompletion(driverId, animation.id, true);
|
|
465
|
-
return animation.id;
|
|
466
|
-
}
|
|
464
|
+
ActiveAnimation animation) {
|
|
467
465
|
animation.startedAtS = nowSeconds();
|
|
468
466
|
animation.lastFrameS = animation.startedAtS;
|
|
467
|
+
// current = start even while latched is load-bearing: cancelAnimation,
|
|
468
|
+
// beginInteraction, prepareAnimation's visibleBefore and registerView's
|
|
469
|
+
// visible all read animation->current, giving a never-rendered latch
|
|
470
|
+
// freeze-at-start / replace-from-start semantics with no extra branches.
|
|
469
471
|
animation.current = animation.start;
|
|
472
|
+
animation.started = !state.views.empty();
|
|
470
473
|
state.animation = std::move(animation);
|
|
474
|
+
if (!state.animation->started) {
|
|
475
|
+
// Latch: no host view yet (animateTo raced the mount). The first
|
|
476
|
+
// registerViewAndroid rebases the clock, joins animatingDrivers() and
|
|
477
|
+
// schedules the frame loop. Non-zero id is still returned so the JS
|
|
478
|
+
// side does not treat this as rejection.
|
|
479
|
+
return state.animation->id;
|
|
480
|
+
}
|
|
471
481
|
auto &active = animatingDrivers();
|
|
472
482
|
if (std::find(active.begin(), active.end(), driverId) == active.end()) {
|
|
473
483
|
active.push_back(driverId);
|
|
@@ -652,7 +662,7 @@ int32_t animateTiming(
|
|
|
652
662
|
active.start = resolvedStart;
|
|
653
663
|
active.target = presentation;
|
|
654
664
|
active.durationS = animation.durationMs / 1000.0;
|
|
655
|
-
return startAnimation(driverId, state, std::move(active)
|
|
665
|
+
return startAnimation(driverId, state, std::move(active));
|
|
656
666
|
}
|
|
657
667
|
|
|
658
668
|
int32_t animateSpring(
|
|
@@ -695,7 +705,7 @@ int32_t animateSpring(
|
|
|
695
705
|
active.springVelocity[index] =
|
|
696
706
|
velocity * (targetChannels[index] - startChannels[index]);
|
|
697
707
|
}
|
|
698
|
-
return startAnimation(driverId, state, std::move(active)
|
|
708
|
+
return startAnimation(driverId, state, std::move(active));
|
|
699
709
|
}
|
|
700
710
|
|
|
701
711
|
int32_t animateKeyframes(
|
|
@@ -726,7 +736,7 @@ int32_t animateKeyframes(
|
|
|
726
736
|
active.start = resolvedStart;
|
|
727
737
|
active.target = presentation;
|
|
728
738
|
active.durationS = durationMs / 1000.0;
|
|
729
|
-
return startAnimation(driverId, state, std::move(active)
|
|
739
|
+
return startAnimation(driverId, state, std::move(active));
|
|
730
740
|
}
|
|
731
741
|
|
|
732
742
|
int32_t rejectAnimation(uint64_t driverId) {
|
|
@@ -839,6 +849,9 @@ void registerViewAndroid(
|
|
|
839
849
|
? state.animation->current
|
|
840
850
|
: state.latest;
|
|
841
851
|
JNIEnv *env = facebook::jni::Environment::current();
|
|
852
|
+
// The existing-view branch below is unreachable while an animation is
|
|
853
|
+
// latched (a latch implies views was empty at animate time), so only the
|
|
854
|
+
// new-view path needs to start latches.
|
|
842
855
|
for (auto &existing : state.views) {
|
|
843
856
|
if (env->IsSameObject(existing.view.get(), view.get())) {
|
|
844
857
|
existing.density = density;
|
|
@@ -850,8 +863,27 @@ void registerViewAndroid(
|
|
|
850
863
|
}
|
|
851
864
|
ViewEntry entry{
|
|
852
865
|
facebook::jni::make_global(view), density, hostWidthPx, hostHeightPx};
|
|
866
|
+
// `visible` above already delivered animation->current (= start) to the
|
|
867
|
+
// registering view — the correct first frame for a latched animation.
|
|
853
868
|
deliverToView(entry, visible);
|
|
854
869
|
state.views.push_back(std::move(entry));
|
|
870
|
+
if (state.animation.has_value() && !state.animation->started) {
|
|
871
|
+
// Start the latched animation: rebase the clock so the first frame
|
|
872
|
+
// integrates from now, not from the pre-mount animateTo call.
|
|
873
|
+
// Note this function has no isOnMainThread() guard (Kotlin calls it from
|
|
874
|
+
// the view's main-thread attach path); scheduleFrame() is itself
|
|
875
|
+
// main-thread-gated, so an off-main register cannot bind the
|
|
876
|
+
// choreographer to the wrong thread.
|
|
877
|
+
auto &animation = *state.animation;
|
|
878
|
+
animation.started = true;
|
|
879
|
+
animation.startedAtS = nowSeconds();
|
|
880
|
+
animation.lastFrameS = animation.startedAtS;
|
|
881
|
+
auto &active = animatingDrivers();
|
|
882
|
+
if (std::find(active.begin(), active.end(), driverId) == active.end()) {
|
|
883
|
+
active.push_back(driverId);
|
|
884
|
+
}
|
|
885
|
+
scheduleFrame();
|
|
886
|
+
}
|
|
855
887
|
}
|
|
856
888
|
|
|
857
889
|
void setViewHostGeometryAndroid(
|
|
@@ -58,6 +58,10 @@ struct ActiveAnimation {
|
|
|
58
58
|
std::vector<Keyframe> keyframes;
|
|
59
59
|
double durationMs = 0;
|
|
60
60
|
CFTimeInterval startedAt = 0;
|
|
61
|
+
// False while the animation is latched: built before any host view
|
|
62
|
+
// registered, held un-rendered until the first registerView rebases
|
|
63
|
+
// startedAt and starts it. startedAt is only meaningful once started.
|
|
64
|
+
bool started = false;
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
struct DriverState {
|
|
@@ -170,7 +174,12 @@ int32_t allocateAnimationId(DriverState &state) {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
Presentation canonicalFrozenPresentation(DriverState &state) {
|
|
173
|
-
|
|
177
|
+
// A latched animation never rendered, so state.latest already holds its
|
|
178
|
+
// target; freezing there would jump the clip. Freeze at the start instead.
|
|
179
|
+
Presentation canonical =
|
|
180
|
+
state.animation.has_value() && !state.animation->started
|
|
181
|
+
? state.animation->start
|
|
182
|
+
: state.latest;
|
|
174
183
|
bool hasCanonical = false;
|
|
175
184
|
for (const ViewKey key : state.views) {
|
|
176
185
|
const Presentation frozen = [viewForKey(key) smoothClipFreezePresentation];
|
|
@@ -224,9 +233,16 @@ void prepareAnimation(
|
|
|
224
233
|
Presentation resolvedAnimationStart(
|
|
225
234
|
const DriverState &state,
|
|
226
235
|
const AnimationStart &start) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
if (start.hasInteractiveStart && state.ownership == Ownership::Interactive) {
|
|
237
|
+
return start.interactiveStart;
|
|
238
|
+
}
|
|
239
|
+
// Replacing a latched animation (e.g. open→close before the host mounted):
|
|
240
|
+
// it never rendered, and state.latest is its target — start from where the
|
|
241
|
+
// clip actually is, the latch's own start.
|
|
242
|
+
if (state.animation.has_value() && !state.animation->started) {
|
|
243
|
+
return state.animation->start;
|
|
244
|
+
}
|
|
245
|
+
return state.latest;
|
|
230
246
|
}
|
|
231
247
|
|
|
232
248
|
// Installs the remaining part of the active animation on a view that joins
|
|
@@ -303,8 +319,10 @@ void registerView(
|
|
|
303
319
|
state.latest = initialPresentation;
|
|
304
320
|
state.hasLatest = true;
|
|
305
321
|
}
|
|
322
|
+
const bool hasAnimation = state.animation.has_value();
|
|
323
|
+
const bool startsLatch = hasAnimation && !state.animation->started;
|
|
306
324
|
const bool shouldReplay =
|
|
307
|
-
state.animation
|
|
325
|
+
hasAnimation && state.animation->started && !state.views.empty();
|
|
308
326
|
Presentation visible = state.latest;
|
|
309
327
|
if (shouldReplay) {
|
|
310
328
|
// A registered peer that never laid out reports zero geometry; joining
|
|
@@ -321,6 +339,10 @@ void registerView(
|
|
|
321
339
|
visible = reference != nil
|
|
322
340
|
? [reference smoothClipCurrentPresentation]
|
|
323
341
|
: state.animation->start;
|
|
342
|
+
} else if (startsLatch) {
|
|
343
|
+
// state.latest already holds the latch's TARGET; applying it here is
|
|
344
|
+
// exactly the jump this path exists to avoid.
|
|
345
|
+
visible = state.animation->start;
|
|
324
346
|
}
|
|
325
347
|
const ViewKey key = keyForView(view);
|
|
326
348
|
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
@@ -328,8 +350,16 @@ void registerView(
|
|
|
328
350
|
state.views.push_back(key);
|
|
329
351
|
}
|
|
330
352
|
[view smoothClipApplyPresentation:visible];
|
|
331
|
-
if (
|
|
353
|
+
if (startsLatch) {
|
|
354
|
+
// Start the latched animation: rebase the join clock before any elapsed
|
|
355
|
+
// math runs (dispatch below and the first-layout joinActiveAnimation).
|
|
356
|
+
state.animation->startedAt = CACurrentMediaTime();
|
|
357
|
+
state.animation->started = true;
|
|
358
|
+
}
|
|
359
|
+
if (!(shouldReplay || startsLatch)) return;
|
|
332
360
|
|
|
361
|
+
// The participant insert inside the join is also what keeps unregisterView
|
|
362
|
+
// orphan-safe for a mount-then-unmount before first layout.
|
|
333
363
|
dispatchActiveAnimationJoin(state, view, key, visible);
|
|
334
364
|
}
|
|
335
365
|
|
|
@@ -339,6 +369,10 @@ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
|
339
369
|
if (iterator == registry().end()) return false;
|
|
340
370
|
auto &state = iterator->second;
|
|
341
371
|
if (!state.animation.has_value() || state.views.empty()) return false;
|
|
372
|
+
// Unreachable in the normal flow (a pending install implies the animation
|
|
373
|
+
// was dispatched, which implies started), but a join must never compute
|
|
374
|
+
// elapsed time against an un-rebased latch clock.
|
|
375
|
+
if (!state.animation->started) return false;
|
|
342
376
|
const ViewKey key = keyForView(view);
|
|
343
377
|
if (std::find(state.views.begin(), state.views.end(), key) ==
|
|
344
378
|
state.views.end()) {
|
|
@@ -500,10 +534,13 @@ int32_t animateTiming(
|
|
|
500
534
|
active.durationMs = animation.durationMs;
|
|
501
535
|
active.startedAt = CACurrentMediaTime();
|
|
502
536
|
active.participants.insert(state.views.begin(), state.views.end());
|
|
537
|
+
active.started = !state.views.empty();
|
|
503
538
|
state.animation = std::move(active);
|
|
504
|
-
if (state.
|
|
505
|
-
|
|
506
|
-
|
|
539
|
+
if (!state.animation->started) {
|
|
540
|
+
// Latch: no host view yet (animateTo raced the mount). Ownership stays
|
|
541
|
+
// Native — the pending animation owns rendering intent — and the first
|
|
542
|
+
// registerView rebases startedAt and starts it. The non-zero id is still
|
|
543
|
+
// returned synchronously so the JS side does not treat this as rejection.
|
|
507
544
|
return animationId;
|
|
508
545
|
}
|
|
509
546
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -549,10 +586,10 @@ int32_t animateSpring(
|
|
|
549
586
|
active.spring = animation;
|
|
550
587
|
active.startedAt = CACurrentMediaTime();
|
|
551
588
|
active.participants.insert(state.views.begin(), state.views.end());
|
|
589
|
+
active.started = !state.views.empty();
|
|
552
590
|
state.animation = std::move(active);
|
|
553
|
-
if (state.
|
|
554
|
-
|
|
555
|
-
emitCompletion(driverId, state, animationId, true);
|
|
591
|
+
if (!state.animation->started) {
|
|
592
|
+
// Latched until the first registerView — see animateTiming.
|
|
556
593
|
return animationId;
|
|
557
594
|
}
|
|
558
595
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
@@ -600,10 +637,10 @@ int32_t animateKeyframes(
|
|
|
600
637
|
active.durationMs = durationMs;
|
|
601
638
|
active.startedAt = CACurrentMediaTime();
|
|
602
639
|
active.participants.insert(state.views.begin(), state.views.end());
|
|
640
|
+
active.started = !state.views.empty();
|
|
603
641
|
state.animation = std::move(active);
|
|
604
|
-
if (state.
|
|
605
|
-
|
|
606
|
-
emitCompletion(driverId, state, animationId, true);
|
|
642
|
+
if (!state.animation->started) {
|
|
643
|
+
// Latched until the first registerView — see animateTiming.
|
|
607
644
|
return animationId;
|
|
608
645
|
}
|
|
609
646
|
for (const ViewKey key : state.views) {
|
package/package.json
CHANGED