diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
index 77bae1426b30..0fd23272409d 100644
--- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
+++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
@@ -15,8 +15,9 @@ import type {HostInstance} from 'react-native';
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
import * as Fantom from '@react-native/fantom';
-import {createRef} from 'react';
+import {createRef, useEffect, useState} from 'react';
import {Animated, useAnimatedValue} from 'react-native';
+import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
test('animated opacity', () => {
@@ -83,3 +84,217 @@ test('animated opacity', () => {
,
);
});
+
+test('animate layout props', () => {
+ const viewRef = createRef();
+ allowStyleProp('height');
+
+ let _animatedHeight;
+ let _heightAnimation;
+
+ function MyApp() {
+ const animatedHeight = useAnimatedValue(0);
+ _animatedHeight = animatedHeight;
+ return (
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _heightAnimation = Animated.timing(_animatedHeight, {
+ toValue: 100,
+ duration: 200,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(100);
+
+ // TODO: getFabricUpdateProps is not working with the cloneMutliple method
+ // expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(100);
+ expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
+ ,
+ );
+
+ Fantom.unstable_produceFramesForDuration(100);
+
+ // TODO: this shouldn't be neccessary since animation should be stopped after duration
+ Fantom.runTask(() => {
+ _heightAnimation?.stop();
+ });
+
+ expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
+ ,
+ );
+});
+
+test('animate layout props and rerender', () => {
+ const viewRef = createRef();
+ allowStyleProp('height');
+
+ let _animatedHeight;
+ let _heightAnimation;
+ let _setWidth;
+
+ function MyApp() {
+ const animatedHeight = useAnimatedValue(0);
+ const [width, setWidth] = useState(100);
+ _animatedHeight = animatedHeight;
+ _setWidth = setWidth;
+ return (
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _heightAnimation = Animated.timing(_animatedHeight, {
+ toValue: 100,
+ duration: 1000,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(500);
+ expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
+ ,
+ );
+
+ Fantom.runTask(() => {
+ _setWidth(200);
+ });
+
+ // TODO: this shouldn't be neccessary since animation should be stopped after duration
+ Fantom.runTask(() => {
+ _heightAnimation?.stop();
+ });
+
+ // TODO: getFabricUpdateProps is not working with the cloneMutliple method
+ // expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
+ expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
+ ,
+ );
+});
+
+test('animate layout props and rerender in many components', () => {
+ const viewRef = createRef();
+ allowStyleProp('height');
+
+ let _animatedHeight;
+ let _heightAnimation;
+ let _setWidth;
+ const N = 100;
+
+ function AnimatedComponent() {
+ const animatedHeight = useAnimatedValue(0);
+
+ useEffect(() => {
+ Animated.timing(animatedHeight, {
+ toValue: 100,
+ duration: 1000,
+ useNativeDriver: true,
+ }).start();
+ });
+ return (
+
+ );
+ }
+
+ function MyApp() {
+ const animatedHeight = useAnimatedValue(0);
+ const [width, setWidth] = useState(100);
+ _animatedHeight = animatedHeight;
+ _setWidth = setWidth;
+ return (
+
+ {Array.from({length: N}, (_, i) => (
+
+ ))}
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _heightAnimation = Animated.timing(_animatedHeight, {
+ toValue: 100,
+ duration: 1000,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(500);
+ expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
+
+ {Array.from({length: N}, (_, i) => (
+
+ ))}
+ ,
+ );
+
+ Fantom.runTask(() => {
+ _setWidth(200);
+ });
+
+ // TODO: this shouldn't be neccessary since animation should be stopped after duration
+ Fantom.runTask(() => {
+ _heightAnimation?.stop();
+ });
+
+ // TODO: getFabricUpdateProps is not working with the cloneMutliple method
+ // expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
+ expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
+
+ {Array.from({length: N}, (_, i) => (
+
+ ))}
+ ,
+ );
+});
diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
index 5f631431b8be..aaf06696b425 100644
--- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
+++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
@@ -14,7 +14,9 @@ import type {AnimatedNodeConfig} from './AnimatedNode';
import type {AnimatedStyleAllowlist} from './AnimatedStyle';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
+import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
+import {getNodeFromPublicInstance} from '../../ReactPrivate/ReactNativePrivateInterface';
import flattenStyle from '../../StyleSheet/flattenStyle';
import {AnimatedEvent} from '../AnimatedEvent';
import AnimatedNode from './AnimatedNode';
@@ -251,7 +253,9 @@ export default class AnimatedProps extends AnimatedNode {
super.__setPlatformConfig(platformConfig);
if (this._target != null) {
- this.#connectAnimatedView(this._target);
+ const target = this._target;
+ this.#connectAnimatedView(target);
+ this.#connectShadowNode(target);
}
}
}
@@ -260,9 +264,10 @@ export default class AnimatedProps extends AnimatedNode {
if (this._target?.instance === instance) {
return;
}
- this._target = {instance, connectedViewTag: null};
+ const target = (this._target = {instance, connectedViewTag: null});
if (this.__isNative) {
- this.#connectAnimatedView(this._target);
+ this.#connectAnimatedView(target);
+ this.#connectShadowNode(target);
}
}
@@ -283,6 +288,27 @@ export default class AnimatedProps extends AnimatedNode {
target.connectedViewTag = viewTag;
}
+ #connectShadowNode(target: TargetView): void {
+ if (
+ !ReactNativeFeatureFlags.cxxNativeAnimatedEnabled() ||
+ //eslint-disable-next-line
+ !ReactNativeFeatureFlags.useSharedAnimatedBackend()
+ ) {
+ return;
+ }
+
+ invariant(this.__isNative, 'Expected node to be marked as "native"');
+ // $FlowExpectedError[incompatible-type] - target.instance may be an HTMLElement but we need ReactNativeElement for Fabric
+ const shadowNode = getNodeFromPublicInstance(target.instance);
+ if (shadowNode == null) {
+ return;
+ }
+ NativeAnimatedHelper.API.connectAnimatedNodeToShadowNodeFamily(
+ this.__getNativeTag(),
+ shadowNode,
+ );
+ }
+
#disconnectAnimatedView(target: TargetView): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
const viewTag = target.connectedViewTag;
diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm
index a168bddff83a..e8245b74a44b 100644
--- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm
+++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm
@@ -10,6 +10,7 @@
#import
#import
#import
+#import
#import
#import "RCTAnimationPlugins.h"
@@ -165,6 +166,12 @@ - (void)setSurfacePresenter:(id)surfacePresenter
}];
}
+RCT_EXPORT_METHOD(connectAnimatedNodeToShadowNodeFamily : (double)nodeTag shadowNode : (NSDictionary *)shadowNode)
+{
+ // This method should only be called when using CxxNativeAnimated
+ react_native_assert(false);
+}
+
RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView : (double)nodeTag viewTag : (double)viewTag)
{
[self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
diff --git a/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec b/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec
index cc2f5c3d7e63..0f875f66b0cb 100644
--- a/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec
+++ b/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec
@@ -48,6 +48,7 @@ Pod::Spec.new do |s|
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")
add_dependency(s, "React-featureflags")
+ add_dependency(s, "React-debug")
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
diff --git a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp
index 8a8b8edc79d2..0410bd6e7bc2 100644
--- a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp
@@ -9,9 +9,9 @@
#include
#include
+#include
namespace facebook::react {
-
AnimatedModule::AnimatedModule(
std::shared_ptr jsInvoker,
std::shared_ptr nodesManagerProvider)
@@ -160,6 +160,19 @@ void AnimatedModule::connectAnimatedNodeToView(
ConnectAnimatedNodeToViewOp{.nodeTag = nodeTag, .viewTag = viewTag});
}
+void AnimatedModule::connectAnimatedNodeToShadowNodeFamily(
+ jsi::Runtime& rt,
+ Tag nodeTag,
+ jsi::Object shadowNodeObj) {
+ const auto& shadowNode = Bridging>::fromJs(
+ rt, jsi::Value(rt, shadowNodeObj));
+
+ operations_.emplace_back(
+ ConnectAnimatedNodeToShadowNodeFamilyOp{
+ .nodeTag = nodeTag,
+ .shadowNodeFamily = shadowNode->getFamilyShared()});
+}
+
void AnimatedModule::disconnectAnimatedNodeFromView(
jsi::Runtime& /*rt*/,
Tag nodeTag,
@@ -282,6 +295,11 @@ void AnimatedModule::executeOperation(
DisconnectAnimatedNodeFromViewOp>) {
nodesManager->disconnectAnimatedNodeFromView(
op.nodeTag, op.viewTag);
+ } else if constexpr (std::is_same_v<
+ T,
+ ConnectAnimatedNodeToShadowNodeFamilyOp>) {
+ nodesManager->connectAnimatedNodeToShadowNodeFamily(
+ op.nodeTag, op.shadowNodeFamily);
} else if constexpr (std::is_same_v) {
nodesManager->restoreDefaultValues(op.nodeTag);
} else if constexpr (std::is_same_v) {
diff --git a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h
index a54254e6a6b7..858cd95e1b46 100644
--- a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h
+++ b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h
@@ -87,6 +87,11 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, publi
Tag viewTag{};
};
+ struct ConnectAnimatedNodeToShadowNodeFamilyOp {
+ Tag nodeTag{};
+ std::shared_ptr shadowNodeFamily{};
+ };
+
struct DisconnectAnimatedNodeFromViewOp {
Tag nodeTag{};
Tag viewTag{};
@@ -124,6 +129,7 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, publi
SetAnimatedNodeOffsetOp,
SetAnimatedNodeValueOp,
ConnectAnimatedNodeToViewOp,
+ ConnectAnimatedNodeToShadowNodeFamilyOp,
DisconnectAnimatedNodeFromViewOp,
RestoreDefaultValuesOp,
FlattenAnimatedNodeOffsetOp,
@@ -176,6 +182,8 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, publi
void connectAnimatedNodeToView(jsi::Runtime &rt, Tag nodeTag, Tag viewTag);
+ void connectAnimatedNodeToShadowNodeFamily(jsi::Runtime &rt, Tag nodeTag, jsi::Object shadowNode);
+
void disconnectAnimatedNodeFromView(jsi::Runtime &rt, Tag nodeTag, Tag viewTag);
void restoreDefaultValues(jsi::Runtime &rt, Tag nodeTag);
diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp
index 5dc4369a382a..aff0bd654d3c 100644
--- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp
@@ -238,6 +238,20 @@ void NativeAnimatedNodesManager::connectAnimatedNodeToView(
}
}
+void NativeAnimatedNodesManager::connectAnimatedNodeToShadowNodeFamily(
+ Tag propsNodeTag,
+ std::shared_ptr family) noexcept {
+ react_native_assert(propsNodeTag);
+ auto node = getAnimatedNode(propsNodeTag);
+ if (node != nullptr && family != nullptr) {
+ std::lock_guard lock(tagToShadowNodeFamilyMutex_);
+ tagToShadowNodeFamily_[family->getTag()] = family;
+ } else {
+ LOG(WARNING)
+ << "Cannot ConnectAnimatedNodeToShadowNodeFamily, animated node has to be props type";
+ }
+}
+
void NativeAnimatedNodesManager::disconnectAnimatedNodeFromView(
Tag propsNodeTag,
Tag viewTag) noexcept {
@@ -251,6 +265,10 @@ void NativeAnimatedNodesManager::disconnectAnimatedNodeFromView(
std::lock_guard lock(connectedAnimatedNodesMutex_);
connectedAnimatedNodes_.erase(viewTag);
}
+ {
+ std::lock_guard lock(tagToShadowNodeFamilyMutex_);
+ tagToShadowNodeFamily_.erase(viewTag);
+ }
updatedNodeTags_.insert(node->tag());
onManagedPropsRemoved(viewTag);
@@ -985,15 +1003,47 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
}
for (auto& [tag, props] : updateViewPropsDirect_) {
- // TODO: also handle layout props (updateViewProps_). It is skipped for
- // now, because the backend requires shadowNodeFamilies to be able to
- // commit to the ShadowTree
propsBuilder.storeDynamic(props);
mutations.push_back(
AnimationMutation{tag, nullptr, propsBuilder.get()});
containsChange = true;
}
- updateViewPropsDirect_.clear();
+ {
+ std::lock_guard lock(tagToShadowNodeFamilyMutex_);
+ for (auto& [tag, props] : updateViewProps_) {
+ auto familyIt = tagToShadowNodeFamily_.find(tag);
+ if (familyIt == tagToShadowNodeFamily_.end()) {
+ continue;
+ }
+ if (auto family = familyIt->second.lock()) {
+ // C++ Animated produces props in the form of a folly::dynamic, so
+ // it wouldn't make sense to unpack it here. However, for the
+ // purposes of testing, we want to be able to use the statically
+ // typed AnimationMutation. At a later stage we will instead just
+ // pass the dynamic directly to propsBuilder and the new API could
+ // be used by 3rd party libraries or in the fututre by Animated.
+ if (props.find("width") != props.items().end()) {
+ propsBuilder.setWidth(
+ yoga::Style::SizeLength::points(props["width"].asDouble()));
+ }
+ if (props.find("height") != props.items().end()) {
+ propsBuilder.setHeight(
+ yoga::Style::SizeLength::points(props["height"].asDouble()));
+ }
+ mutations.push_back(
+ AnimationMutation{
+ .tag = tag,
+ .family = family,
+ .props = propsBuilder.get(),
+ });
+ }
+ containsChange = true;
+ }
+ }
+ if (containsChange) {
+ updateViewPropsDirect_.clear();
+ updateViewProps_.clear();
+ }
}
if (!containsChange) {
@@ -1013,16 +1063,38 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
}
}
- // Step 2: update all nodes that are connected to the finished animations.
+ // Step 2: update all nodes that are connected to the finished
+ // animations.
updateNodes(finishedAnimationValueNodes);
isEventAnimationInProgress_ = false;
for (auto& [tag, props] : updateViewPropsDirect_) {
- // TODO: handle layout props
propsBuilder.storeDynamic(props);
mutations.push_back(
- AnimationMutation{tag, nullptr, propsBuilder.get()});
+ AnimationMutation{
+ .tag = tag,
+ .family = nullptr,
+ .props = propsBuilder.get(),
+ });
+ }
+ {
+ std::lock_guard lock(tagToShadowNodeFamilyMutex_);
+ for (auto& [tag, props] : updateViewProps_) {
+ auto familyIt = tagToShadowNodeFamily_.find(tag);
+ if (familyIt == tagToShadowNodeFamily_.end()) {
+ continue;
+ }
+ if (auto family = familyIt->second.lock()) {
+ propsBuilder.storeDynamic(props);
+ mutations.push_back(
+ AnimationMutation{
+ .tag = tag,
+ .family = family,
+ .props = propsBuilder.get(),
+ });
+ }
+ }
}
}
} else {
@@ -1103,7 +1175,8 @@ void NativeAnimatedNodesManager::onRender() {
}
}
- // Step 2: update all nodes that are connected to the finished animations.
+ // Step 2: update all nodes that are connected to the finished
+ // animations.
updateNodes(finishedAnimationValueNodes);
isEventAnimationInProgress_ = false;
diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h
index 5e86b3bd00b8..40632cbc1dcf 100644
--- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h
+++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h
@@ -21,6 +21,7 @@
#include
#endif
#include
+#include
#include
#include
#include
@@ -101,6 +102,8 @@ class NativeAnimatedNodesManager {
void connectAnimatedNodeToView(Tag propsNodeTag, Tag viewTag) noexcept;
+ void connectAnimatedNodeToShadowNodeFamily(Tag propsNodeTag, std::shared_ptr family) noexcept;
+
void disconnectAnimatedNodes(Tag parentTag, Tag childTag) noexcept;
void disconnectAnimatedNodeFromView(Tag propsNodeTag, Tag viewTag) noexcept;
@@ -258,6 +261,9 @@ class NativeAnimatedNodesManager {
std::unordered_map updateViewProps_{};
std::unordered_map updateViewPropsDirect_{};
+ mutable std::mutex tagToShadowNodeFamilyMutex_;
+ std::unordered_map> tagToShadowNodeFamily_{};
+
/*
* Sometimes a view is not longer connected to a PropsAnimatedNode, but
* NativeAnimated has previously changed the view's props via direct
diff --git a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h
index e8105de34b6f..3da4db587d46 100644
--- a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h
+++ b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h
@@ -14,6 +14,7 @@
#include "AnimatedNode.h"
#include
+#include
#include
namespace facebook::react {
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp
new file mode 100644
index 000000000000..bfddaeaf427c
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "AnimatedPropsRegistry.h"
+#include
+#include "AnimatedProps.h"
+
+namespace facebook::react {
+
+void AnimatedPropsRegistry::update(
+ const std::unordered_map& surfaceUpdates) {
+ auto lock = std::lock_guard(mutex_);
+ for (const auto& [surfaceId, updates] : surfaceUpdates) {
+ auto& surfaceContext = surfaceContexts_[surfaceId];
+ auto& pendingMap = surfaceContext.pendingMap;
+ auto& pendingFamilies = surfaceContext.pendingFamilies;
+
+ auto& updatesMap = updates.propsMap;
+ auto& updatesFamilies = updates.families;
+
+ for (auto& family : updatesFamilies) {
+ pendingFamilies.insert(family);
+ }
+
+ for (auto& [tag, animatedProps] : updatesMap) {
+ auto it = pendingMap.find(tag);
+ if (it == pendingMap.end()) {
+ it = pendingMap.insert_or_assign(tag, std::make_unique())
+ .first;
+ }
+ auto& snapshot = it->second;
+ auto& viewProps = snapshot->props;
+
+ for (const auto& animatedProp : animatedProps.props) {
+ snapshot->propNames.insert(animatedProp->propName);
+ cloneProp(viewProps, *animatedProp);
+ }
+ }
+ }
+}
+
+std::pair&, SnapshotMap&>
+AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
+ auto lock = std::lock_guard(mutex_);
+ auto& [pendingMap, map, pendingFamilies, families] =
+ surfaceContexts_[surfaceId];
+
+ for (auto& family : pendingFamilies) {
+ families.insert(family);
+ }
+ for (auto& [tag, propsSnapshot] : pendingMap) {
+ auto currentIt = map.find(tag);
+ if (currentIt == map.end()) {
+ map.insert_or_assign(tag, std::move(propsSnapshot));
+ } else {
+ auto& currentSnapshot = currentIt->second;
+ for (auto& propName : propsSnapshot->propNames) {
+ currentSnapshot->propNames.insert(propName);
+ updateProp(propName, currentSnapshot->props, *propsSnapshot);
+ }
+ }
+ }
+ pendingMap.clear();
+ pendingFamilies.clear();
+
+ return {families, map};
+}
+
+void AnimatedPropsRegistry::clear(SurfaceId surfaceId) {
+ auto lock = std::lock_guard(mutex_);
+
+ auto& surfaceContext = surfaceContexts_[surfaceId];
+ surfaceContext.families.clear();
+ surfaceContext.map.clear();
+}
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h
new file mode 100644
index 000000000000..4cccbf5be6fe
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h
@@ -0,0 +1,83 @@
+/*
+ * 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
+#include "AnimatedProps.h"
+
+namespace facebook::react {
+
+struct PropsSnapshot {
+ BaseViewProps props;
+ std::unordered_set propNames;
+};
+
+struct SurfaceContext {
+ std::unordered_map> pendingMap, map;
+ std::unordered_set pendingFamilies, families;
+};
+
+struct SurfaceUpdates {
+ std::unordered_set families;
+ std::unordered_map propsMap;
+};
+
+using SnapshotMap = std::unordered_map>;
+
+class AnimatedPropsRegistry {
+ public:
+ void update(const std::unordered_map &surfaceUpdates);
+ void clear(SurfaceId surfaceId);
+ std::pair &, SnapshotMap &> getMap(SurfaceId surfaceId);
+
+ private:
+ std::unordered_map surfaceContexts_;
+ std::mutex mutex_;
+};
+
+inline void updateProp(const PropName propName, BaseViewProps &viewProps, const PropsSnapshot &snapshot)
+{
+ switch (propName) {
+ case OPACITY:
+ viewProps.opacity = snapshot.props.opacity;
+ break;
+
+ case WIDTH:
+ viewProps.yogaStyle.setDimension(
+ yoga::Dimension::Width, snapshot.props.yogaStyle.dimension(yoga::Dimension::Width));
+ break;
+
+ case HEIGHT: {
+ auto d = snapshot.props.yogaStyle.dimension(yoga::Dimension::Height);
+ viewProps.yogaStyle.setDimension(yoga::Dimension::Height, d);
+ break;
+ }
+
+ case TRANSFORM:
+ viewProps.transform = snapshot.props.transform;
+ break;
+
+ case BORDER_RADII:
+ viewProps.borderRadii = snapshot.props.borderRadii;
+ break;
+
+ case FLEX:
+ viewProps.yogaStyle.setFlex(snapshot.props.yogaStyle.flex());
+ break;
+
+ case BACKGROUND_COLOR:
+ viewProps.backgroundColor = snapshot.props.backgroundColor;
+ break;
+ }
+}
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
index 308f19922a08..6ee64331da2b 100644
--- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include "AnimatedPropsRegistry.h"
namespace facebook::react {
@@ -70,12 +71,14 @@ AnimationBackend::AnimationBackend(
stopOnRenderCallback_(std::move(stopOnRenderCallback)),
directManipulationCallback_(std::move(directManipulationCallback)),
fabricCommitCallback_(std::move(fabricCommitCallback)),
- uiManager_(uiManager) {}
+ animatedPropsRegistry_(std::make_shared()),
+ uiManager_(uiManager),
+ commitHook_(uiManager, animatedPropsRegistry_) {}
void AnimationBackend::onAnimationFrame(double timestamp) {
- std::unordered_map updates;
- std::unordered_map>
- surfaceToFamilies;
+ std::unordered_map synchronousUpdates;
+ std::unordered_map surfaceUpdates;
+
bool hasAnyLayoutUpdates = false;
for (auto& callback : callbacks) {
auto muatations = callback(static_cast(timestamp));
@@ -83,22 +86,28 @@ void AnimationBackend::onAnimationFrame(double timestamp) {
hasAnyLayoutUpdates |= mutationHasLayoutUpdates(mutation);
const auto family = mutation.family;
if (family != nullptr) {
- surfaceToFamilies[family->getSurfaceId()].insert(family);
+ auto& [families, updates] = surfaceUpdates[family->getSurfaceId()];
+ families.insert(family.get());
+ updates[mutation.tag] = std::move(mutation.props);
+ } else {
+ synchronousUpdates[mutation.tag] = std::move(mutation.props);
}
- updates[mutation.tag] = std::move(mutation.props);
}
}
+ animatedPropsRegistry_->update(surfaceUpdates);
+
if (hasAnyLayoutUpdates) {
- commitUpdates(surfaceToFamilies, updates);
+ commitUpdates(surfaceUpdates);
} else {
- synchronouslyUpdateProps(updates);
+ synchronouslyUpdateProps(synchronousUpdates);
}
}
void AnimationBackend::start(const Callback& callback, bool isAsync) {
callbacks.push_back(callback);
- // TODO: startOnRenderCallback_ should provide the timestamp from the platform
+ // TODO: startOnRenderCallback_ should provide the timestamp from the
+ // platform
if (startOnRenderCallback_) {
startOnRenderCallback_(
[this]() {
@@ -117,13 +126,11 @@ void AnimationBackend::stop(bool isAsync) {
}
void AnimationBackend::commitUpdates(
- const std::unordered_map<
- SurfaceId,
- std::unordered_set>& surfaceToFamilies,
- std::unordered_map& updates) {
- for (const auto& surfaceEntry : surfaceToFamilies) {
+ std::unordered_map& surfaceUpdates) {
+ for (auto& surfaceEntry : surfaceUpdates) {
const auto& surfaceId = surfaceEntry.first;
- const auto& surfaceFamilies = surfaceEntry.second;
+ const auto& surfaceFamilies = surfaceEntry.second.families;
+ auto& updates = surfaceEntry.second.propsMap;
uiManager_->getShadowTreeRegistry().visit(
surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) {
shadowTree.commit(
@@ -165,4 +172,8 @@ void AnimationBackend::synchronouslyUpdateProps(
}
}
+void AnimationBackend::clearRegistry(SurfaceId surfaceId) {
+ animatedPropsRegistry_->clear(surfaceId);
+}
+
} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
index ed19f5a52eed..d8f7e3a326d9 100644
--- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
@@ -15,6 +15,8 @@
#include
#include "AnimatedProps.h"
#include "AnimatedPropsBuilder.h"
+#include "AnimatedPropsRegistry.h"
+#include "AnimationBackendCommitHook.h"
namespace facebook::react {
@@ -32,7 +34,7 @@ class UIManagerNativeAnimatedDelegateBackendImpl : public UIManagerNativeAnimate
struct AnimationMutation {
Tag tag;
- const ShadowNodeFamily *family;
+ std::shared_ptr family;
AnimatedProps props;
};
@@ -51,7 +53,9 @@ class AnimationBackend : public UIManagerAnimationBackend {
const StopOnRenderCallback stopOnRenderCallback_;
const DirectManipulationCallback directManipulationCallback_;
const FabricCommitCallback fabricCommitCallback_;
+ std::shared_ptr animatedPropsRegistry_;
UIManager *uiManager_;
+ AnimationBackendCommitHook commitHook_;
AnimationBackend(
StartOnRenderCallback &&startOnRenderCallback,
@@ -59,10 +63,9 @@ class AnimationBackend : public UIManagerAnimationBackend {
DirectManipulationCallback &&directManipulationCallback,
FabricCommitCallback &&fabricCommitCallback,
UIManager *uiManager);
- void commitUpdates(
- const std::unordered_map> &surfaceToFamilies,
- std::unordered_map &updates);
+ void commitUpdates(std::unordered_map &surfaceUpdates);
void synchronouslyUpdateProps(const std::unordered_map &updates);
+ void clearRegistry(SurfaceId surfaceId) override;
void onAnimationFrame(double timestamp) override;
void start(const Callback &callback, bool isAsync);
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp
new file mode 100644
index 000000000000..5cb990fb68ad
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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
+
+namespace facebook::react {
+
+AnimationBackendCommitHook::AnimationBackendCommitHook(
+ UIManager* uiManager,
+ std::shared_ptr animatedPropsRegistry)
+ : animatedPropsRegistry_(std::move(animatedPropsRegistry)) {
+ uiManager->registerCommitHook(*this);
+}
+
+RootShadowNode::Unshared AnimationBackendCommitHook::shadowTreeWillCommit(
+ const ShadowTree& shadowTree,
+ const RootShadowNode::Shared& oldRootShadowNode,
+ const RootShadowNode::Unshared& newRootShadowNode,
+ const ShadowTreeCommitOptions& commitOptions) noexcept {
+ if (commitOptions.source != ShadowTreeCommitSource::React) {
+ return newRootShadowNode;
+ }
+
+ const auto& res = animatedPropsRegistry_->getMap(shadowTree.getSurfaceId());
+ auto& surfaceFamilies = res.first;
+ auto& updates = res.second;
+
+ if (surfaceFamilies.empty()) {
+ return newRootShadowNode;
+ }
+ return std::static_pointer_cast(
+ newRootShadowNode->cloneMultiple(
+ surfaceFamilies,
+ [&surfaceFamilies, &updates](
+ const ShadowNode& shadowNode,
+ const ShadowNodeFragment& fragment) {
+ auto newProps = ShadowNodeFragment::propsPlaceholder();
+ std::shared_ptr viewProps = nullptr;
+ if (surfaceFamilies.contains(&shadowNode.getFamily()) &&
+ updates.contains(shadowNode.getTag())) {
+ auto& snapshot = updates.at(shadowNode.getTag());
+ if (!snapshot->propNames.empty()) {
+ PropsParserContext propsParserContext{
+ shadowNode.getSurfaceId(),
+ *shadowNode.getContextContainer()};
+
+ newProps = shadowNode.getComponentDescriptor().cloneProps(
+ propsParserContext, shadowNode.getProps(), {});
+ viewProps = std::const_pointer_cast(
+ std::static_pointer_cast(newProps));
+ }
+
+ for (const auto& propName : snapshot->propNames) {
+ updateProp(propName, *viewProps, *snapshot);
+ }
+ }
+ return shadowNode.clone(
+ {.props = newProps,
+ .children = fragment.children,
+ .state = shadowNode.getState(),
+ .runtimeShadowNodeReference = true});
+ }));
+}
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h
new file mode 100644
index 000000000000..7d6c723d6632
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "AnimatedPropsRegistry.h"
+
+namespace facebook::react {
+
+class AnimationBackendCommitHook : public UIManagerCommitHook {
+ std::shared_ptr animatedPropsRegistry_;
+
+ public:
+ AnimationBackendCommitHook(UIManager *uiManager, std::shared_ptr animatedPropsRegistry);
+ RootShadowNode::Unshared shadowTreeWillCommit(
+ const ShadowTree &shadowTree,
+ const RootShadowNode::Shared &oldRootShadowNode,
+ const RootShadowNode::Unshared &newRootShadowNode,
+ const ShadowTreeCommitOptions &commitOptions) noexcept override;
+ void commitHookWasRegistered(const UIManager &uiManager) noexcept override {}
+ void commitHookWasUnregistered(const UIManager &uiManager) noexcept override {}
+};
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
index 5582d1312a46..931bfce1c1e1 100644
--- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp
@@ -205,6 +205,12 @@ void UIManager::completeSurface(
// after we commit a specific one.
lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision(
surfaceId, shadowTree.getCurrentRevision().rootShadowNode);
+
+ if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
+ if (auto animationBackend = animationBackend_.lock()) {
+ animationBackend->clearRegistry(surfaceId);
+ }
+ }
}
});
}
diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h
index ffc49ff98519..1cbdcf93b2d6 100644
--- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h
+++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h
@@ -19,6 +19,7 @@ class UIManagerAnimationBackend {
virtual void onAnimationFrame(double timestamp) = 0;
// TODO: T240293839 Move over start() function and mutation types
virtual void stop(bool isAsync) = 0;
+ virtual void clearRegistry(SurfaceId surfaceId) = 0;
};
} // namespace facebook::react
diff --git a/packages/react-native/src/private/animated/NativeAnimatedHelper.js b/packages/react-native/src/private/animated/NativeAnimatedHelper.js
index 5cde2ceef75d..9efb2b94b300 100644
--- a/packages/react-native/src/private/animated/NativeAnimatedHelper.js
+++ b/packages/react-native/src/private/animated/NativeAnimatedHelper.js
@@ -17,6 +17,8 @@ import type {
AnimatedNodeConfig,
EventMapping,
} from '../../../Libraries/Animated/NativeAnimatedModule';
+import type {Spec as NativeAnimatedTurboModuleSpec} from '../../../Libraries/Animated/NativeAnimatedTurboModule';
+import type {Node} from '../../../Libraries/Renderer/shims/ReactNativeTypes';
import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmitter';
import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule';
@@ -28,8 +30,17 @@ import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlag
import invariant from 'invariant';
import nullthrows from 'nullthrows';
+interface NativeAnimatedModuleSpec extends NativeAnimatedTurboModuleSpec {
+ // connectAnimatedNodeToShadowNodeFamily is available only in NativeAnimatedNonTurboModule
+ +connectAnimatedNodeToShadowNodeFamily?: (
+ nodeTag: number,
+ // $FlowExpectedError[unclear-type].
+ shadowNode: Object,
+ ) => void;
+}
+
// TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%.
-const NativeAnimatedModule: typeof NativeAnimatedTurboModule =
+const NativeAnimatedModule: ?NativeAnimatedModuleSpec =
NativeAnimatedNonTurboModule ?? NativeAnimatedTurboModule;
let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */
@@ -84,6 +95,13 @@ function createNativeOperations(): $NonMaybeType {
'addListener', // 20
'removeListener', // 21
];
+ if (
+ ReactNativeFeatureFlags.cxxNativeAnimatedEnabled() &&
+ //eslint-disable-next-line
+ ReactNativeFeatureFlags.useSharedAnimatedBackend()
+ ) {
+ methodNames.push('connectAnimatedNodeToShadowNodeFamily');
+ }
const nativeOperations: {
[$Values]: (...$ReadOnlyArray) => void,
} = {};
@@ -312,6 +330,16 @@ const API = {
NativeOperations.connectAnimatedNodeToView(nodeTag, viewTag);
},
+ connectAnimatedNodeToShadowNodeFamily(
+ nodeTag: number,
+ shadowNode: Node,
+ ): void {
+ NativeOperations.connectAnimatedNodeToShadowNodeFamily?.(
+ nodeTag,
+ shadowNode,
+ );
+ },
+
disconnectAnimatedNodeFromView(nodeTag: number, viewTag: number): void {
NativeOperations.disconnectAnimatedNodeFromView(nodeTag, viewTag);
},
diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedModule.js
index 91f64cdcdc39..f71e7f9dcf5c 100644
--- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedModule.js
+++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedModule.js
@@ -49,6 +49,10 @@ export interface Spec extends TurboModule {
+flattenAnimatedNodeOffset: (nodeTag: number) => void;
+extractAnimatedNodeOffset: (nodeTag: number) => void;
+connectAnimatedNodeToView: (nodeTag: number, viewTag: number) => void;
+ +connectAnimatedNodeToShadowNodeFamily?: (
+ nodeTag: number,
+ shadowNode: Object,
+ ) => void;
+disconnectAnimatedNodeFromView: (nodeTag: number, viewTag: number) => void;
+restoreDefaultValues: (nodeTag: number) => void;
+dropAnimatedNode: (tag: number) => void;