mirror of
https://github.com/react/react-native.git
synced 2026-09-29 16:58:04 +08:00
Add ViewTransitionModule for enter/exit/share transitions (#55752)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55752 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 fbshipit-source-id: c6772394efaf6aaedbf4991b951d2eb03eaf2fba
This commit is contained in:
committed by
meta-codesync[bot]
parent
cdad57d8f1
commit
f81f73e5a0
@@ -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(
|
||||
|
||||
@@ -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
|
||||
$<TARGET_OBJECTS:react_renderer_textlayoutmanager>
|
||||
$<TARGET_OBJECTS:react_renderer_uimanager>
|
||||
$<TARGET_OBJECTS:react_renderer_uimanager_consistency>
|
||||
$<TARGET_OBJECTS:react_renderer_viewtransition>
|
||||
$<TARGET_OBJECTS:react_utils>
|
||||
$<TARGET_OBJECTS:reactnativeblob>
|
||||
$<TARGET_OBJECTS:reactnativejni>
|
||||
@@ -318,6 +320,7 @@ target_include_directories(reactnative
|
||||
$<TARGET_PROPERTY:react_renderer_textlayoutmanager,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:react_renderer_uimanager,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:react_renderer_uimanager_consistency,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:react_renderer_viewtransition,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:react_utils,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:reactnativeblob,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:reactnativejni,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -157,6 +157,13 @@ Scheduler::Scheduler(
|
||||
}
|
||||
uiManager_->setAnimationDelegate(animationDelegate);
|
||||
|
||||
// Initialize ViewTransitionModule
|
||||
if (ReactNativeFeatureFlags::viewTransitionEnabled()) {
|
||||
viewTransitionModule_ = std::make_unique<ViewTransitionModule>();
|
||||
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_);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <react/renderer/uimanager/UIManagerAnimationDelegate.h>
|
||||
#include <react/renderer/uimanager/UIManagerBinding.h>
|
||||
#include <react/renderer/uimanager/UIManagerDelegate.h>
|
||||
#include <react/renderer/viewtransition/ViewTransitionModule.h>
|
||||
#include <react/utils/ContextContainer.h>
|
||||
|
||||
namespace facebook::react {
|
||||
@@ -146,6 +147,8 @@ class Scheduler final : public UIManagerDelegate {
|
||||
|
||||
RuntimeScheduler *runtimeScheduler_{nullptr};
|
||||
|
||||
std::unique_ptr<ViewTransitionModule> viewTransitionModule_;
|
||||
|
||||
mutable std::shared_mutex onSurfaceStartCallbackMutex_;
|
||||
OnSurfaceStartCallback onSurfaceStartCallback_;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -16,18 +16,16 @@ class UIManagerViewTransitionDelegate {
|
||||
public:
|
||||
virtual ~UIManagerViewTransitionDelegate() = default;
|
||||
|
||||
virtual void applyViewTransitionName(
|
||||
const std::shared_ptr<const ShadowNode> &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<const ShadowNode> &shadowNode, const std::string &name) {}
|
||||
virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {}
|
||||
|
||||
virtual void restoreViewTransitionName(const std::shared_ptr<const ShadowNode> &shadowNode) {}
|
||||
virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {}
|
||||
|
||||
virtual void captureLayoutMetricsFromRoot(const std::shared_ptr<const ShadowNode> &shadowNode) {}
|
||||
virtual void captureLayoutMetricsFromRoot(const ShadowNode &shadowNode) {}
|
||||
|
||||
virtual void startViewTransition(
|
||||
std::function<void()> mutationCallback,
|
||||
|
||||
@@ -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)
|
||||
+158
@@ -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 <glog/logging.h>
|
||||
|
||||
#include <react/renderer/core/LayoutableShadowNode.h>
|
||||
#include <react/renderer/uimanager/UIManager.h>
|
||||
|
||||
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<const LayoutableShadowNode*>(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<void()> mutationCallback,
|
||||
std::function<void()> onReadyCallback,
|
||||
std::function<void()> 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<std::string>& 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
|
||||
+84
@@ -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 <unordered_set>
|
||||
|
||||
#include <react/renderer/core/LayoutMetrics.h>
|
||||
#include <react/renderer/core/ShadowNode.h>
|
||||
#include <react/renderer/mounting/ShadowViewMutation.h>
|
||||
#include <react/renderer/uimanager/UIManagerViewTransitionDelegate.h>
|
||||
|
||||
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<void()> mutationCallback,
|
||||
std::function<void()> onReadyCallback,
|
||||
std::function<void()> 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<std::string> &names, Tag newTag, Tag oldTag);
|
||||
|
||||
// registry of layout of old/new views
|
||||
std::unordered_map<std::string, AnimationKeyFrameView> oldLayout_{};
|
||||
std::unordered_map<std::string, AnimationKeyFrameView> newLayout_{};
|
||||
// temporary registry of measured layout metrics keyed by tag
|
||||
std::unordered_map<Tag, LayoutMetrics> 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<Tag, std::unordered_set<std::string>> nameRegistry_{};
|
||||
|
||||
// used for cancel/restore viewTransitionName
|
||||
std::unordered_map<Tag, std::unordered_set<std::string>> cancelledNameRegistry_{};
|
||||
|
||||
UIManager *uiManager_{nullptr};
|
||||
|
||||
bool transitionStarted_{false};
|
||||
};
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user