Skip to content

Commit 5bce1c2

Browse files
zeyapfacebook-github-bot
authored andcommitted
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
1 parent e3898dd commit 5bce1c2

File tree

12 files changed

+301
-12
lines changed

12 files changed

+301
-12
lines changed

packages/react-native/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ let reactFabric = RNTarget(
456456
"components/root/tests",
457457
],
458458
dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga],
459-
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"]
459+
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"]
460460
)
461461

462462
let reactFabricInputAccessory = RNTarget(

packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ add_react_common_subdir(react/renderer/componentregistry)
8989
add_react_common_subdir(react/renderer/mounting)
9090
add_react_common_subdir(react/renderer/scheduler)
9191
add_react_common_subdir(react/renderer/telemetry)
92+
add_react_common_subdir(react/renderer/viewtransition)
9293
add_react_common_subdir(react/renderer/uimanager)
9394
add_react_common_subdir(react/renderer/bridging)
9495
add_react_common_subdir(react/renderer/core)
@@ -223,6 +224,7 @@ add_library(reactnative
223224
$<TARGET_OBJECTS:react_renderer_textlayoutmanager>
224225
$<TARGET_OBJECTS:react_renderer_uimanager>
225226
$<TARGET_OBJECTS:react_renderer_uimanager_consistency>
227+
$<TARGET_OBJECTS:react_renderer_viewtransition>
226228
$<TARGET_OBJECTS:react_utils>
227229
$<TARGET_OBJECTS:reactnativeblob>
228230
$<TARGET_OBJECTS:reactnativejni>
@@ -318,6 +320,7 @@ target_include_directories(reactnative
318320
$<TARGET_PROPERTY:react_renderer_textlayoutmanager,INTERFACE_INCLUDE_DIRECTORIES>
319321
$<TARGET_PROPERTY:react_renderer_uimanager,INTERFACE_INCLUDE_DIRECTORIES>
320322
$<TARGET_PROPERTY:react_renderer_uimanager_consistency,INTERFACE_INCLUDE_DIRECTORIES>
323+
$<TARGET_PROPERTY:react_renderer_viewtransition,INTERFACE_INCLUDE_DIRECTORIES>
321324
$<TARGET_PROPERTY:react_utils,INTERFACE_INCLUDE_DIRECTORIES>
322325
$<TARGET_PROPERTY:reactnativeblob,INTERFACE_INCLUDE_DIRECTORIES>
323326
$<TARGET_PROPERTY:reactnativejni,INTERFACE_INCLUDE_DIRECTORIES>

packages/react-native/ReactCommon/React-Fabric.podspec

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Pod::Spec.new do |s|
161161
ss.header_dir = "react/renderer/scheduler"
162162

163163
ss.dependency "React-Fabric/animationbackend"
164+
ss.dependency "React-Fabric/viewtransition"
164165
ss.dependency "React-performancecdpmetrics"
165166
ss.dependency "React-performancetimeline"
166167
ss.dependency "React-Fabric/observers/events"
@@ -220,4 +221,9 @@ Pod::Spec.new do |s|
220221
ss.header_dir = "react/renderer/leakchecker"
221222
ss.pod_target_xcconfig = { "GCC_WARN_PEDANTIC" => "YES" }
222223
end
224+
225+
s.subspec "viewtransition" do |ss|
226+
ss.source_files = podspec_sources("react/renderer/viewtransition/**/*.{m,mm,cpp,h}", "react/renderer/viewtransition/**/*.h")
227+
ss.header_dir = "react/renderer/viewtransition"
228+
end
223229
end

packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ target_link_libraries(react_renderer_scheduler
3030
react_renderer_observers_events
3131
react_renderer_runtimescheduler
3232
react_renderer_uimanager
33+
react_renderer_viewtransition
3334
react_utils
3435
rrc_root
3536
rrc_view

packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ Scheduler::Scheduler(
157157
}
158158
uiManager_->setAnimationDelegate(animationDelegate);
159159

160+
// Initialize ViewTransitionModule
161+
if (ReactNativeFeatureFlags::viewTransitionEnabled()) {
162+
viewTransitionModule_ = std::make_unique<ViewTransitionModule>();
163+
viewTransitionModule_->setUIManager(uiManager_.get());
164+
uiManager_->setViewTransitionDelegate(viewTransitionModule_.get());
165+
}
166+
160167
uiManager->registerMountHook(*eventPerformanceLogger_);
161168
}
162169

@@ -186,6 +193,7 @@ Scheduler::~Scheduler() {
186193
// The thread-safety of this operation is guaranteed by this requirement.
187194
uiManager_->setDelegate(nullptr);
188195
uiManager_->setAnimationDelegate(nullptr);
196+
uiManager_->setViewTransitionDelegate(nullptr);
189197

190198
if (cdpMetricsReporter_) {
191199
performanceEntryReporter_->removeEventListener(&*cdpMetricsReporter_);

packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <react/renderer/uimanager/UIManagerAnimationDelegate.h>
2828
#include <react/renderer/uimanager/UIManagerBinding.h>
2929
#include <react/renderer/uimanager/UIManagerDelegate.h>
30+
#include <react/renderer/viewtransition/ViewTransitionModule.h>
3031
#include <react/utils/ContextContainer.h>
3132

3233
namespace facebook::react {
@@ -146,6 +147,8 @@ class Scheduler final : public UIManagerDelegate {
146147

147148
RuntimeScheduler *runtimeScheduler_{nullptr};
148149

150+
std::unique_ptr<ViewTransitionModule> viewTransitionModule_;
151+
149152
mutable std::shared_mutex onSurfaceStartCallbackMutex_;
150153
OnSurfaceStartCallback onSurfaceStartCallback_;
151154
};

packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ jsi::Value UIManagerBinding::get(
925925

926926
auto* viewTransitionDelegate = uiManager->getViewTransitionDelegate();
927927
if (viewTransitionDelegate != nullptr) {
928-
viewTransitionDelegate->captureLayoutMetricsFromRoot(shadowNode);
928+
viewTransitionDelegate->captureLayoutMetricsFromRoot(*shadowNode);
929929
}
930930

931931
return result;
@@ -960,7 +960,7 @@ jsi::Value UIManagerBinding::get(
960960
uiManager->getViewTransitionDelegate();
961961
if (viewTransitionDelegate != nullptr) {
962962
viewTransitionDelegate->applyViewTransitionName(
963-
shadowNode, transitionName, className);
963+
*shadowNode, transitionName, className);
964964
}
965965
}
966966
}
@@ -994,7 +994,7 @@ jsi::Value UIManagerBinding::get(
994994
uiManager->getViewTransitionDelegate();
995995
if (viewTransitionDelegate != nullptr) {
996996
viewTransitionDelegate->cancelViewTransitionName(
997-
shadowNode, transitionName);
997+
*shadowNode, transitionName);
998998
}
999999
}
10001000
}
@@ -1023,7 +1023,7 @@ jsi::Value UIManagerBinding::get(
10231023
auto* viewTransitionDelegate =
10241024
uiManager->getViewTransitionDelegate();
10251025
if (viewTransitionDelegate != nullptr) {
1026-
viewTransitionDelegate->restoreViewTransitionName(shadowNode);
1026+
viewTransitionDelegate->restoreViewTransitionName(*shadowNode);
10271027
}
10281028
}
10291029

packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@ class UIManagerViewTransitionDelegate {
1616
public:
1717
virtual ~UIManagerViewTransitionDelegate() = default;
1818

19-
virtual void applyViewTransitionName(
20-
const std::shared_ptr<const ShadowNode> &shadowNode,
21-
const std::string &name,
22-
const std::string &className)
19+
virtual void
20+
applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className)
2321
{
2422
}
2523

26-
virtual void cancelViewTransitionName(const std::shared_ptr<const ShadowNode> &shadowNode, const std::string &name) {}
24+
virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {}
2725

28-
virtual void restoreViewTransitionName(const std::shared_ptr<const ShadowNode> &shadowNode) {}
26+
virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {}
2927

30-
virtual void captureLayoutMetricsFromRoot(const std::shared_ptr<const ShadowNode> &shadowNode) {}
28+
virtual void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) {}
3129

3230
virtual void startViewTransition(
3331
std::function<void()> mutationCallback,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
cmake_minimum_required(VERSION 3.13)
7+
set(CMAKE_VERBOSE_MAKEFILE on)
8+
9+
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
10+
11+
file(GLOB react_renderer_viewtransition_SRC CONFIGURE_DEPENDS *.cpp)
12+
add_library(react_renderer_viewtransition STATIC ${react_renderer_viewtransition_SRC})
13+
14+
target_include_directories(react_renderer_viewtransition PUBLIC ${REACT_COMMON_DIR})
15+
16+
target_link_libraries(react_renderer_viewtransition
17+
glog
18+
react_renderer_core
19+
react_renderer_mounting
20+
react_renderer_uimanager
21+
)
22+
target_compile_reactnative_options(react_renderer_viewtransition PRIVATE)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "ViewTransitionModule.h"
9+
10+
#include <glog/logging.h>
11+
12+
#include <react/renderer/core/LayoutableShadowNode.h>
13+
#include <react/renderer/uimanager/UIManager.h>
14+
15+
namespace facebook::react {
16+
17+
void ViewTransitionModule::setUIManager(UIManager* uiManager) {
18+
uiManager_ = uiManager;
19+
}
20+
21+
void ViewTransitionModule::applyViewTransitionName(
22+
const ShadowNode& shadowNode,
23+
const std::string& name,
24+
const std::string& /*className*/) {
25+
auto tag = shadowNode.getTag();
26+
auto surfaceId = shadowNode.getSurfaceId();
27+
28+
// Look up the captured layout metrics for this shadowNode
29+
auto metricsIt = capturedLayoutMetricsFromRoot_.find(tag);
30+
if (metricsIt == capturedLayoutMetricsFromRoot_.end()) {
31+
// No measurement captured yet, nothing to do
32+
return;
33+
}
34+
35+
const auto& layoutMetrics = metricsIt->second;
36+
37+
// Convert LayoutMetrics to AnimationKeyFrameViewLayoutMetrics
38+
AnimationKeyFrameViewLayoutMetrics keyframeMetrics{
39+
.originFromRoot = layoutMetrics.frame.origin,
40+
.size = layoutMetrics.frame.size,
41+
.pointScaleFactor = layoutMetrics.pointScaleFactor};
42+
43+
nameRegistry_[tag].insert(name);
44+
45+
// If applyViewTransitionName is called after transition started, this is the
46+
// "new" state (end snapshot). Otherwise, this is the "old" state (start
47+
// snapshot)
48+
if (!transitionStarted_) {
49+
AnimationKeyFrameView oldView{
50+
.layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId};
51+
oldLayout_[name] = oldView;
52+
} else {
53+
AnimationKeyFrameView newView{
54+
.layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId};
55+
newLayout_[name] = newView;
56+
}
57+
58+
capturedLayoutMetricsFromRoot_.erase(tag);
59+
}
60+
61+
void ViewTransitionModule::cancelViewTransitionName(
62+
const ShadowNode& shadowNode,
63+
const std::string& name) {
64+
oldLayout_.erase(name);
65+
newLayout_.erase(name);
66+
cancelledNameRegistry_[shadowNode.getTag()].insert(name);
67+
}
68+
69+
void ViewTransitionModule::restoreViewTransitionName(
70+
const ShadowNode& shadowNode) {
71+
nameRegistry_[shadowNode.getTag()].merge(
72+
cancelledNameRegistry_[shadowNode.getTag()]);
73+
cancelledNameRegistry_.erase(shadowNode.getTag());
74+
}
75+
76+
void ViewTransitionModule::captureLayoutMetricsFromRoot(
77+
const ShadowNode& shadowNode) {
78+
if (uiManager_ == nullptr) {
79+
return;
80+
}
81+
82+
// Get the current revision (root node) for this surface
83+
auto currentRevision =
84+
uiManager_->getShadowTreeRevisionProvider()->getCurrentRevision(
85+
shadowNode.getSurfaceId());
86+
87+
if (currentRevision == nullptr) {
88+
return;
89+
}
90+
91+
// Cast root to LayoutableShadowNode
92+
auto layoutableRoot =
93+
dynamic_cast<const LayoutableShadowNode*>(currentRevision.get());
94+
if (layoutableRoot == nullptr) {
95+
return;
96+
}
97+
98+
// Compute layout metrics from root
99+
auto layoutMetrics = LayoutableShadowNode::computeLayoutMetricsFromRoot(
100+
shadowNode.getFamily(), *layoutableRoot, {});
101+
102+
// Store the layout metrics keyed by tag
103+
capturedLayoutMetricsFromRoot_[shadowNode.getTag()] = layoutMetrics;
104+
}
105+
106+
void ViewTransitionModule::startViewTransition(
107+
std::function<void()> mutationCallback,
108+
std::function<void()> onReadyCallback,
109+
std::function<void()> onCompleteCallback) {
110+
// Mark transition as started
111+
transitionStarted_ = true;
112+
113+
// Call mutation callback (including commitRoot, measureInstance,
114+
// applyViewTransitionName for old & new)
115+
if (mutationCallback) {
116+
mutationCallback();
117+
}
118+
119+
// TODO: capture pseudo elements
120+
121+
if (onReadyCallback) {
122+
onReadyCallback();
123+
}
124+
125+
// Transition animation starts
126+
127+
// Call onComplete callback when transition finishes
128+
if (onCompleteCallback) {
129+
onCompleteCallback();
130+
}
131+
}
132+
133+
void ViewTransitionModule::startViewTransitionEnd() {
134+
for (const auto& it : nameRegistry_) {
135+
onTransitionAnimationEnd(it.second, it.first, 0);
136+
}
137+
138+
transitionStarted_ = false;
139+
}
140+
141+
void ViewTransitionModule::onTransitionAnimationEnd(
142+
const std::unordered_set<std::string>& names,
143+
Tag newTag,
144+
Tag oldTag) {
145+
for (const auto& name : names) {
146+
oldLayout_.erase(name);
147+
newLayout_.erase(name);
148+
}
149+
150+
if (newTag != 0) {
151+
nameRegistry_.erase(newTag);
152+
}
153+
if (oldTag != 0) {
154+
nameRegistry_.erase(oldTag);
155+
}
156+
}
157+
158+
} // namespace facebook::react

0 commit comments

Comments
 (0)