diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
index ae7e2f61ca84..cf6604aa4a7b 100644
--- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
+++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
@@ -17,6 +17,7 @@ import ensureInstance from '../../../src/private/__tests__/utilities/ensureInsta
import * as Fantom from '@react-native/fantom';
import {createRef} 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', () => {
@@ -73,3 +74,60 @@ 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(
+ ,
+ );
+});
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 036eeaf3992f..a5084a571af0 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);
@@ -989,15 +1007,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) {
@@ -1017,16 +1067,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 {
@@ -1107,7 +1179,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/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
index 308f19922a08..9ee3db07610a 100644
--- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp
@@ -83,7 +83,7 @@ void AnimationBackend::onAnimationFrame(double timestamp) {
hasAnyLayoutUpdates |= mutationHasLayoutUpdates(mutation);
const auto family = mutation.family;
if (family != nullptr) {
- surfaceToFamilies[family->getSurfaceId()].insert(family);
+ surfaceToFamilies[family->getSurfaceId()].insert(family.get());
}
updates[mutation.tag] = std::move(mutation.props);
}
diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
index ed19f5a52eed..062440f0af4a 100644
--- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
+++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h
@@ -32,7 +32,7 @@ class UIManagerNativeAnimatedDelegateBackendImpl : public UIManagerNativeAnimate
struct AnimationMutation {
Tag tag;
- const ShadowNodeFamily *family;
+ std::shared_ptr family;
AnimatedProps props;
};
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;