From 9ad39daf650584e1397bc83a833089d762021c83 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 26 Feb 2026 09:50:10 -0800 Subject: [PATCH] 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 | 40 +++ 4 files changed, 309 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 d49a865037ad..72a7e3f8d1ab 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, @@ -708,6 +712,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 f5494cc4cd67..023b3704a9c1 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 = + readyResolveFunc](jsi::Runtime& rt) mutable { + if (*readyResolveFunc) { + (*readyResolveFunc)->call(rt); + } + }); + }, + [finishedResolveFunc = std::move(finishedResolveFunc), + uiManager]() { + uiManager->runtimeExecutor_( + [finishedResolveFunc = + 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..3a9615e25e5c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -0,0 +1,40 @@ +/* + * 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