From 76df4b2c7359bef508daa60b835251bdce33676d Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 1/8] Add pseudo-element shadow nodes for view transitions Summary: Implement `createViewTransitionInstance`, which is invoked by the React reconciler to create pseudo-element shadow nodes that visually represent the old state of elements participating in a view transition. Key changes: - Add `createViewTransitionInstance` JSI binding in `UIManagerBinding`, accepting a transition name and pseudo-element tag - Add virtual `createViewTransitionInstance` method to `UIManagerViewTransitionDelegate` - Implement the method in `ViewTransitionModule`: creates an absolutely-positioned, non-interactive `View` shadow node matching the old element's layout metrics (position, size) - Manage two pseudo-element node maps: `oldPseudoElementNodes_` for the current transition and `oldPseudoElementNodesForNextTransition_` for entering nodes that may exit in a future transition - Update `getOldViewTransitionInstance` to return the pseudo-element's tag (instead of the original element's tag) when a pseudo-element exists - Add `applySnapshotsOnPseudoElementShadowNodes` stub for future platform-level bitmap snapshot integration `createViewTransitionInstance` is typically called after `applyViewTransitionName` in the React reconciler. See the diagram below for the full flow. {F1987481080} Differential Revision: D98981886 --- .../renderer/uimanager/UIManagerBinding.cpp | 31 ++++++ .../UIManagerViewTransitionDelegate.h | 2 + .../viewtransition/ViewTransitionModule.cpp | 95 +++++++++++++++++-- .../viewtransition/ViewTransitionModule.h | 11 +++ 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 1aa9fdf2ab24..9b6b08f4642b 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -981,6 +981,37 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "createViewTransitionInstance") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto transitionName = arguments[0].isString() + ? stringFromValue(runtime, arguments[0]) + : ""; + auto pseudoElementTag = tagFromValue(arguments[1]); + + if (!transitionName.empty()) { + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->createViewTransitionInstance( + transitionName, pseudoElementTag); + } + } + + return jsi::Value::undefined(); + }); + } + if (methodName == "cancelViewTransitionName") { auto paramCount = 2; return jsi::Function::createFromHostFunction( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index 9d4d83637f4c..f8f82d1433fc 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -23,6 +23,8 @@ class UIManagerViewTransitionDelegate { { } + virtual void createViewTransitionInstance(const std::string & /*name*/, Tag /*pseudoElementTag*/) {} + virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {} virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {} diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index b4d759bf4950..5fd165304b37 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,9 +7,8 @@ #include "ViewTransitionModule.h" -#include - #include +#include #include namespace facebook::react { @@ -45,6 +44,16 @@ void ViewTransitionModule::applyViewTransitionName( AnimationKeyFrameView oldView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; oldLayout_[name] = oldView; + + // TODO: capture bitmap snapshot of old view via platform + + if (auto it = oldPseudoElementNodesForNextTransition_.find(name); + it != oldPseudoElementNodesForNextTransition_.end()) { + auto pseudoElementNode = it->second; + oldPseudoElementNodes_[name] = pseudoElementNode; + oldPseudoElementNodesForNextTransition_.erase(it); + } + } else { AnimationKeyFrameView newView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; @@ -52,6 +61,67 @@ void ViewTransitionModule::applyViewTransitionName( } } +void ViewTransitionModule::createViewTransitionInstance( + const std::string& name, + Tag pseudoElementTag) { + if (uiManager_ == nullptr) { + return; + } + + // if createViewTransitionInstance is called before transition started, it + // creates the old pseudo elements for exiting nodes that potentially + // participate in current transition that's about to happen; if called after + // transition started, it creates old pseudo elements for entering nodes, and + // will be used in next transition when these node are exiting + bool forNextTransition = false; + AnimationKeyFrameView view = {}; + auto it = oldLayout_.find(name); + if (it == oldLayout_.end()) { + forNextTransition = true; + if (auto newIt = newLayout_.find(name); newIt != newLayout_.end()) { + view = newIt->second; + } + } else { + view = it->second; + } + + // Build props: absolute position matching old element, non-interactive + if (pseudoElementTag > 0 && view.tag > 0) { + // Create a base node with layout props via createNode + // TODO: T262559684 created dedicated shadow node type for old pseudo + // element + auto rawProps = RawProps( + folly::dynamic::object("position", "absolute")( + "left", view.layoutMetrics.originFromRoot.x)( + "top", view.layoutMetrics.originFromRoot.y)( + "width", view.layoutMetrics.size.width)( + "height", view.layoutMetrics.size.height)("pointerEvents", "none")( + "opacity", 0)("collapsable", false)); + + auto baseNode = uiManager_->createNode( + pseudoElementTag, + "View", + view.surfaceId, + std::move(rawProps), + nullptr /* instanceHandle */); + + if (baseNode == nullptr) { + return; + } + + // Clone the shadow node — bitmap will be set by platform + auto pseudoElementNode = baseNode->clone({}); + + if (pseudoElementNode != nullptr) { + if (!forNextTransition) { + oldPseudoElementNodes_[name] = pseudoElementNode; + } else { + oldPseudoElementNodesForNextTransition_[name] = pseudoElementNode; + } + } + } +} + void ViewTransitionModule::cancelViewTransitionName( const ShadowNode& shadowNode, const std::string& name) { @@ -67,6 +137,14 @@ void ViewTransitionModule::restoreViewTransitionName( cancelledNameRegistry_.erase(shadowNode.getTag()); } +void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { + if (oldPseudoElementNodes_.empty() || uiManager_ == nullptr) { + return; + } + + // TODO: set bitmap snapshots on pseudo-element views via platform +} + LayoutMetrics ViewTransitionModule::captureLayoutMetricsFromRoot( const ShadowNode& shadowNode) { if (uiManager_ == nullptr) { @@ -100,13 +178,13 @@ void ViewTransitionModule::startViewTransition( // Mark transition as started transitionStarted_ = true; - // Call mutation callback (including commitRoot, measureInstance - // applyViewTransitionName for old & new) + // Call mutation callback (including commitRoot, measureInstance, + // applyViewTransitionName, createViewTransitionInstance for old & new) if (mutationCallback) { mutationCallback(); } - // TODO: capture pseudo elements + applySnapshotsOnPseudoElementShadowNodes(); if (onReadyCallback) { onReadyCallback(); @@ -128,6 +206,7 @@ void ViewTransitionModule::startViewTransitionEnd() { } } nameRegistry_.clear(); + oldPseudoElementNodes_.clear(); transitionStarted_ = false; } @@ -152,12 +231,16 @@ ViewTransitionModule::getViewTransitionInstance( auto it = oldLayout_.find(name); if (it != oldLayout_.end()) { const auto& view = it->second; + auto pseudoElementIt = oldPseudoElementNodes_.find(name); + auto nativeTag = pseudoElementIt != oldPseudoElementNodes_.end() + ? pseudoElementIt->second->getTag() + : view.tag; return ViewTransitionInstance{ .x = view.layoutMetrics.originFromRoot.x, .y = view.layoutMetrics.originFromRoot.y, .width = view.layoutMetrics.size.width, .height = view.layoutMetrics.size.height, - .nativeTag = view.tag}; + .nativeTag = nativeTag}; } } diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index f5d1f59fdc50..a076e3796ba4 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -29,6 +29,10 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { void applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className) override; + // creates a pseudo-element shadow node for a given transition name using the + // captured old layout metrics + void createViewTransitionInstance(const std::string &name, Tag pseudoElementTag) override; + // if a viewTransitionName is cancelled, the element doesn't have view-transition-name and browser won't be taking // snapshot void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) override; @@ -72,8 +76,15 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; + // pseudo-element nodes keyed by transition name + std::unordered_map> oldPseudoElementNodes_{}; + // will be restored into oldPseudoElementNodes_ in next transition + std::unordered_map> oldPseudoElementNodesForNextTransition_{}; + LayoutMetrics captureLayoutMetricsFromRoot(const ShadowNode &shadowNode); + void applySnapshotsOnPseudoElementShadowNodes(); + UIManager *uiManager_{nullptr}; bool transitionStarted_{false}; From 956320c91c389f61fb6ddd0017830a8c52c046d3 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 2/8] Append pseudo-element shadow nodes to root at commit Summary: Append view transition pseudo-element shadow nodes to the root's children at commit time (`shadowTreeWillCommit`), so they are committed into the shadow tree and rendered by the platform. Key changes: - Add `getPseudoElementNodes(surfaceId)` virtual method to `UIManagerViewTransitionDelegate`, returning pseudo-element shadow nodes filtered by surface ID - Implement the method in `ViewTransitionModule`, iterating over `oldPseudoElementNodes_` and collecting nodes matching the given surface - In `shadowTreeWillCommit`, when view transitions are enabled, query the delegate for pseudo-element nodes and insert them at the end of `rootChildren` before committing This ensures pseudo-element nodes (created by `createViewTransitionInstance`) are included in the committed shadow tree and ultimately mounted as platform views that display old-element snapshots during transitions. Differential Revision: D98982122 --- .../react/renderer/scheduler/Scheduler.cpp | 5 +- .../react/renderer/scheduler/Scheduler.h | 2 +- .../viewtransition/ViewTransitionModule.cpp | 121 ++++++++++++++++-- .../viewtransition/ViewTransitionModule.h | 46 ++++++- 4 files changed, 156 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 3a1393ef40b0..477ecb8a8820 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -160,9 +160,8 @@ Scheduler::Scheduler( // Initialize ViewTransitionModule if (ReactNativeFeatureFlags::viewTransitionEnabled()) { - viewTransitionModule_ = std::make_unique(); - viewTransitionModule_->setUIManager(uiManager_.get()); - uiManager_->setViewTransitionDelegate(viewTransitionModule_.get()); + viewTransitionModule_ = std::make_shared(); + viewTransitionModule_->initialize(uiManager_.get(), viewTransitionModule_); } uiManager->registerMountHook(*eventPerformanceLogger_); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index ad16e3e40879..00ed0f43ed06 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -147,7 +147,7 @@ class Scheduler final : public UIManagerDelegate { RuntimeScheduler *runtimeScheduler_{nullptr}; - std::unique_ptr viewTransitionModule_; + std::shared_ptr viewTransitionModule_; mutable std::shared_mutex onSurfaceStartCallbackMutex_; OnSurfaceStartCallback onSurfaceStartCallback_; diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 5fd165304b37..30a891fd83c0 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,14 +7,51 @@ #include "ViewTransitionModule.h" +#include #include #include +#include +#include #include namespace facebook::react { -void ViewTransitionModule::setUIManager(UIManager* uiManager) { +ViewTransitionModule::~ViewTransitionModule() { + if (uiManager_ != nullptr) { + if (uiManager_->getViewTransitionDelegate() == this) { + uiManager_->setViewTransitionDelegate(nullptr); + } + uiManager_->unregisterCommitHook(*this); + uiManager_ = nullptr; + } +} + +void ViewTransitionModule::initialize( + UIManager* uiManager, + std::weak_ptr weakThis) { + if (uiManager_ != nullptr) { + uiManager_->unregisterCommitHook(*this); + } uiManager_ = uiManager; + if (uiManager_ != nullptr) { + uiManager_->registerCommitHook(*this); + + // Register as MountingOverrideDelegate on existing surfaces + uiManager_->getShadowTreeRegistry().enumerate( + [weakThis](const ShadowTree& shadowTree, bool& /*stop*/) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + // Register on surfaces started in the future + uiManager_->setOnSurfaceStartCallback( + [weakThis](const ShadowTree& shadowTree) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + uiManager_->setViewTransitionDelegate(this); + } } void ViewTransitionModule::applyViewTransitionName( @@ -47,11 +84,9 @@ void ViewTransitionModule::applyViewTransitionName( // TODO: capture bitmap snapshot of old view via platform - if (auto it = oldPseudoElementNodesForNextTransition_.find(name); - it != oldPseudoElementNodesForNextTransition_.end()) { - auto pseudoElementNode = it->second; - oldPseudoElementNodes_[name] = pseudoElementNode; - oldPseudoElementNodesForNextTransition_.erase(it); + if (auto it = oldPseudoElementNodesRepository_.find(name); + it != oldPseudoElementNodesRepository_.end()) { + oldPseudoElementNodes_[name] = it->second.node; } } else { @@ -115,11 +150,81 @@ void ViewTransitionModule::createViewTransitionInstance( if (pseudoElementNode != nullptr) { if (!forNextTransition) { oldPseudoElementNodes_[name] = pseudoElementNode; - } else { - oldPseudoElementNodesForNextTransition_[name] = pseudoElementNode; + } + oldPseudoElementNodesRepository_[name] = InactivePseudoElement{ + .node = pseudoElementNode, .sourceTag = view.tag}; + } + } +} + +RootShadowNode::Unshared ViewTransitionModule::shadowTreeWillCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& /*oldRootShadowNode*/, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTreeCommitOptions& /*commitOptions*/) noexcept { + if (oldPseudoElementNodes_.empty()) { + return newRootShadowNode; + } + + auto surfaceId = shadowTree.getSurfaceId(); + + // Collect pseudo-element nodes for this surface, skipping any that are + // already present in the children list (from a previous commit hook run). + const auto& existingChildren = newRootShadowNode->getChildren(); + std::unordered_set existingTags; + existingTags.reserve(existingChildren.size()); + for (const auto& child : existingChildren) { + existingTags.insert(child->getTag()); + } + + auto newChildren = + std::make_shared>>( + existingChildren); + bool appended = false; + for (const auto& [name, node] : oldPseudoElementNodes_) { + if (node->getSurfaceId() == surfaceId && + existingTags.find(node->getTag()) == existingTags.end()) { + newChildren->push_back(node); + appended = true; + } + } + + if (!appended) { + return newRootShadowNode; + } + + return std::make_shared( + *newRootShadowNode, + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = newChildren, + }); +} + +bool ViewTransitionModule::shouldOverridePullTransaction() const { + return !oldPseudoElementNodesRepository_.empty(); +} + +std::optional ViewTransitionModule::pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry& telemetry, + ShadowViewMutationList mutations) const { + for (const auto& mutation : mutations) { + if (mutation.type == ShadowViewMutation::Delete) { + auto tag = mutation.oldChildShadowView.tag; + for (auto it = oldPseudoElementNodesRepository_.begin(); + it != oldPseudoElementNodesRepository_.end();) { + if (it->second.sourceTag == tag) { + it = oldPseudoElementNodesRepository_.erase(it); + } else { + ++it; + } } } } + return MountingTransaction{ + surfaceId, number, std::move(mutations), telemetry}; } void ViewTransitionModule::cancelViewTransitionName( diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index a076e3796ba4..3a340a96883f 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -11,18 +11,25 @@ #include #include +#include #include +#include #include namespace facebook::react { +class ShadowTree; class UIManager; -class ViewTransitionModule : public UIManagerViewTransitionDelegate { +class ViewTransitionModule : public UIManagerViewTransitionDelegate, + public UIManagerCommitHook, + public MountingOverrideDelegate { public: - ~ViewTransitionModule() override = default; + ~ViewTransitionModule() override; - void setUIManager(UIManager *uiManager); + void initialize(UIManager *uiManager, std::weak_ptr weakThis); + +#pragma mark - UIManagerViewTransitionDelegate // will be called when a view will transition. if a view already has a view-transition-name, it may not be called // again until it's removed @@ -50,6 +57,25 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { std::optional getViewTransitionInstance(const std::string &name, const std::string &pseudo) override; +#pragma mark - UIManagerCommitHook + + void commitHookWasRegistered(const UIManager & /*uiManager*/) noexcept override {} + void commitHookWasUnregistered(const UIManager & /*uiManager*/) noexcept override {} + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree &shadowTree, + const RootShadowNode::Shared &oldRootShadowNode, + const RootShadowNode::Unshared &newRootShadowNode, + const ShadowTreeCommitOptions &commitOptions) noexcept override; + +#pragma mark - MountingOverrideDelegate + + bool shouldOverridePullTransaction() const override; + std::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -76,10 +102,18 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; - // pseudo-element nodes keyed by transition name + // pseudo-element nodes keyed by transition name, appended to/removed from root children at next ShadowTree commit + // TODO: T262559264 should be cleaned up from ShadowTree as soon as transition animation ends std::unordered_map> oldPseudoElementNodes_{}; - // will be restored into oldPseudoElementNodes_ in next transition - std::unordered_map> oldPseudoElementNodesForNextTransition_{}; + + struct InactivePseudoElement { + std::shared_ptr node; + Tag sourceTag{0}; // tag of the original view this was created from + }; + // pseudo-element nodes created for entering nodes, to be copied into + // oldPseudoElementNodes_ during the next applyViewTransitionName call. + // Mutable because pullTransaction (const) needs to erase unmounted entries. + mutable std::unordered_map oldPseudoElementNodesRepository_{}; LayoutMetrics captureLayoutMetricsFromRoot(const ShadowNode &shadowNode); From 3695019d3a1f295b76669b124911cf3d74421ce1 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 3/8] Allow pseudo-element shadow node lookup in JS Summary: Add `findPseudoElementShadowNodeByTag` to look up pseudo-element shadow nodes by tag from JS, and update the animation helper to connect animated nodes to pseudo-element shadow nodes for old-element animations. Key changes: - Add `findPseudoElementShadowNodeByTag(tag)` virtual method to `UIManagerViewTransitionDelegate` and implement in `ViewTransitionModule` (linear scan over `oldPseudoElementNodes_`) - Expose the method via `NativeViewTransition` TurboModule so JS can resolve a pseudo-element's shadow node by its native tag - Add `findPseudoElementShadowNodeByTag` to `NativeViewTransition.js` spec - Update `ViewTransitionAnimationHelper.js`: - `animateInternal` accepts a new `pseudo` parameter (`'old'` | `'new'`) - When `pseudo === 'old'`, use `findPseudoElementShadowNodeByTag` instead of `findShadowNodeByTag_DEPRECATED` to connect animated nodes to the correct shadow node family - Pass `'new'` for enter animations and propagate `_pseudo` from pseudo-element objects for exit animations Differential Revision: D98982251 --- .../viewtransition/NativeViewTransition.cpp | 16 ++++++++++ .../viewtransition/NativeViewTransition.h | 2 ++ .../UIManagerViewTransitionDelegate.h | 10 +++++++ .../viewtransition/ViewTransitionModule.cpp | 30 +++++++++++++++++++ .../viewtransition/ViewTransitionModule.h | 6 ++-- .../specs/NativeViewTransition.js | 1 + 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp index 6fb69012df42..8190c7c2f76b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp @@ -51,4 +51,20 @@ std::optional NativeViewTransition::getViewTransitionInstance( return result; } +jsi::Value NativeViewTransition::findPseudoElementShadowNodeByTag( + jsi::Runtime& rt, + double reactTag) { + auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager(); + auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + auto shadowNode = viewTransitionDelegate->findPseudoElementShadowNodeByTag( + static_cast(reactTag)); + if (shadowNode) { + return Bridging>::toJs(rt, shadowNode); + } + } + + return jsi::Value::null(); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h index bd5cb3b424f2..f3311f40bef3 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h @@ -26,6 +26,8 @@ class NativeViewTransition : public NativeViewTransitionCxxSpec getViewTransitionInstance(jsi::Runtime &rt, const std::string &name, const std::string &pseudo); + + jsi::Value findPseudoElementShadowNodeByTag(jsi::Runtime &rt, double reactTag); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index f8f82d1433fc..d3685775c353 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -57,6 +57,16 @@ class UIManagerViewTransitionDelegate { { return std::nullopt; } + + // Similar to UIManager::findShadowNodeByTag, but searches all direct children + // of the root node (where pseudo-element nodes live) rather than just the + // first child. Pseudo-element nodes are appended as additional children of the + // root node, rather than inserted into the main React tree, to avoid + // disrupting the user-created component tree. + virtual std::shared_ptr findPseudoElementShadowNodeByTag(Tag /*tag*/) const + { + return nullptr; + } }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 30a891fd83c0..d67ad6e24a88 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -227,6 +227,36 @@ std::optional ViewTransitionModule::pullTransaction( surfaceId, number, std::move(mutations), telemetry}; } +std::shared_ptr +ViewTransitionModule::findPseudoElementShadowNodeByTag(Tag tag) const { + if (uiManager_ == nullptr) { + return nullptr; + } + + auto shadowNode = std::shared_ptr{}; + + uiManager_->getShadowTreeRegistry().enumerate( + [&](const ShadowTree& shadowTree, bool& stop) { + const auto rootShadowNode = + shadowTree.getCurrentRevision().rootShadowNode; + + if (rootShadowNode != nullptr) { + const auto& children = rootShadowNode->getChildren(); + // Pseudo element nodes are appended after the first child (the main + // React tree), so iterate from index 1 onwards. + for (size_t i = 1; i < children.size(); ++i) { + if (children[i]->getTag() == tag) { + shadowNode = children[i]; + stop = true; + return; + } + } + } + }); + + return shadowNode; +} + void ViewTransitionModule::cancelViewTransitionName( const ShadowNode& shadowNode, const std::string& name) { diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index 3a340a96883f..ae585978b409 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -76,6 +76,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, const TransactionTelemetry &telemetry, ShadowViewMutationList mutations) const override; + std::shared_ptr findPseudoElementShadowNodeByTag(Tag tag) const override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -102,8 +104,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; - // pseudo-element nodes keyed by transition name, appended to/removed from root children at next ShadowTree commit - // TODO: T262559264 should be cleaned up from ShadowTree as soon as transition animation ends + // pseudo-element nodes keyed by transition name, appended to root children via UIManagerCommitHook + // TODO: T262559264 pseudo elements should be cleaned up as soon as transition animation ends std::unordered_map> oldPseudoElementNodes_{}; struct InactivePseudoElement { diff --git a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js index 1c56f51e1f3e..c9fa79cd3639 100644 --- a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js +++ b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js @@ -23,6 +23,7 @@ export interface Spec extends TurboModule { height: number, nativeTag: number, }; + +findPseudoElementShadowNodeByTag: (reactTag: number) => ?unknown /* Node */; } export default TurboModuleRegistry.get( From eefb2d8545a9eb89b2a55696df03bec797d31b24 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 4/8] Add bitmap snapshot capture/display delegate plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Wire view transition bitmap snapshot capture and display through the C++ delegate chain so platforms can implement snapshotting old-element views. **ViewTransitionModule.cpp**: Call `uiManagerDidCaptureViewSnapshot(tag, surfaceId)` in `applyViewTransitionName` to capture a bitmap of the old view while it is still mounted, and `uiManagerDidSetViewSnapshot(sourceTag, targetTag, surfaceId)` in `applySnapshotsOnPseudoElementShadowNodes` to map captured bitmaps onto pseudo-element views. **UIManagerDelegate / SchedulerDelegate**: Add pure virtual `captureViewSnapshot` and `setViewSnapshot` methods. **Scheduler**: Forward `UIManagerDelegate` calls to `SchedulerDelegate`. **Platform stubs**: Empty implementations for iOS (`RCTScheduler.mm`), CxxPlatform (`SchedulerDelegateImpl`), and Android JNI (`FabricUIManagerBinding`) — Android implementation follows in D99173446. Differential Revision: D98354659 --- .../react-native/React/Fabric/RCTScheduler.mm | 18 +++++++++++ .../react/fabric/FabricUIManagerBinding.cpp | 17 +++++++++++ .../jni/react/fabric/FabricUIManagerBinding.h | 6 ++++ .../react/renderer/scheduler/Scheduler.cpp | 21 +++++++++++++ .../react/renderer/scheduler/Scheduler.h | 3 ++ .../renderer/scheduler/SchedulerDelegate.h | 5 ++++ .../renderer/uimanager/UIManagerDelegate.h | 5 ++++ .../viewtransition/ViewTransitionModule.cpp | 30 +++++++++++++++++-- .../scheduler/SchedulerDelegateImpl.cpp | 11 +++++++ .../scheduler/SchedulerDelegateImpl.h | 6 ++++ .../api-snapshots/ReactAndroidDebugCxx.api | 18 ++++++++++- .../api-snapshots/ReactAndroidReleaseCxx.api | 18 ++++++++++- .../api-snapshots/ReactAppleDebugCxx.api | 18 ++++++++++- .../api-snapshots/ReactAppleReleaseCxx.api | 18 ++++++++++- .../api-snapshots/ReactCommonDebugCxx.api | 18 ++++++++++- .../api-snapshots/ReactCommonReleaseCxx.api | 18 ++++++++++- 16 files changed, 222 insertions(+), 8 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTScheduler.mm b/packages/react-native/React/Fabric/RCTScheduler.mm index 408ed9545b2d..9519f18428e4 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.mm +++ b/packages/react-native/React/Fabric/RCTScheduler.mm @@ -85,6 +85,24 @@ void schedulerDidUpdateShadowTree(const std::unordered_map // This delegate method is not currently used on iOS. } + void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override + { + // Does nothing. + // View transition snapshots are not currently implemented on iOS. + } + + void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override + { + // Does nothing. + // View transition snapshots are not currently implemented on iOS. + } + + void schedulerDidClearPendingSnapshots() override + { + // Does nothing. + // View transition snapshots are not currently implemented on iOS. + } + private: void *scheduler_; }; diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index 4abc6b79b379..5b971cde5300 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -797,6 +797,23 @@ void FabricUIManagerBinding::schedulerDidUpdateShadowTree( // no-op } +void FabricUIManagerBinding::schedulerDidCaptureViewSnapshot( + Tag tag, + SurfaceId surfaceId) { + // TODO: implement this +} + +void FabricUIManagerBinding::schedulerDidSetViewSnapshot( + Tag sourceTag, + Tag targetTag, + SurfaceId surfaceId) { + // TODO: implement this +} + +void FabricUIManagerBinding::schedulerDidClearPendingSnapshots() { + // TODO: implement this +} + void FabricUIManagerBinding::onAnimationStarted() { auto mountingManager = getMountingManager("onAnimationStarted"); if (!mountingManager) { diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index 6d129e7ffeaa..caf652d2352e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -117,6 +117,12 @@ class FabricUIManagerBinding : public jni::HybridClass, void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; + void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; + + void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override; + + void schedulerDidClearPendingSnapshots() override; + void setPixelDensity(float pointScaleFactor); void driveCxxAnimations(); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 477ecb8a8820..28750e02d16f 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -364,6 +364,27 @@ void Scheduler::uiManagerDidUpdateShadowTree( } } +void Scheduler::uiManagerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) { + if (delegate_ != nullptr) { + delegate_->schedulerDidCaptureViewSnapshot(tag, surfaceId); + } +} + +void Scheduler::uiManagerDidSetViewSnapshot( + Tag sourceTag, + Tag targetTag, + SurfaceId surfaceId) { + if (delegate_ != nullptr) { + delegate_->schedulerDidSetViewSnapshot(sourceTag, targetTag, surfaceId); + } +} + +void Scheduler::uiManagerDidClearPendingSnapshots() { + if (delegate_ != nullptr) { + delegate_->schedulerDidClearPendingSnapshots(); + } +} + void Scheduler::uiManagerShouldAddEventListener( std::shared_ptr listener) { addEventListener(listener); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index 00ed0f43ed06..cc665dfde4b5 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -94,6 +94,9 @@ class Scheduler final : public UIManagerDelegate { bool blockNativeResponder) override; void uiManagerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; void uiManagerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; + void uiManagerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; + void uiManagerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override; + void uiManagerDidClearPendingSnapshots() override; void uiManagerShouldAddEventListener(std::shared_ptr listener) final; void uiManagerShouldRemoveEventListener(const std::shared_ptr &listener) final; void uiManagerDidFinishReactCommit(const ShadowTree &shadowTree) override; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h index fafb5f90f297..06d9773e4ba5 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerDelegate.h @@ -66,6 +66,11 @@ class SchedulerDelegate { virtual void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) = 0; + // View transition bitmap snapshot capture and application. + virtual void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) = 0; + virtual void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) = 0; + virtual void schedulerDidClearPendingSnapshots() = 0; + virtual ~SchedulerDelegate() noexcept = default; }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h index 92ee339f0929..235b1fe34eac 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h @@ -97,6 +97,11 @@ class UIManagerDelegate { using OnSurfaceStartCallback = std::function; virtual void uiManagerShouldSetOnSurfaceStartCallback(OnSurfaceStartCallback &&callback) = 0; + // View transition bitmap snapshot capture and application. + virtual void uiManagerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) = 0; + virtual void uiManagerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) = 0; + virtual void uiManagerDidClearPendingSnapshots() = 0; + virtual ~UIManagerDelegate() noexcept = default; }; diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index d67ad6e24a88..ee14c7bd5851 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -82,7 +82,14 @@ void ViewTransitionModule::applyViewTransitionName( .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; oldLayout_[name] = oldView; - // TODO: capture bitmap snapshot of old view via platform + // Request the platform to capture a bitmap snapshot of the old view + // while it's still mounted. The platform stores the bitmap keyed by tag. + if (uiManager_ != nullptr) { + auto* delegate = uiManager_->getDelegate(); + if (delegate != nullptr) { + delegate->uiManagerDidCaptureViewSnapshot(tag, surfaceId); + } + } if (auto it = oldPseudoElementNodesRepository_.find(name); it != oldPseudoElementNodesRepository_.end()) { @@ -277,7 +284,18 @@ void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { return; } - // TODO: set bitmap snapshots on pseudo-element views via platform + // Set view snapshots — the pseudo-element nodes themselves will be committed + // through the normal completeRoot flow via getPseudoElementNodes(). + auto* delegate = uiManager_->getDelegate(); + if (delegate != nullptr) { + for (const auto& [name, node] : oldPseudoElementNodes_) { + auto layoutIt = oldLayout_.find(name); + if (layoutIt != oldLayout_.end()) { + delegate->uiManagerDidSetViewSnapshot( + layoutIt->second.tag, node->getTag(), node->getSurfaceId()); + } + } + } } LayoutMetrics ViewTransitionModule::captureLayoutMetricsFromRoot( @@ -343,6 +361,14 @@ void ViewTransitionModule::startViewTransitionEnd() { nameRegistry_.clear(); oldPseudoElementNodes_.clear(); + // Clear any pending bitmap snapshots that were captured but never consumed. + if (uiManager_ != nullptr) { + auto* delegate = uiManager_->getDelegate(); + if (delegate != nullptr) { + delegate->uiManagerDidClearPendingSnapshots(); + } + } + transitionStarted_ = false; } diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp index dcc1df6a5cdf..9ad1bb1a4f54 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp +++ b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp @@ -76,4 +76,15 @@ void SchedulerDelegateImpl::schedulerDidUpdateShadowTree( mountingManager_->onUpdateShadowTree(tagToProps); } +void SchedulerDelegateImpl::schedulerDidCaptureViewSnapshot( + Tag /*tag*/, + SurfaceId /*surfaceId*/) {} + +void SchedulerDelegateImpl::schedulerDidSetViewSnapshot( + Tag /*sourceTag*/, + Tag /*targetTag*/, + SurfaceId /*surfaceId*/) {} + +void SchedulerDelegateImpl::schedulerDidClearPendingSnapshots() {} + } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h index b60f33962ce9..089b64d5731c 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h +++ b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h @@ -51,6 +51,12 @@ class SchedulerDelegateImpl : public SchedulerDelegate { void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; + void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; + + void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override; + + void schedulerDidClearPendingSnapshots() override; + std::shared_ptr mountingManager_; std::shared_ptr uiManager_; }; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 0262c22621f0..61b687ff5d11 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4453,6 +4453,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -4460,6 +4462,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -4477,11 +4480,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -5250,6 +5256,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -5257,6 +5265,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -5284,8 +5293,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -5358,10 +5369,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index e8bcf55187c8..c0ddff83a20f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4450,6 +4450,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -4457,6 +4459,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -4474,11 +4477,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -5241,6 +5247,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -5248,6 +5256,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -5275,8 +5284,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -5349,10 +5360,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index c10403dd56f2..314ad5098fe1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -7040,6 +7040,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -7047,6 +7049,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -7064,11 +7067,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -7820,6 +7826,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -7827,6 +7835,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -7854,8 +7863,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -7925,10 +7936,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index a9965274a91c..69fd72ccabf0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -7037,6 +7037,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -7044,6 +7046,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -7061,11 +7064,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -7811,6 +7817,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -7818,6 +7826,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -7845,8 +7854,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -7916,10 +7927,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 52daa8ae3342..db5b97440f2c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3021,6 +3021,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -3028,6 +3030,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -3045,11 +3048,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -3717,6 +3723,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -3724,6 +3732,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -3751,8 +3760,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -3813,10 +3824,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 85bf37ad00bd..f35367f460b8 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3018,6 +3018,8 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public facebook::react::SchedulerDelegate* getDelegate() const; public std::shared_ptr getContextContainer() const; public std::shared_ptr getUIManager() const; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) override; + public virtual void uiManagerDidClearPendingSnapshots() override; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) override; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) override; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) override; @@ -3025,6 +3027,7 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) override; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) override; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) override; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) override; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) override; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) final; @@ -3042,11 +3045,14 @@ class facebook::react::Scheduler : public facebook::react::UIManagerDelegate { } class facebook::react::SchedulerDelegate { + public virtual void schedulerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void schedulerDidClearPendingSnapshots() = 0; public virtual void schedulerDidDispatchCommand(const facebook::react::ShadowView& shadowView, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void schedulerDidFinishTransaction(const std::shared_ptr& mountingCoordinator) = 0; public virtual void schedulerDidRequestPreliminaryViewAllocation(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void schedulerDidSendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType) = 0; public virtual void schedulerDidSetIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void schedulerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void schedulerShouldMergeReactRevision(facebook::react::SurfaceId surfaceId) = 0; public virtual void schedulerShouldRenderTransactions(const std::shared_ptr& mountingCoordinator) = 0; @@ -3708,6 +3714,8 @@ class facebook::react::UIManagerCommitHook { class facebook::react::UIManagerDelegate { public using OnSurfaceStartCallback = std::function; + public virtual void uiManagerDidCaptureViewSnapshot(facebook::react::Tag tag, facebook::react::SurfaceId surfaceId) = 0; + public virtual void uiManagerDidClearPendingSnapshots() = 0; public virtual void uiManagerDidCreateShadowNode(const facebook::react::ShadowNode& shadowNode) = 0; public virtual void uiManagerDidDispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) = 0; public virtual void uiManagerDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) = 0; @@ -3715,6 +3723,7 @@ class facebook::react::UIManagerDelegate { public virtual void uiManagerDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidSendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType) = 0; public virtual void uiManagerDidSetIsJSResponder(const std::shared_ptr& shadowNode, bool isJSResponder, bool blockNativeResponder) = 0; + public virtual void uiManagerDidSetViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId) = 0; public virtual void uiManagerDidStartSurface(const facebook::react::ShadowTree& shadowTree) = 0; public virtual void uiManagerDidUpdateShadowTree(const std::unordered_map& tagToProps) = 0; public virtual void uiManagerShouldAddEventListener(std::shared_ptr listener) = 0; @@ -3742,8 +3751,10 @@ class facebook::react::UIManagerNativeAnimatedDelegateImpl : public facebook::re class facebook::react::UIManagerViewTransitionDelegate { public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo); + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className); public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name); + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag); public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode); public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback); public virtual void startViewTransitionEnd(); @@ -3804,10 +3815,15 @@ class facebook::react::ViewShadowNodeProps : public facebook::react::HostPlatfor public ViewShadowNodeProps(const facebook::react::PropsParserContext& context, const facebook::react::ViewShadowNodeProps& sourceProps, const facebook::react::RawProps& rawProps); } -class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate { +class facebook::react::ViewTransitionModule : public facebook::react::UIManagerViewTransitionDelegate, public facebook::react::UIManagerCommitHook { + public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) noexcept override; public virtual std::optional getViewTransitionInstance(const std::string& name, const std::string& pseudo) override; + public virtual std::shared_ptr findPseudoElementShadowNodeByTag(facebook::react::Tag tag) const override; public virtual void applyViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name, const std::string& className) override; public virtual void cancelViewTransitionName(const facebook::react::ShadowNode& shadowNode, const std::string& name) override; + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void createViewTransitionInstance(const std::string& name, facebook::react::Tag pseudoElementTag) override; public virtual void restoreViewTransitionName(const facebook::react::ShadowNode& shadowNode) override; public virtual void startViewTransition(std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) override; public virtual void startViewTransitionEnd() override; From 5369b8bd591af3cdb1289c318fd2b8fc5e18eca9 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 5/8] Android bitmap snapshot capture and display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Implement the Android side of view transition bitmap snapshots. **`ViewTransitionSnapshotManager`** (new Kotlin class): Manages the full snapshot lifecycle as a `UIManagerListener`. Captures bitmaps on the UI thread, maps them from source to target pseudo-element tags, re-applies after mount cycles (since views may be recreated), and self-cleans when views are deleted. **JNI bridge:** `FabricUIManagerBinding` → `FabricMountingManager` → `FabricUIManager` JNI delegates to the snapshot manager. **`SurfaceMountingManager.applyViewSnapshot`:** sets bitmap as view background using the KTX `Bitmap.toDrawable` extension. Differential Revision: D99173446 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/fabric/FabricUIManager.java | 36 ++++ .../fabric/ViewTransitionSnapshotManager.kt | 186 ++++++++++++++++++ .../fabric/mounting/SurfaceMountingManager.kt | 9 + .../react/fabric/FabricMountingManager.cpp | 24 +++ .../jni/react/fabric/FabricMountingManager.h | 6 + .../react/fabric/FabricUIManagerBinding.cpp | 12 +- .../api-snapshots/ReactAndroidDebugCxx.api | 1 + .../api-snapshots/ReactAndroidReleaseCxx.api | 1 + 9 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 56b7c520ffd6..104d0feefee3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2275,6 +2275,7 @@ public final class com/facebook/react/fabric/FabricUIManagerProviderImpl : com/f public final class com/facebook/react/fabric/mounting/SurfaceMountingManager { public final fun addViewAt (III)V + public final fun applyViewSnapshot (ILandroid/graphics/Bitmap;)V public final fun attachRootView (Landroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V public final fun deleteView (I)V public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 64d9655cba59..f6658992ec41 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -208,6 +208,8 @@ public class FabricUIManager private boolean mDriveCxxAnimations = false; + private @Nullable ViewTransitionSnapshotManager mViewTransitionSnapshotManager; + private long mDispatchViewUpdatesTime = 0l; private long mCommitStartTime = 0l; private long mLayoutTime = 0l; @@ -811,6 +813,40 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); } + /** Called from C++ via JNI. */ + @SuppressLint("NotInvokedPrivateMethod") + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private void captureViewSnapshot(final int reactTag, final int surfaceId) { + getViewTransitionSnapshotManager().captureViewSnapshot(reactTag, surfaceId); + } + + /** Called from C++ via JNI. */ + @SuppressLint("NotInvokedPrivateMethod") + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private void setViewSnapshot(final int sourceTag, final int targetTag, final int surfaceId) { + getViewTransitionSnapshotManager().setViewSnapshot(sourceTag, targetTag); + } + + /** Called from C++ via JNI. */ + @SuppressLint("NotInvokedPrivateMethod") + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private void clearPendingSnapshots() { + getViewTransitionSnapshotManager().clearPendingSnapshots(); + } + + private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotManager() { + if (mViewTransitionSnapshotManager == null) { + mViewTransitionSnapshotManager = new ViewTransitionSnapshotManager(this, mMountingManager); + } + return mViewTransitionSnapshotManager; + } + @SuppressLint("NotInvokedPrivateMethod") @SuppressWarnings("unused") @AnyThread diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt new file mode 100644 index 000000000000..efd963165358 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.fabric + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.PixelCopy +import android.view.View +import android.view.Window +import androidx.annotation.RequiresApi +import androidx.annotation.UiThread +import androidx.core.graphics.createBitmap +import com.facebook.infer.annotation.ThreadConfined +import com.facebook.react.bridge.UIManager +import com.facebook.react.bridge.UIManagerListener +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.fabric.mounting.MountingManager + +/** + * Manages bitmap snapshots of views during view transitions. Captures bitmaps from old views and + * applies them to pseudo-element shadow nodes, re-applying after each mount cycle since views may + * be recreated. Cleans up entries whose views have been deleted. + */ +@OptIn(UnstableReactNativeAPI::class) +internal class ViewTransitionSnapshotManager( + private val uiManager: FabricUIManager, + private val mountingManager: MountingManager, +) : UIManagerListener { + + companion object { + private fun captureSoftwareBitmap(view: View): Bitmap { + val bitmap = createBitmap(view.width, view.height) + view.draw(Canvas(bitmap)) + return bitmap + } + } + + // Captured bitmaps keyed by source tag. Populated by onBitmapCaptured. + @ThreadConfined(ThreadConfined.UI) private val viewSnapshots = LinkedHashMap() + + // Source→target tag mapping. Populated by setViewSnapshot. + // A snapshot is resolved when both maps contain an entry for the same source tag. + @ThreadConfined(ThreadConfined.UI) private val pendingTargets = LinkedHashMap() + + @ThreadConfined(ThreadConfined.UI) private var listenerRegistered = false + + private val mainHandler = Handler(Looper.getMainLooper()) + + @UiThread + private fun onBitmapCaptured(reactTag: Int, bitmap: Bitmap) { + viewSnapshots[reactTag] = bitmap + if (reactTag in pendingTargets) { + ensureListenerRegistered() + } + } + + @UiThread + private fun ensureListenerRegistered() { + if (!listenerRegistered) { + listenerRegistered = true + uiManager.addUIManagerEventListener(this) + } + } + + /** + * Captures a bitmap snapshot of the view identified by the given tag. On API 26+, uses PixelCopy + * to capture directly from the GPU-composited surface (faster for complex views, captures + * hardware-accelerated content). Falls back to View.draw() on older APIs. + */ + fun captureViewSnapshot(reactTag: Int, surfaceId: Int) { + UiThreadUtil.runOnUiThread { + val smm = mountingManager.getSurfaceManager(surfaceId) ?: return@runOnUiThread + if (!smm.getViewExists(reactTag)) return@runOnUiThread + val view = smm.getView(reactTag) + if (view.width <= 0 || view.height <= 0) return@runOnUiThread + + val window = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + (view.context as? com.facebook.react.bridge.ReactContext)?.getCurrentActivity()?.window + } else { + null + } + + if (window != null) { + captureHardwareBitmap(view, reactTag, window) + } else { + // Software fallback runs synchronously, so onBitmapCaptured always + // completes before setViewSnapshot is called. + onBitmapCaptured(reactTag, captureSoftwareBitmap(view)) + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun captureHardwareBitmap(view: View, reactTag: Int, window: Window) { + val bitmap = createBitmap(view.width, view.height) + val location = IntArray(2) + view.getLocationInWindow(location) + val rect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height) + // PixelCopy callback is posted to mainHandler, so onBitmapCaptured may run after + // setViewSnapshot has already recorded the target tag for this source tag. + try { + PixelCopy.request( + window, + rect, + bitmap, + { copyResult -> + if (copyResult == PixelCopy.SUCCESS) { + val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) + if (hwBitmap != null) { + bitmap.recycle() + onBitmapCaptured(reactTag, hwBitmap) + } else { + onBitmapCaptured(reactTag, bitmap) + } + } else { + bitmap.recycle() + onBitmapCaptured(reactTag, captureSoftwareBitmap(view)) + } + }, + mainHandler, + ) + } catch (e: IllegalArgumentException) { + // Window surface may have been destroyed (e.g., device idle/sleep). + // Fall back to software rendering. + bitmap.recycle() + onBitmapCaptured(reactTag, captureSoftwareBitmap(view)) + } + } + + /** + * Maps a previously captured bitmap from a source view to a target pseudo-element view. If the + * bitmap is already available, the snapshot becomes resolved and will be re-applied after mount + * cycles. + */ + fun setViewSnapshot(sourceTag: Int, targetTag: Int) { + UiThreadUtil.runOnUiThread { + pendingTargets[sourceTag] = targetTag + if (sourceTag in viewSnapshots) { + ensureListenerRegistered() + } + } + } + + /** + * Clears all snapshots. Called when a view transition ends to release bitmaps and unregister the + * mount listener. + */ + fun clearPendingSnapshots() { + UiThreadUtil.runOnUiThread { + viewSnapshots.clear() + pendingTargets.clear() + if (listenerRegistered) { + listenerRegistered = false + uiManager.removeUIManagerEventListener(this) + } + } + } + + override fun willDispatchViewUpdates(uiManager: UIManager) {} + + override fun willMountItems(uiManager: UIManager) {} + + @UiThread + override fun didMountItems(uiManager: UIManager) { + for ((sourceTag, targetTag) in pendingTargets) { + val smm = mountingManager.getSurfaceManagerForView(targetTag) ?: continue + val bitmap = viewSnapshots[sourceTag] ?: continue + smm.applyViewSnapshot(targetTag, bitmap) + } + } + + override fun didDispatchMountItems(uiManager: UIManager) {} + + override fun didScheduleMountItems(uiManager: UIManager) {} +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index c09962b407cd..b535c10ce5aa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -8,6 +8,7 @@ package com.facebook.react.fabric.mounting import android.annotation.SuppressLint +import android.graphics.Bitmap import android.os.SystemClock import android.view.View import android.view.ViewGroup @@ -15,6 +16,7 @@ import android.view.ViewParent import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.collection.SparseArrayCompat +import androidx.core.graphics.drawable.toDrawable import com.facebook.common.logging.FLog import com.facebook.infer.annotation.ThreadConfined import com.facebook.react.bridge.GuardedRunnable @@ -1089,6 +1091,13 @@ internal constructor( private fun getNullableViewState(reactTag: Int): ViewState? = tagToViewState[reactTag] + /** Applies a bitmap as the background of the view with the given tag, if it exists. */ + @UiThread + public fun applyViewSnapshot(tag: Int, bitmap: Bitmap) { + val view = getNullableViewState(tag)?.view ?: return + view.background = bitmap.toDrawable(view.resources) + } + public fun printSurfaceState(): Unit { FLog.e(TAG, "Views created for surface $surfaceId:") for (viewState in tagToViewState.values) { diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index 2eb8986cb70c..b82bdab278c2 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -1229,6 +1229,30 @@ void FabricMountingManager::synchronouslyUpdateViewOnUIThread( synchronouslyUpdateViewOnUIThreadJNI(javaUIManager_, viewTag, propsMap); } +void FabricMountingManager::captureViewSnapshot(Tag tag, SurfaceId surfaceId) { + static auto captureViewSnapshotJNI = + JFabricUIManager::javaClassStatic()->getMethod( + "captureViewSnapshot"); + captureViewSnapshotJNI(javaUIManager_, tag, surfaceId); +} + +void FabricMountingManager::setViewSnapshot( + Tag sourceTag, + Tag targetTag, + SurfaceId surfaceId) { + static auto setViewSnapshotJNI = + JFabricUIManager::javaClassStatic()->getMethod( + "setViewSnapshot"); + setViewSnapshotJNI(javaUIManager_, sourceTag, targetTag, surfaceId); +} + +void FabricMountingManager::clearPendingSnapshots() { + static auto clearPendingSnapshotsJNI = + JFabricUIManager::javaClassStatic()->getMethod( + "clearPendingSnapshots"); + clearPendingSnapshotsJNI(javaUIManager_); +} + void FabricMountingManager::scheduleReactRevisionMerge(SurfaceId surfaceId) { static const auto scheduleReactRevisionMerge = JFabricUIManager::javaClassStatic()->getMethod( diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 50c80646783c..377880b658bb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -68,6 +68,12 @@ class FabricMountingManager final { void synchronouslyUpdateViewOnUIThread(Tag viewTag, const folly::dynamic &props); + void captureViewSnapshot(Tag tag, SurfaceId surfaceId); + + void setViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId); + + void clearPendingSnapshots(); + void scheduleReactRevisionMerge(SurfaceId surfaceId); private: diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index 5b971cde5300..839efa3dce53 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -800,18 +800,24 @@ void FabricUIManagerBinding::schedulerDidUpdateShadowTree( void FabricUIManagerBinding::schedulerDidCaptureViewSnapshot( Tag tag, SurfaceId surfaceId) { - // TODO: implement this + if (mountingManager_) { + mountingManager_->captureViewSnapshot(tag, surfaceId); + } } void FabricUIManagerBinding::schedulerDidSetViewSnapshot( Tag sourceTag, Tag targetTag, SurfaceId surfaceId) { - // TODO: implement this + if (mountingManager_) { + mountingManager_->setViewSnapshot(sourceTag, targetTag, surfaceId); + } } void FabricUIManagerBinding::schedulerDidClearPendingSnapshots() { - // TODO: implement this + if (mountingManager_) { + mountingManager_->clearPendingSnapshots(); + } } void FabricUIManagerBinding::onAnimationStarted() { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 61b687ff5d11..725830820727 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2325,6 +2325,7 @@ class facebook::react::FabricMountingManager { public void scheduleReactRevisionMerge(facebook::react::SurfaceId surfaceId); public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType); public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder); + public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index c0ddff83a20f..3d481c044fc5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2323,6 +2323,7 @@ class facebook::react::FabricMountingManager { public void scheduleReactRevisionMerge(facebook::react::SurfaceId surfaceId); public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType); public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder); + public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId); public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props); public ~FabricMountingManager(); } From b73ec30ee0cc5b7afb96bf3c559413293bfae1e5 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 6/8] Suspend overlapping view transitions Summary: When a new view transition starts while another is still active, queue it instead of running immediately. The queued transition runs after the current one finishes via `startViewTransitionEnd`. - Add `suspendOnActiveViewTransition()` to `UIManagerViewTransitionDelegate`, exposed as a method on `nativeFabricUIManager` so the reconciler can signal suspension - `ViewTransitionModule` queues pending transitions in a `std::queue` when `suspendNextTransition_` is set - `startViewTransitionEnd` drains the queue sequentially, each transition triggering the next on completion Differential Revision: D99366975 --- .../renderer/uimanager/UIManagerBinding.cpp | 18 ++++++++++ .../UIManagerViewTransitionDelegate.h | 4 +++ .../viewtransition/ViewTransitionModule.cpp | 36 +++++++++++++++++++ .../viewtransition/ViewTransitionModule.h | 14 ++++++++ 4 files changed, 72 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 9b6b08f4642b..3735852dc419 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1074,6 +1074,24 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "suspendOnActiveViewTransition") { + return jsi::Function::createFromHostFunction( + runtime, + name, + 0, + [uiManager]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* /*arguments*/, + size_t /*count*/) -> jsi::Value { + auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->suspendOnActiveViewTransition(); + } + return jsi::Value::undefined(); + }); + } + if (methodName == "startViewTransition") { auto paramCount = 1; return jsi::Function::createFromHostFunction( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index d3685775c353..a21cd4e3d00f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -67,6 +67,10 @@ class UIManagerViewTransitionDelegate { { return nullptr; } + + // Called by the reconciler to signal that the next view transition should + // be suspended until the currently active one finishes. + virtual void suspendOnActiveViewTransition() {} }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index ee14c7bd5851..2aa9e80f0838 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -328,6 +328,21 @@ void ViewTransitionModule::startViewTransition( std::function mutationCallback, std::function onReadyCallback, std::function onCompleteCallback) { + // If the reconciler signalled suspension and a transition is still active, + // queue this transition to run after the current one finishes. + // Only queue if the previous transition is still running; if it already + // finished, the flag is stale and we should run normally. + if (suspendNextTransition_ && transitionStarted_) { + suspendNextTransition_ = false; + pendingTransitions_.push( + PendingTransition{ + std::move(mutationCallback), + std::move(onReadyCallback), + std::move(onCompleteCallback)}); + return; + } + suspendNextTransition_ = false; + // Mark transition as started transitionStarted_ = true; @@ -351,6 +366,15 @@ void ViewTransitionModule::startViewTransition( } } +void ViewTransitionModule::suspendOnActiveViewTransition() { + // Signal that the next transition should be suspended until the current + // one finishes. The actual queueing happens in startViewTransition. + if (transitionStarted_) { + // if there's no active transition, suspendOnActiveViewTransition is no-op + suspendNextTransition_ = true; + } +} + void ViewTransitionModule::startViewTransitionEnd() { for (const auto& [tag, names] : nameRegistry_) { for (const auto& name : names) { @@ -370,6 +394,18 @@ void ViewTransitionModule::startViewTransitionEnd() { } transitionStarted_ = false; + + if (!pendingTransitions_.empty()) { + auto pendingTransition = pendingTransitions_.front(); + pendingTransitions_.pop(); + startViewTransition( + std::move(pendingTransition.mutationCallback), + std::move(pendingTransition.onReadyCallback), + std::move(pendingTransition.onCompleteCallback)); + // when this transition finishes, it'll call startViewTransitionEnd + // during its complete callback and pendingTransitions_ will be processed + // again + } } std::optional diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index ae585978b409..7e8a332ee24a 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -78,6 +79,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, std::shared_ptr findPseudoElementShadowNodeByTag(Tag tag) const override; + void suspendOnActiveViewTransition() override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -124,6 +127,17 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, UIManager *uiManager_{nullptr}; bool transitionStarted_{false}; + + // When suspendNextTransition_ is true and a transition is active, the next + // startViewTransition calls are queued instead of running immediately. + bool suspendNextTransition_{false}; + + struct PendingTransition { + std::function mutationCallback; + std::function onReadyCallback; + std::function onCompleteCallback; + }; + std::queue pendingTransitions_{}; }; } // namespace facebook::react From bf02fd28e50d1e48e36fce10fc235145c6921b4c Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 11:50:51 -0700 Subject: [PATCH 7/8] Add startViewTransitionReadyFinished callback Summary: Add `startViewTransitionReadyFinished()` to the view transition delegate so the reconciler can notify the native side when the ready callback's async work (e.g. spawned work from the `ready` promise `.then`) has completed. This was previously not capturable from the C++ side since `onReadyCallback` resolves the JS promise synchronously but the reconciler's follow-up work runs as a microtask. - Add `startViewTransitionReadyFinished` to `UIManagerViewTransitionDelegate` and `ViewTransitionModule` - Expose as a method on `nativeFabricUIManager` via `UIManagerBinding` - Track `transitionReadyFinished_` state, reset to `false` before `onReadyCallback` and set to `true` when the reconciler calls back Differential Revision: D99443648 --- .../renderer/uimanager/UIManagerBinding.cpp | 18 ++++++++++++++++++ .../UIManagerViewTransitionDelegate.h | 2 ++ .../viewtransition/ViewTransitionModule.cpp | 6 +++++- .../viewtransition/ViewTransitionModule.h | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 3735852dc419..c79fc75df5b4 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1092,6 +1092,24 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "startViewTransitionReadyFinished") { + return jsi::Function::createFromHostFunction( + runtime, + name, + 0, + [uiManager]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* /*arguments*/, + size_t /*count*/) -> jsi::Value { + auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->startViewTransitionReadyFinished(); + } + return jsi::Value::undefined(); + }); + } + if (methodName == "startViewTransition") { auto paramCount = 1; return jsi::Function::createFromHostFunction( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index a21cd4e3d00f..e82107992127 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -41,6 +41,8 @@ class UIManagerViewTransitionDelegate { { } + virtual void startViewTransitionReadyFinished() {} + virtual void startViewTransitionEnd() {} struct ViewTransitionInstance { diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 2aa9e80f0838..3cda3cdc4d52 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -353,7 +353,7 @@ void ViewTransitionModule::startViewTransition( } applySnapshotsOnPseudoElementShadowNodes(); - + transitionReadyFinished_ = false; if (onReadyCallback) { onReadyCallback(); } @@ -366,6 +366,10 @@ void ViewTransitionModule::startViewTransition( } } +void ViewTransitionModule::startViewTransitionReadyFinished() { + transitionReadyFinished_ = true; +} + void ViewTransitionModule::suspendOnActiveViewTransition() { // Signal that the next transition should be suspended until the current // one finishes. The actual queueing happens in startViewTransition. diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index 7e8a332ee24a..33331ba3c4fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -53,6 +53,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, std::function onReadyCallback, std::function onCompleteCallback) override; + void startViewTransitionReadyFinished() override; + void startViewTransitionEnd() override; std::optional getViewTransitionInstance(const std::string &name, const std::string &pseudo) @@ -128,6 +130,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, bool transitionStarted_{false}; + bool transitionReadyFinished_{false}; + // When suspendNextTransition_ is true and a transition is active, the next // startViewTransition calls are queued instead of running immediately. bool suspendNextTransition_{false}; From 9fc2e8e198ac28165ce33af36519a714e4441772 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 13:31:09 -0700 Subject: [PATCH 8/8] Allow waiting for user land Animated animations before firing transition complete callback (#56459) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/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 --- .../viewtransition/NativeViewTransition.cpp | 22 +++++++++++++++ .../viewtransition/NativeViewTransition.h | 4 +++ .../UIManagerViewTransitionDelegate.h | 4 +++ .../viewtransition/ViewTransitionModule.cpp | 28 ++++++++++++++++--- .../viewtransition/ViewTransitionModule.h | 10 +++++++ .../specs/NativeViewTransition.js | 2 ++ 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp index 8190c7c2f76b..6add3a25cdfe 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp @@ -67,4 +67,26 @@ jsi::Value NativeViewTransition::findPseudoElementShadowNodeByTag( return jsi::Value::null(); } +void NativeViewTransition::waitForTransitionAnimation( + jsi::Runtime& rt, + double animationId) { + auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager(); + auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->waitForTransitionAnimation( + static_cast(animationId)); + } +} + +void NativeViewTransition::transitionAnimationFinished( + jsi::Runtime& rt, + double animationId) { + auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager(); + auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->transitionAnimationFinished( + static_cast(animationId)); + } +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h index f3311f40bef3..55593f7c2f60 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h @@ -28,6 +28,10 @@ class NativeViewTransition : public NativeViewTransitionCxxSpec onCompleteCallback; }; std::queue pendingTransitions_{}; + + // Tracks animation IDs that must complete before onCompleteCallback_ fires. + // Animations are registered via waitForTransitionAnimation (called from JS + // after connectAnimatedNodeToView) and removed via transitionAnimationFinished. + std::unordered_set pendingAnimationIds_{}; + std::function onCompleteCallback_{nullptr}; }; } // namespace facebook::react diff --git a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js index c9fa79cd3639..c3bebd473528 100644 --- a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js +++ b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js @@ -24,6 +24,8 @@ export interface Spec extends TurboModule { nativeTag: number, }; +findPseudoElementShadowNodeByTag: (reactTag: number) => ?unknown /* Node */; + +waitForTransitionAnimation: (animationId: number) => void; + +transitionAnimationFinished: (animationId: number) => void; } export default TurboModuleRegistry.get(