Skip to content

Commit e3781ec

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 830bc38 commit e3781ec

File tree

7 files changed

+261
-12
lines changed

7 files changed

+261
-12
lines changed

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: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +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)
21+
{
22+
}
2323

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

27-
virtual void restoreViewTransitionName(const std::shared_ptr<const ShadowNode> &shadowNode) {};
26+
virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {};
2827

29-
virtual void captureLayoutMetricsFromRoot(const std::shared_ptr<const ShadowNode> &shadowNode) {};
28+
virtual void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) {};
3029

3130
virtual void startViewTransition(
3231
std::function<void()> mutationCallback,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
for (const auto& it : nameRegistry_) {
128+
onTransitionAnimationEnd(it.second, it.first, 0);
129+
}
130+
131+
// Call onComplete callback when transition finishes
132+
if (onCompleteCallback) {
133+
onCompleteCallback();
134+
}
135+
136+
transitionStarted_ = false;
137+
}
138+
139+
void ViewTransitionModule::onTransitionAnimationEnd(
140+
const std::unordered_set<std::string>& names,
141+
Tag newTag,
142+
Tag oldTag) {
143+
for (const auto& name : names) {
144+
oldLayout_.erase(name);
145+
newLayout_.erase(name);
146+
}
147+
148+
if (newTag != 0) {
149+
nameRegistry_.erase(newTag);
150+
}
151+
if (oldTag != 0) {
152+
nameRegistry_.erase(oldTag);
153+
}
154+
}
155+
156+
} // namespace facebook::react
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
#pragma once
9+
10+
#include <unordered_set>
11+
12+
#include <react/renderer/core/LayoutMetrics.h>
13+
#include <react/renderer/core/ShadowNode.h>
14+
#include <react/renderer/mounting/ShadowViewMutation.h>
15+
#include <react/renderer/uimanager/UIManagerViewTransitionDelegate.h>
16+
17+
namespace facebook::react {
18+
19+
class UIManager;
20+
21+
class ViewTransitionModule : public UIManagerViewTransitionDelegate {
22+
public:
23+
~ViewTransitionModule() override = default;
24+
25+
void setUIManager(UIManager *uiManager);
26+
27+
// will be called when a view will transition. if a view already has a view-transition-name, it may not be called
28+
// again until it's removed
29+
void applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className)
30+
override;
31+
32+
// if a viewTransitionName is cancelled, the element doesn't have view-transition-name and browser won't be taking
33+
// snapshot
34+
void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) override;
35+
36+
// restore cancellation
37+
void restoreViewTransitionName(const ShadowNode &shadowNode) override;
38+
39+
void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) override;
40+
41+
void startViewTransition(
42+
std::function<void()> mutationCallback,
43+
std::function<void()> onReadyCallback,
44+
std::function<void()> onCompleteCallback) override;
45+
46+
// Animation state structure for storing minimal view data
47+
struct AnimationKeyFrameViewLayoutMetrics {
48+
Point originFromRoot;
49+
Size size;
50+
Float pointScaleFactor{};
51+
};
52+
53+
struct AnimationKeyFrameView {
54+
AnimationKeyFrameViewLayoutMetrics layoutMetrics;
55+
Tag tag{0};
56+
SurfaceId surfaceId{0};
57+
};
58+
59+
private:
60+
void onTransitionAnimationEnd(const std::unordered_set<std::string> &names, Tag newTag, Tag oldTag);
61+
62+
// registry of layout of old/new views
63+
std::unordered_map<std::string, AnimationKeyFrameView> oldLayout_{};
64+
std::unordered_map<std::string, AnimationKeyFrameView> newLayout_{};
65+
// temporary registry of measured layout metrics keyed by tag
66+
std::unordered_map<Tag, LayoutMetrics> capturedLayoutMetricsFromRoot_{};
67+
68+
// tag -> names registry, populated during applyViewTransitionName
69+
// Note that tag and name are not 1:1 mapping
70+
// - In some nested composition 2 names are mappped to the same tag
71+
// - tags of old and new views are mapped to the same name(s)
72+
std::unordered_map<Tag, std::unordered_set<std::string>> nameRegistry_{};
73+
74+
// used for cancel/restore viewTransitionName
75+
std::unordered_map<Tag, std::unordered_set<std::string>> cancelledNameRegistry_{};
76+
77+
UIManager *uiManager_{nullptr};
78+
79+
bool transitionStarted_{false};
80+
};
81+
82+
} // namespace facebook::react

0 commit comments

Comments
 (0)