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;