From fe724a370d1f53f1d731d9c0e497ee71ee6b0417 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 25 Feb 2026 11:27:56 -0800 Subject: [PATCH 1/2] Add UIManagerViewTransitionDelegate interface and View Transition APIs (#55742) Summary: Changelog: [General] [Added] - Add UIManagerViewTransitionDelegate interface and View Transition APIs Adds `UIManagerViewTransitionDelegate` interface and View Transition APIs to UIManager, enabling integration with React reconciler's `` component. Exposes JSI bindings in UIManagerBinding, which will be consumed by react fabric renderer (https://github.com/facebook/react/pull/35764) - `measureInstance` - returns layout metrics and calls `captureLayoutMetricsFromRoot` to capture snapshot - `applyViewTransitionName` / `cancelViewTransitionName` / `restoreViewTransitionName` - manage transition name registration - `startViewTransition` - orchestrates transition lifecycle with mutation/ready/complete callbacks The delegate methods are gated by the `viewTransitionEnabled` feature flag. Reviewed By: sammy-SC Differential Revision: D92537193 --- .../react/renderer/uimanager/UIManager.cpp | 11 + .../react/renderer/uimanager/UIManager.h | 8 + .../renderer/uimanager/UIManagerBinding.cpp | 250 ++++++++++++++++++ .../UIManagerViewTransitionDelegate.h | 37 +++ 4 files changed, 306 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 815283ff8f83..3f079ebe2289 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -488,6 +488,10 @@ void UIManager::sendAccessibilityEvent( } } +UIManagerViewTransitionDelegate* UIManager::getViewTransitionDelegate() const { + return viewTransitionDelegate_; +} + void UIManager::configureNextLayoutAnimation( jsi::Runtime& runtime, const RawValue& config, @@ -698,6 +702,13 @@ void UIManager::setNativeAnimatedDelegate( nativeAnimatedDelegate_ = delegate; } +void UIManager::setViewTransitionDelegate( + UIManagerViewTransitionDelegate* delegate) { + if (ReactNativeFeatureFlags::viewTransitionEnabled()) { + viewTransitionDelegate_ = delegate; + } +} + void UIManager::unstable_setAnimationBackend( std::shared_ptr animationBackend) { animationBackend_ = animationBackend; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 4140457aefb8..28b8729e3e01 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +75,12 @@ class UIManager final : public ShadowTreeDelegate { void setNativeAnimatedDelegate(std::weak_ptr delegate); + /** + * Sets and gets UIManager's ViewTransition API delegate. + */ + void setViewTransitionDelegate(UIManagerViewTransitionDelegate *delegate); + UIManagerViewTransitionDelegate *getViewTransitionDelegate() const; + void animationTick() const; void synchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props); @@ -242,6 +249,7 @@ class UIManager final : public ShadowTreeDelegate { UIManagerDelegate *delegate_{}; UIManagerAnimationDelegate *animationDelegate_{nullptr}; std::weak_ptr nativeAnimatedDelegate_; + UIManagerViewTransitionDelegate *viewTransitionDelegate_{nullptr}; const RuntimeExecutor runtimeExecutor_{}; ShadowTreeRegistry shadowTreeRegistry_{}; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 5f661273a678..06c840eef0de 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -876,6 +876,256 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "measureInstance") { + auto paramCount = 1; + 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); + + if (!arguments[0].isObject()) { + auto result = jsi::Object(runtime); + result.setProperty(runtime, "x", 0); + result.setProperty(runtime, "y", 0); + result.setProperty(runtime, "width", 0); + result.setProperty(runtime, "height", 0); + return result; + } + + auto shadowNode = Bridging>::fromJs( + runtime, arguments[0]); + + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); + + if (currentRevision == nullptr) { + auto result = jsi::Object(runtime); + result.setProperty(runtime, "x", 0); + result.setProperty(runtime, "y", 0); + result.setProperty(runtime, "width", 0); + result.setProperty(runtime, "height", 0); + return result; + } + + auto domRect = dom::getBoundingClientRect( + currentRevision, *shadowNode, true /* includeTransform */); + + auto result = jsi::Object(runtime); + result.setProperty(runtime, "x", domRect.x); + result.setProperty(runtime, "y", domRect.y); + result.setProperty(runtime, "width", domRect.width); + result.setProperty(runtime, "height", domRect.height); + + auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->captureLayoutMetricsFromRoot(shadowNode); + } + + return result; + }); + } + + if (methodName == "applyViewTransitionName") { + auto paramCount = 3; + 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); + + if (arguments[0].isObject()) { + auto shadowNode = + Bridging>::fromJs( + runtime, arguments[0]); + auto transitionName = arguments[1].isString() + ? stringFromValue(runtime, arguments[1]) + : ""; + auto className = arguments[2].isString() + ? stringFromValue(runtime, arguments[2]) + : ""; + if (!transitionName.empty()) { + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->applyViewTransitionName( + shadowNode, transitionName, className); + } + } + } + + return jsi::Value::undefined(); + }); + } + + if (methodName == "cancelViewTransitionName") { + 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); + + if (arguments[0].isObject()) { + auto shadowNode = + Bridging>::fromJs( + runtime, arguments[0]); + auto transitionName = arguments[1].isString() + ? stringFromValue(runtime, arguments[1]) + : ""; + if (!transitionName.empty()) { + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->cancelViewTransitionName( + shadowNode, transitionName); + } + } + } + + return jsi::Value::undefined(); + }); + } + + if (methodName == "restoreViewTransitionName") { + auto paramCount = 1; + 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); + + if (arguments[0].isObject()) { + auto shadowNode = + Bridging>::fromJs( + runtime, arguments[0]); + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->restoreViewTransitionName(shadowNode); + } + } + + return jsi::Value::undefined(); + }); + } + + if (methodName == "startViewTransition") { + auto paramCount = 1; + 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* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate == nullptr) { + return jsi::Value::undefined(); + } + + auto promiseConstructor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + + auto readyResolveFunc = + std::make_shared>(); + auto finishedResolveFunc = + std::make_shared>(); + + auto mutationFunc = std::make_shared( + arguments[0].asObject(runtime).asFunction(runtime)); + + auto readyPromise = promiseConstructor.callAsConstructor( + runtime, + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "readyExecutor"), + 2, + [readyResolveFunc]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* args, + size_t /*count*/) -> jsi::Value { + auto onReadyFunc = std::make_shared( + args[0].asObject(runtime).asFunction(runtime)); + *readyResolveFunc = onReadyFunc; + return jsi::Value::undefined(); + })); + + auto finishedPromise = promiseConstructor.callAsConstructor( + runtime, + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "finishedExecutor"), + 2, + [finishedResolveFunc]( + jsi::Runtime& rt, + const jsi::Value& /*thisValue*/, + const jsi::Value* args, + size_t /*count*/) -> jsi::Value { + auto onCompleteFunc = std::make_shared( + args[0].asObject(rt).asFunction(rt)); + *finishedResolveFunc = onCompleteFunc; + return jsi::Value::undefined(); + })); + + auto result = jsi::Object(runtime); + result.setProperty(runtime, "ready", std::move(readyPromise)); + result.setProperty(runtime, "finished", std::move(finishedPromise)); + + viewTransitionDelegate->startViewTransition( + [&runtime, mutationFunc = std::move(mutationFunc)]() { + mutationFunc->call(runtime); + }, + [readyResolveFunc = std::move(readyResolveFunc), uiManager]() { + uiManager->runtimeExecutor_( + [readyResolveFunc = std::move(readyResolveFunc)]( + jsi::Runtime& rt) mutable { + if (*readyResolveFunc) { + (*readyResolveFunc)->call(rt); + } + }); + }, + [finishedResolveFunc = std::move(finishedResolveFunc), + uiManager]() { + uiManager->runtimeExecutor_( + [finishedResolveFunc = std::move(finishedResolveFunc)]( + jsi::Runtime& rt) mutable { + if (*finishedResolveFunc) { + (*finishedResolveFunc)->call(rt); + } + }); + }); + + return jsi::Value(runtime, result); + }); + } + return jsi::Value::undefined(); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h new file mode 100644 index 000000000000..ac8c950e9bcf --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class UIManagerViewTransitionDelegate { + public: + virtual ~UIManagerViewTransitionDelegate() = default; + + virtual void applyViewTransitionName( + const std::shared_ptr &shadowNode, + const std::string &name, + const std::string &className) {}; + + virtual void cancelViewTransitionName(const std::shared_ptr &shadowNode, const std::string &name) { + }; + + virtual void restoreViewTransitionName(const std::shared_ptr &shadowNode) {}; + + virtual void captureLayoutMetricsFromRoot(const std::shared_ptr &shadowNode) {}; + + virtual void startViewTransition( + std::function mutationCallback, + std::function onReadyCallback, + std::function onCompleteCallback) {}; +}; + +} // namespace facebook::react From 3e5008fba5f89e40b66da9407c0164fcf83281be Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 25 Feb 2026 11:27:56 -0800 Subject: [PATCH 2/2] Add ViewTransitionModule for enter/exit/share transitions (#55752) Summary: Adds `ViewTransitionModule` - the native implementation of `UIManagerViewTransitionDelegate` that tracks view transition state and orchestrates enter/exit/share transitions. The module: - Captures layout metrics from root for participating views via `captureLayoutMetricsFromRoot` - Manages view-transition-name registration (`applyViewTransitionName`, `cancelViewTransitionName`, `restoreViewTransitionName`) - Detects transition type (enter/exit/share) based on old/new layout snapshots - Orchestrates transition lifecycle via `startViewTransition` Scheduler initializes the module when `viewTransitionEnabled` feature flag is enabled. ## Changelog: [General] [Added] - ViewTransitionModule for React Native View Transitions Reviewed By: sammy-SC Differential Revision: D92537219 --- .../react/renderer/scheduler/CMakeLists.txt | 1 + .../react/renderer/scheduler/Scheduler.cpp | 8 + .../react/renderer/scheduler/Scheduler.h | 3 + .../renderer/uimanager/UIManagerBinding.cpp | 8 +- .../UIManagerViewTransitionDelegate.h | 15 +- .../viewtransition/ViewTransitionModule.cpp | 156 ++++++++++++++++++ .../viewtransition/ViewTransitionModule.h | 82 +++++++++ 7 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt index b39e940016ab..e979712b3c43 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries(react_renderer_scheduler react_renderer_observers_events react_renderer_runtimescheduler react_renderer_uimanager + react_renderer_viewtransition react_utils rrc_root rrc_view diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 7bec66e48fbe..626428a50717 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -157,6 +157,13 @@ Scheduler::Scheduler( } uiManager_->setAnimationDelegate(animationDelegate); + // Initialize ViewTransitionModule + if (ReactNativeFeatureFlags::viewTransitionEnabled()) { + viewTransitionModule_ = std::make_unique(); + viewTransitionModule_->setUIManager(uiManager_.get()); + uiManager_->setViewTransitionDelegate(viewTransitionModule_.get()); + } + uiManager->registerMountHook(*eventPerformanceLogger_); } @@ -186,6 +193,7 @@ Scheduler::~Scheduler() { // The thread-safety of this operation is guaranteed by this requirement. uiManager_->setDelegate(nullptr); uiManager_->setAnimationDelegate(nullptr); + uiManager_->setViewTransitionDelegate(nullptr); if (cdpMetricsReporter_) { performanceEntryReporter_->removeEventListener(&*cdpMetricsReporter_); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index 080c5407c131..ad16e3e40879 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -27,6 +27,7 @@ #include #include #include +#include #include namespace facebook::react { @@ -146,6 +147,8 @@ class Scheduler final : public UIManagerDelegate { RuntimeScheduler *runtimeScheduler_{nullptr}; + std::unique_ptr viewTransitionModule_; + mutable std::shared_mutex onSurfaceStartCallbackMutex_; OnSurfaceStartCallback onSurfaceStartCallback_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 06c840eef0de..efbef4472b21 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -925,7 +925,7 @@ jsi::Value UIManagerBinding::get( auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); if (viewTransitionDelegate != nullptr) { - viewTransitionDelegate->captureLayoutMetricsFromRoot(shadowNode); + viewTransitionDelegate->captureLayoutMetricsFromRoot(*shadowNode); } return result; @@ -960,7 +960,7 @@ jsi::Value UIManagerBinding::get( uiManager->getViewTransitionDelegate(); if (viewTransitionDelegate != nullptr) { viewTransitionDelegate->applyViewTransitionName( - shadowNode, transitionName, className); + *shadowNode, transitionName, className); } } } @@ -994,7 +994,7 @@ jsi::Value UIManagerBinding::get( uiManager->getViewTransitionDelegate(); if (viewTransitionDelegate != nullptr) { viewTransitionDelegate->cancelViewTransitionName( - shadowNode, transitionName); + *shadowNode, transitionName); } } } @@ -1023,7 +1023,7 @@ jsi::Value UIManagerBinding::get( auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate(); if (viewTransitionDelegate != nullptr) { - viewTransitionDelegate->restoreViewTransitionName(shadowNode); + viewTransitionDelegate->restoreViewTransitionName(*shadowNode); } } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index ac8c950e9bcf..7aa64317b635 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -16,17 +16,16 @@ class UIManagerViewTransitionDelegate { public: virtual ~UIManagerViewTransitionDelegate() = default; - virtual void applyViewTransitionName( - const std::shared_ptr &shadowNode, - const std::string &name, - const std::string &className) {}; + virtual void + applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className) + { + } - virtual void cancelViewTransitionName(const std::shared_ptr &shadowNode, const std::string &name) { - }; + virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {}; - virtual void restoreViewTransitionName(const std::shared_ptr &shadowNode) {}; + virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {}; - virtual void captureLayoutMetricsFromRoot(const std::shared_ptr &shadowNode) {}; + virtual void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) {}; virtual void startViewTransition( std::function mutationCallback, diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp new file mode 100644 index 000000000000..a5e4d3b8ad0f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -0,0 +1,156 @@ +/* + * 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. + */ + +#include "ViewTransitionModule.h" + +#include + +#include +#include + +namespace facebook::react { + +void ViewTransitionModule::setUIManager(UIManager* uiManager) { + uiManager_ = uiManager; +} + +void ViewTransitionModule::applyViewTransitionName( + const ShadowNode& shadowNode, + const std::string& name, + const std::string& /*className*/) { + auto tag = shadowNode.getTag(); + auto surfaceId = shadowNode.getSurfaceId(); + + // Look up the captured layout metrics for this shadowNode + auto metricsIt = capturedLayoutMetricsFromRoot_.find(tag); + if (metricsIt == capturedLayoutMetricsFromRoot_.end()) { + // No measurement captured yet, nothing to do + return; + } + + const auto& layoutMetrics = metricsIt->second; + + // Convert LayoutMetrics to AnimationKeyFrameViewLayoutMetrics + AnimationKeyFrameViewLayoutMetrics keyframeMetrics{ + .originFromRoot = layoutMetrics.frame.origin, + .size = layoutMetrics.frame.size, + .pointScaleFactor = layoutMetrics.pointScaleFactor}; + + nameRegistry_[tag].insert(name); + + // If applyViewTransitionName is called after transition started, this is the + // "new" state (end snapshot). Otherwise, this is the "old" state (start + // snapshot) + if (!transitionStarted_) { + AnimationKeyFrameView oldView{ + .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; + oldLayout_[name] = oldView; + } else { + AnimationKeyFrameView newView{ + .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; + newLayout_[name] = newView; + } + + capturedLayoutMetricsFromRoot_.erase(tag); +} + +void ViewTransitionModule::cancelViewTransitionName( + const ShadowNode& shadowNode, + const std::string& name) { + oldLayout_.erase(name); + newLayout_.erase(name); + cancelledNameRegistry_[shadowNode.getTag()].insert(name); +} + +void ViewTransitionModule::restoreViewTransitionName( + const ShadowNode& shadowNode) { + nameRegistry_[shadowNode.getTag()].merge( + cancelledNameRegistry_[shadowNode.getTag()]); + cancelledNameRegistry_.erase(shadowNode.getTag()); +} + +void ViewTransitionModule::captureLayoutMetricsFromRoot( + const ShadowNode& shadowNode) { + if (uiManager_ == nullptr) { + return; + } + + // Get the current revision (root node) for this surface + auto currentRevision = + uiManager_->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode.getSurfaceId()); + + if (currentRevision == nullptr) { + return; + } + + // Cast root to LayoutableShadowNode + auto layoutableRoot = + dynamic_cast(currentRevision.get()); + if (layoutableRoot == nullptr) { + return; + } + + // Compute layout metrics from root + auto layoutMetrics = LayoutableShadowNode::computeLayoutMetricsFromRoot( + shadowNode.getFamily(), *layoutableRoot, {}); + + // Store the layout metrics keyed by tag + capturedLayoutMetricsFromRoot_[shadowNode.getTag()] = layoutMetrics; +} + +void ViewTransitionModule::startViewTransition( + std::function mutationCallback, + std::function onReadyCallback, + std::function onCompleteCallback) { + // Mark transition as started + transitionStarted_ = true; + + // Call mutation callback (including commitRoot, measureInstance, + // applyViewTransitionName for old & new) + if (mutationCallback) { + mutationCallback(); + } + + // TODO: capture pseudo elements + + if (onReadyCallback) { + onReadyCallback(); + } + + // Transition animation starts + + for (const auto& it : nameRegistry_) { + onTransitionAnimationEnd(it.second, it.first, 0); + } + + // Call onComplete callback when transition finishes + if (onCompleteCallback) { + onCompleteCallback(); + } + + transitionStarted_ = false; +} + +void ViewTransitionModule::onTransitionAnimationEnd( + const std::unordered_set& names, + Tag newTag, + Tag oldTag) { + for (const auto& name : names) { + oldLayout_.erase(name); + newLayout_.erase(name); + } + + if (newTag != 0) { + nameRegistry_.erase(newTag); + } + if (oldTag != 0) { + nameRegistry_.erase(oldTag); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h new file mode 100644 index 000000000000..267e9ecb4de8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -0,0 +1,82 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace facebook::react { + +class UIManager; + +class ViewTransitionModule : public UIManagerViewTransitionDelegate { + public: + ~ViewTransitionModule() override = default; + + void setUIManager(UIManager *uiManager); + + // 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 + void applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className) + 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; + + // restore cancellation + void restoreViewTransitionName(const ShadowNode &shadowNode) override; + + void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) override; + + void startViewTransition( + std::function mutationCallback, + std::function onReadyCallback, + std::function onCompleteCallback) override; + + // Animation state structure for storing minimal view data + struct AnimationKeyFrameViewLayoutMetrics { + Point originFromRoot; + Size size; + float pointScaleFactor{}; + }; + + struct AnimationKeyFrameView { + AnimationKeyFrameViewLayoutMetrics layoutMetrics; + Tag tag{0}; + SurfaceId surfaceId{0}; + }; + + private: + void onTransitionAnimationEnd(const std::unordered_set &names, Tag newTag, Tag oldTag); + + // registry of layout of old/new views + std::unordered_map oldLayout_{}; + std::unordered_map newLayout_{}; + // temporary registry of measured layout metrics keyed by tag + std::unordered_map capturedLayoutMetricsFromRoot_{}; + + // tag -> names registry, populated during applyViewTransitionName + // Note that tag and name are not 1:1 mapping + // - In some nested composition 2 names are mappped to the same tag + // - tags of old and new views are mapped to the same name(s) + std::unordered_map> nameRegistry_{}; + + // used for cancel/restore viewTransitionName + std::unordered_map> cancelledNameRegistry_{}; + + UIManager *uiManager_{nullptr}; + + bool transitionStarted_{false}; +}; + +} // namespace facebook::react