Skip to content

Commit a4f78b3

Browse files
zeyapmeta-codesync[bot]
authored andcommitted
Allow waiting for user land Animated animations before firing transition complete callback (#56459)
Summary: Pull Request resolved: #56459 ## Changelog: [Internal] [Added] - Allow waiting for user land Animated animations before firing transition complete callback Instead of calling `onCompleteCallback` synchronously at the end of `startViewTransition`, defer it until all animations have finished. Since transition animation can be kicked off in user land from view transition event handlers, we need extra APIs to signal to native side when animation starts and finishes. This ensures the transition lifecycle properly waits for native animated transitions to complete. - Add `waitForTransitionAnimation` / `transitionAnimationFinished` to the delegate interface and expose via `NativeViewTransition` TurboModule - JS calls `waitForTransitionAnimation(animationId)` before starting each animation and `transitionAnimationFinished(animationId)` in the completion callback - `ViewTransitionModule` tracks pending animation IDs in a `std::set` and fires `onCompleteCallback_` only when all animations are done - only track and wait for animations started during transition-ready callback - if no animation requests to be waited for, resolve complete promise immediately when transition-ready callback finishes Reviewed By: sammy-SC Differential Revision: D99366980 fbshipit-source-id: 6313bdd2b3f7b90a5aee24e0faec905bce75d715
1 parent 3cf4024 commit a4f78b3

6 files changed

Lines changed: 66 additions & 4 deletions

File tree

packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,26 @@ jsi::Value NativeViewTransition::findPseudoElementShadowNodeByTag(
6767
return jsi::Value::null();
6868
}
6969

70+
void NativeViewTransition::waitForTransitionAnimation(
71+
jsi::Runtime& rt,
72+
double animationId) {
73+
auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager();
74+
auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate();
75+
if (viewTransitionDelegate != nullptr) {
76+
viewTransitionDelegate->waitForTransitionAnimation(
77+
static_cast<int>(animationId));
78+
}
79+
}
80+
81+
void NativeViewTransition::transitionAnimationFinished(
82+
jsi::Runtime& rt,
83+
double animationId) {
84+
auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager();
85+
auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate();
86+
if (viewTransitionDelegate != nullptr) {
87+
viewTransitionDelegate->transitionAnimationFinished(
88+
static_cast<int>(animationId));
89+
}
90+
}
91+
7092
} // namespace facebook::react

packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class NativeViewTransition : public NativeViewTransitionCxxSpec<NativeViewTransi
2828
getViewTransitionInstance(jsi::Runtime &rt, const std::string &name, const std::string &pseudo);
2929

3030
jsi::Value findPseudoElementShadowNodeByTag(jsi::Runtime &rt, double reactTag);
31+
32+
void waitForTransitionAnimation(jsi::Runtime &rt, double animationId);
33+
34+
void transitionAnimationFinished(jsi::Runtime &rt, double animationId);
3135
};
3236

3337
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ class UIManagerViewTransitionDelegate {
7373
// Called by the reconciler to signal that the next view transition should
7474
// be suspended until the currently active one finishes.
7575
virtual void suspendOnActiveViewTransition() {}
76+
77+
virtual void waitForTransitionAnimation(int animationId) {}
78+
79+
virtual void transitionAnimationFinished(int animationId) {}
7680
};
7781

7882
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ void ViewTransitionModule::startViewTransition(
345345

346346
// Mark transition as started
347347
transitionStarted_ = true;
348+
pendingAnimationIds_.clear();
349+
onCompleteCallback_ = onCompleteCallback;
348350

349351
// Call mutation callback (including commitRoot, measureInstance,
350352
// applyViewTransitionName, createViewTransitionInstance for old & new)
@@ -353,21 +355,39 @@ void ViewTransitionModule::startViewTransition(
353355
}
354356

355357
applySnapshotsOnPseudoElementShadowNodes();
358+
356359
transitionReadyFinished_ = false;
360+
// onReadyCallback starts the animations. Any animation registered via
361+
// waitForTransitionAnimation during this callback is part of this transition.
357362
if (onReadyCallback) {
358363
onReadyCallback();
359364
}
365+
}
360366

361-
// Transition animation starts
367+
void ViewTransitionModule::waitForTransitionAnimation(int animationId) {
368+
if (!transitionReadyFinished_) {
369+
pendingAnimationIds_.insert(animationId);
370+
}
371+
}
362372

363-
// Call onComplete callback when transition finishes
364-
if (onCompleteCallback) {
365-
onCompleteCallback();
373+
void ViewTransitionModule::transitionAnimationFinished(int animationId) {
374+
pendingAnimationIds_.erase(animationId);
375+
if (transitionReadyFinished_ && pendingAnimationIds_.empty() &&
376+
onCompleteCallback_) {
377+
auto callback = std::move(onCompleteCallback_);
378+
onCompleteCallback_ = nullptr;
379+
callback();
366380
}
367381
}
368382

369383
void ViewTransitionModule::startViewTransitionReadyFinished() {
370384
transitionReadyFinished_ = true;
385+
386+
if (pendingAnimationIds_.empty() && onCompleteCallback_) {
387+
auto callback = std::move(onCompleteCallback_);
388+
onCompleteCallback_ = nullptr;
389+
callback();
390+
}
371391
}
372392

373393
void ViewTransitionModule::suspendOnActiveViewTransition() {

packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate,
8383

8484
void suspendOnActiveViewTransition() override;
8585

86+
void waitForTransitionAnimation(int animationId) override;
87+
88+
void transitionAnimationFinished(int animationId) override;
89+
8690
// Animation state structure for storing minimal view data
8791
struct AnimationKeyFrameViewLayoutMetrics {
8892
Point originFromRoot;
@@ -142,6 +146,12 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate,
142146
std::function<void()> onCompleteCallback;
143147
};
144148
std::queue<PendingTransition> pendingTransitions_{};
149+
150+
// Tracks animation IDs that must complete before onCompleteCallback_ fires.
151+
// Animations are registered via waitForTransitionAnimation (called from JS
152+
// after connectAnimatedNodeToView) and removed via transitionAnimationFinished.
153+
std::unordered_set<int> pendingAnimationIds_{};
154+
std::function<void()> onCompleteCallback_{nullptr};
145155
};
146156

147157
} // namespace facebook::react

packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface Spec extends TurboModule {
2424
nativeTag: number,
2525
};
2626
+findPseudoElementShadowNodeByTag: (reactTag: number) => ?unknown /* Node */;
27+
+waitForTransitionAnimation: (animationId: number) => void;
28+
+transitionAnimationFinished: (animationId: number) => void;
2729
}
2830

2931
export default TurboModuleRegistry.get<Spec>(

0 commit comments

Comments
 (0)