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};