diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 2f1dac130e53..852fd7388f91 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -456,7 +456,7 @@ let reactFabric = RNTarget( "components/root/tests", ], dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga], - sources: ["animationbackend", "animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/scrollview/platform/ios", "components/legacyviewmanagerinterop", "components/legacyviewmanagerinterop/platform/ios", "dom", "scheduler", "mounting", "observers/events", "observers/intersection", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency"] + sources: ["animationbackend", "animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/scrollview/platform/ios", "components/legacyviewmanagerinterop", "components/legacyviewmanagerinterop/platform/ios", "dom", "scheduler", "mounting", "observers/events", "observers/intersection", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency", "viewtransition"] ) let reactFabricInputAccessory = RNTarget( diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 979ec533075f..21e895e7835f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -89,6 +89,7 @@ add_react_common_subdir(react/renderer/componentregistry) add_react_common_subdir(react/renderer/mounting) add_react_common_subdir(react/renderer/scheduler) add_react_common_subdir(react/renderer/telemetry) +add_react_common_subdir(react/renderer/viewtransition) add_react_common_subdir(react/renderer/uimanager) add_react_common_subdir(react/renderer/bridging) add_react_common_subdir(react/renderer/core) @@ -223,6 +224,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -318,6 +320,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index fe7534f089c0..56fc9d3dbc7e 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -161,6 +161,7 @@ Pod::Spec.new do |s| ss.header_dir = "react/renderer/scheduler" ss.dependency "React-Fabric/animationbackend" + ss.dependency "React-Fabric/viewtransition" ss.dependency "React-performancecdpmetrics" ss.dependency "React-performancetimeline" ss.dependency "React-Fabric/observers/events" @@ -220,4 +221,9 @@ Pod::Spec.new do |s| ss.header_dir = "react/renderer/leakchecker" ss.pod_target_xcconfig = { "GCC_WARN_PEDANTIC" => "YES" } end + + s.subspec "viewtransition" do |ss| + ss.source_files = podspec_sources("react/renderer/viewtransition/**/*.{m,mm,cpp,h}", "react/renderer/viewtransition/**/*.h") + ss.header_dir = "react/renderer/viewtransition" + end end 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 dccc147345fc..a0cf4e16089e 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/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..d8b41c200e3d 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -876,6 +876,265 @@ 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, viewTransitionDelegate]( + 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 = std::make_shared( + jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "finishedResolve"), + 0, + [onCompleteFunc, viewTransitionDelegate]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* /*args*/, + size_t /*count*/) -> jsi::Value { + onCompleteFunc->call(runtime); + viewTransitionDelegate->startViewTransitionEnd(); + return jsi::Value::undefined(); + })); + 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), &runtime]() { + if (*readyResolveFunc) { + (*readyResolveFunc)->call(runtime); + } + }, + [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..8df388b57b9e --- /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 ShadowNode &shadowNode, const std::string &name, const std::string &className) + { + } + + virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {} + + virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {} + + virtual void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) {} + + virtual void startViewTransition( + std::function mutationCallback, + std::function onReadyCallback, + std::function onCompleteCallback) + { + } + + virtual void startViewTransitionEnd() {} +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/viewtransition/CMakeLists.txt new file mode 100644 index 000000000000..901f82ae5c63 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_renderer_viewtransition_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_renderer_viewtransition STATIC ${react_renderer_viewtransition_SRC}) + +target_include_directories(react_renderer_viewtransition PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_renderer_viewtransition + glog + react_renderer_core + react_renderer_mounting + react_renderer_uimanager +) +target_compile_reactnative_options(react_renderer_viewtransition PRIVATE) 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..ec1e0dba67ca --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -0,0 +1,158 @@ +/* + * 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 + + // Call onComplete callback when transition finishes + if (onCompleteCallback) { + onCompleteCallback(); + } +} + +void ViewTransitionModule::startViewTransitionEnd() { + for (const auto& it : nameRegistry_) { + onTransitionAnimationEnd(it.second, it.first, 0); + } + + 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..1efa8c650023 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -0,0 +1,84 @@ +/* + * 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; + + void startViewTransitionEnd() 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 diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index f5e8b1d7c13e..0d57d019e6e7 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -215,6 +215,12 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { excludePatterns: ['react/renderer/leakchecker/tests'], headerDir: 'react/renderer/leakchecker', }, + + { + name: 'viewtransition', + headerPatterns: ['react/renderer/viewtransition/**/*.h'], + headerDir: 'react/renderer/viewtransition', + }, ], }, // Yoga should preserve its directory structure