diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index ae7e2f61ca8423..ceb8d93bcd0de1 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -15,9 +15,10 @@ 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, useState} from 'react'; import {Animated, useAnimatedValue} from 'react-native'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; +import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist'; test('animated opacity', () => { let _opacity; @@ -73,3 +74,119 @@ 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(); + }); + + const viewElement = ensureInstance(viewRef.current, ReactNativeElement); + + Fantom.runTask(() => { + _heightAnimation = Animated.timing(_animatedHeight, { + toValue: 100, + duration: 10, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(10); + + // 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(100); + 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(); + }); + + const viewElement = ensureInstance(viewRef.current, ReactNativeElement); + + 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( + , + ); +}); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index af89f0d9ba21a1..f3d4499fe235d1 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -14,6 +14,7 @@ import type {AnimatedStyleAllowlist} from './AnimatedStyle'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import {findNodeHandle} from '../../ReactNative/RendererProxy'; +import {getInternalInstanceHandleFromPublicInstance} from '../../ReactPrivate/ReactNativePrivateInterface'; import flattenStyle from '../../StyleSheet/flattenStyle'; import {AnimatedEvent} from '../AnimatedEvent'; import AnimatedNode from './AnimatedNode'; @@ -248,6 +249,7 @@ export default class AnimatedProps extends AnimatedNode { if (this._target != null) { this.#connectAnimatedView(this._target); + this.#connectShadowNode(this._target); } } } @@ -259,6 +261,7 @@ export default class AnimatedProps extends AnimatedNode { this._target = {instance, connectedViewTag: null}; if (this.__isNative) { this.#connectAnimatedView(this._target); + this.#connectShadowNode(this._target); } } @@ -279,6 +282,17 @@ export default class AnimatedProps extends AnimatedNode { target.connectedViewTag = viewTag; } + #connectShadowNode(target: TargetView): void { + invariant(this.__isNative, 'Expected node to be marked as "native"'); + let shadowNode = getInternalInstanceHandleFromPublicInstance( + target.instance, + ).stateNode.node; + NativeAnimatedHelper.API.connectAnimatedNodeToShadowNode( + 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 000aa701865375..4b22981b0d46a5 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -167,6 +167,11 @@ - (void)setSurfacePresenter:(id)surfacePresenter }]; } +RCT_EXPORT_METHOD(connectAnimatedNodeToShadowNode:(double)nodeTag shadowNode:(NSDictionary *)shadowNode) +{ + +} + RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView : (double)nodeTag viewTag : (double)viewTag) { [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { diff --git a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp index d03cd98595a1ac..05270dfa186aab 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.cpp @@ -158,6 +158,17 @@ void AnimatedModule::connectAnimatedNodeToView( ConnectAnimatedNodeToViewOp{.nodeTag = nodeTag, .viewTag = viewTag}); } +void AnimatedModule::connectAnimatedNodeToShadowNode( + jsi::Runtime& rt, + Tag nodeTag, + jsi::Object shadowNode) { + auto nativeState = shadowNode.getNativeState(rt); + auto shadowNodeWrapper = + std::dynamic_pointer_cast(nativeState); + operations_.emplace_back(ConnectAnimatedNodeToShadowNodeOp{ + .nodeTag = nodeTag, .shadowNode = shadowNodeWrapper->shadowNode}); +} + void AnimatedModule::disconnectAnimatedNodeFromView( jsi::Runtime& /*rt*/, Tag nodeTag, @@ -270,6 +281,11 @@ void AnimatedModule::executeOperation(const Operation& operation) { nodesManager_->extractAnimatedNodeOffsetOp(op.nodeTag); } else if constexpr (std::is_same_v) { nodesManager_->connectAnimatedNodeToView(op.nodeTag, op.viewTag); + } else if constexpr (std::is_same_v< + T, + ConnectAnimatedNodeToShadowNodeOp>) { + nodesManager_->connectAnimatedNodeToShadowNode( + op.nodeTag, op.shadowNode); } else if constexpr (std::is_same_v< T, DisconnectAnimatedNodeFromViewOp>) { diff --git a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h index a0f84ccbb2c4fd..cd4d2822796dda 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/AnimatedModule.h @@ -88,6 +88,11 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, Tag viewTag{}; }; + struct ConnectAnimatedNodeToShadowNodeOp { + Tag nodeTag{}; + ShadowNode::Shared shadowNode{}; + }; + struct DisconnectAnimatedNodeFromViewOp { Tag nodeTag{}; Tag viewTag{}; @@ -125,6 +130,7 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, SetAnimatedNodeOffsetOp, SetAnimatedNodeValueOp, ConnectAnimatedNodeToViewOp, + ConnectAnimatedNodeToShadowNodeOp, DisconnectAnimatedNodeFromViewOp, RestoreDefaultValuesOp, FlattenAnimatedNodeOffsetOp, @@ -178,6 +184,11 @@ class AnimatedModule : public NativeAnimatedModuleCxxSpec, void connectAnimatedNodeToView(jsi::Runtime& rt, Tag nodeTag, Tag viewTag); + void connectAnimatedNodeToShadowNode( + jsi::Runtime& rt, + Tag nodeTag, + jsi::Object shadowNode); + void disconnectAnimatedNodeFromView(jsi::Runtime& rt, Tag nodeTag, Tag viewTag); diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 248018cb4ed218..da7bf72425d25a 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -234,6 +234,20 @@ void NativeAnimatedNodesManager::connectAnimatedNodeToView( } } +void NativeAnimatedNodesManager::connectAnimatedNodeToShadowNode( + Tag propsNodeTag, + ShadowNode::Shared shadowNode) noexcept { + react_native_assert(propsNodeTag); + auto node = getAnimatedNode(propsNodeTag); + if (node != nullptr) { + node->connectToShadowNode(shadowNode); + updatedNodeTags_.insert(node->tag()); + } else { + LOG(WARNING) + << "Cannot ConnectAnimatedNodeToShadowNode, animated node has to be props type"; + } +} + void NativeAnimatedNodesManager::disconnectAnimatedNodeFromView( Tag propsNodeTag, Tag viewTag) noexcept { @@ -861,10 +875,14 @@ void NativeAnimatedNodesManager::schedulePropsCommit( Tag viewTag, const folly::dynamic& props, bool layoutStyleUpdated, - bool forceFabricCommit) noexcept { + bool forceFabricCommit, + ShadowNode::Shared shadowNode) noexcept { if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { if (layoutStyleUpdated) { mergeObjects(updateViewProps_[viewTag], props); + if (shadowNode) { + tagToShadowNode_[viewTag] = std::move(shadowNode); + } } else { mergeObjects(updateViewPropsDirect_[viewTag], props); } @@ -975,6 +993,28 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() { AnimationMutation{tag, nullptr, propsBuilder.get()}); containsChange = true; } + for (auto& [tag, props] : updateViewProps_) { + auto node = tagToShadowNode_.find(tag); + if (node != tagToShadowNode_.end()) { + 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())); + } + // propsBuilder.storeDynamic(props); + mutations.push_back(AnimationMutation{ + tag, &node->second->getFamily(), propsBuilder.get()}); + } + containsChange = true; + } + if (containsChange) { + updateViewPropsDirect_.clear(); + updateViewProps_.clear(); + tagToShadowNode_.clear(); + } } if (!containsChange) { @@ -994,7 +1034,8 @@ 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; @@ -1005,6 +1046,14 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() { mutations.push_back( AnimationMutation{tag, nullptr, propsBuilder.get()}); } + for (auto& [tag, props] : updateViewProps_) { + auto node = tagToShadowNode_.find(tag); + if (node != tagToShadowNode_.end()) { + propsBuilder.storeDynamic(props); + mutations.push_back(AnimationMutation{ + tag, &node->second->getFamily(), propsBuilder.get()}); + } + } } } else { // There is no active animation. Stop the render callback. @@ -1073,7 +1122,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 9ecf6de7443b6d..906e1a59d252cc 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h @@ -100,6 +100,10 @@ class NativeAnimatedNodesManager { void connectAnimatedNodeToView(Tag propsNodeTag, Tag viewTag) noexcept; + void connectAnimatedNodeToShadowNode( + Tag propsNodeTag, + ShadowNode::Shared node) noexcept; + void disconnectAnimatedNodes(Tag parentTag, Tag childTag) noexcept; void disconnectAnimatedNodeFromView(Tag propsNodeTag, Tag viewTag) noexcept; @@ -158,7 +162,8 @@ class NativeAnimatedNodesManager { Tag viewTag, const folly::dynamic& props, bool layoutStyleUpdated, - bool forceFabricCommit) noexcept; + bool forceFabricCommit, + ShadowNode::Shared shadowNode = nullptr) noexcept; /** * Commits all pending animated property updates to their respective views. @@ -266,6 +271,7 @@ class NativeAnimatedNodesManager { std::unordered_map updateViewProps_{}; std::unordered_map updateViewPropsDirect_{}; + std::unordered_map tagToShadowNode_{}; /* * Sometimes a view is not longer connected to a PropsAnimatedNode, but diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index 8f45b416490735..c5dd2fc586f390 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -82,6 +82,12 @@ NativeAnimatedNodesManagerProvider::getOrCreate( nativeAnimatedNodesManager_ = std::make_shared(animationBackend_); + nativeAnimatedDelegate_ = + std::make_shared( + animationBackend_); + + uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); + uiManager->unstable_setAnimationBackend(animationBackend_); } else { nativeAnimatedNodesManager_ = @@ -90,6 +96,12 @@ NativeAnimatedNodesManagerProvider::getOrCreate( std::move(fabricCommitCallback), std::move(startOnRenderCallback_), std::move(stopOnRenderCallback_)); + + nativeAnimatedDelegate_ = + std::make_shared( + nativeAnimatedNodesManager_); + + uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); } addEventEmitterListener( @@ -112,36 +124,32 @@ NativeAnimatedNodesManagerProvider::getOrCreate( return false; })); - nativeAnimatedDelegate_ = - std::make_shared( - nativeAnimatedNodesManager_); - - uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); - - // TODO: remove force casting. - auto* scheduler = (Scheduler*)uiManager->getDelegate(); - animatedMountingOverrideDelegate_ = - std::make_shared( - *nativeAnimatedNodesManager_, *scheduler); - - // Register on existing surfaces - uiManager->getShadowTreeRegistry().enumerate( - [animatedMountingOverrideDelegate = - std::weak_ptr( - animatedMountingOverrideDelegate_)]( - const ShadowTree& shadowTree, bool& /*stop*/) { - shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( - animatedMountingOverrideDelegate); - }); - // Register on surfaces started in the future - uiManager->setOnSurfaceStartCallback( - [animatedMountingOverrideDelegate = - std::weak_ptr( - animatedMountingOverrideDelegate_)]( - const ShadowTree& shadowTree) { - shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( - animatedMountingOverrideDelegate); - }); + if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + // TODO: remove force casting. + auto* scheduler = (Scheduler*)uiManager->getDelegate(); + animatedMountingOverrideDelegate_ = + std::make_shared( + *nativeAnimatedNodesManager_, *scheduler); + + // Register on existing surfaces + uiManager->getShadowTreeRegistry().enumerate( + [animatedMountingOverrideDelegate = + std::weak_ptr( + animatedMountingOverrideDelegate_)]( + const ShadowTree& shadowTree, bool& /*stop*/) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + animatedMountingOverrideDelegate); + }); + // Register on surfaces started in the future + uiManager->setOnSurfaceStartCallback( + [animatedMountingOverrideDelegate = + std::weak_ptr( + animatedMountingOverrideDelegate_)]( + const ShadowTree& shadowTree) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + animatedMountingOverrideDelegate); + }); + } } return nativeAnimatedNodesManager_; } diff --git a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp index 8303b74741b898..f5e13d43b8960f 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp @@ -58,6 +58,11 @@ void PropsAnimatedNode::connectToView(Tag viewTag) { connectedViewTag_ = viewTag; } +void PropsAnimatedNode::connectToShadowNode( + std::shared_ptr shadowNode) { + viewShadowNode_ = shadowNode; +} + void PropsAnimatedNode::disconnectFromView(Tag viewTag) { react_native_assert( connectedViewTag_ == viewTag && @@ -144,7 +149,11 @@ void PropsAnimatedNode::update(bool forceFabricCommit) { layoutStyleUpdated_ = isLayoutStyleUpdated(getConfig()["props"], *manager_); manager_->schedulePropsCommit( - connectedViewTag_, props_, layoutStyleUpdated_, forceFabricCommit); + connectedViewTag_, + props_, + layoutStyleUpdated_, + forceFabricCommit, + viewShadowNode_.lock()); } } // namespace facebook::react 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 29e300b42f8479..142ccab90518d9 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 { @@ -24,6 +25,7 @@ class PropsAnimatedNode final : public AnimatedNode { const folly::dynamic& config, NativeAnimatedNodesManager& manager); void connectToView(Tag viewTag); + void connectToShadowNode(std::shared_ptr shadowNode); void disconnectFromView(Tag viewTag); void restoreDefaultValues(); @@ -46,5 +48,6 @@ class PropsAnimatedNode final : public AnimatedNode { bool layoutStyleUpdated_{false}; Tag connectedViewTag_{animated::undefinedAnimatedNodeIdentifier}; + std::weak_ptr viewShadowNode_{}; }; } // 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 00000000000000..e42a9cbb447a84 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -0,0 +1,61 @@ +/* + * 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( + std::unordered_map& animatedPropsMap, + std::unordered_set& families) { + for (const auto& family : families) { + if (family != nullptr) { + surfaceToFamilies_[family->getSurfaceId()].insert(family); + } + } + for (auto& [tag, animatedProps] : animatedPropsMap) { + auto& snapshot = map_[tag]; + auto& viewProps = snapshot.props; + + for (auto& animatedProp : animatedProps.props) { + snapshot.propNames.insert(animatedProp->propName); + switch (animatedProp->propName) { + case OPACITY: + viewProps.opacity = get(animatedProp); + break; + + case WIDTH: + viewProps.yogaStyle.setDimension( + yoga::Dimension::Width, + get(animatedProp)); + break; + + case HEIGHT: + viewProps.yogaStyle.setDimension( + yoga::Dimension::Height, + get(animatedProp)); + break; + + case BORDER_RADII: + viewProps.borderRadii = get(animatedProp); + break; + + case FLEX: + viewProps.yogaStyle.setFlex(get(animatedProp)); + break; + + case TRANSFORM: + viewProps.transform = get(animatedProp); + break; + } + } + } +} + +} // 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 00000000000000..1770b8831d451b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h @@ -0,0 +1,35 @@ +/* + * 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; +}; + +class AnimatedPropsRegistry { + public: + std::unordered_map map_; + std::unordered_map> + surfaceToFamilies_; + + void update( + std::unordered_map& animatedPropsMap, + std::unordered_set& families); +}; + +} // 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 c5e438c6eea384..932052b5581f6f 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -7,9 +7,22 @@ #include "AnimationBackend.h" #include +#include "AnimatedPropsRegistry.h" namespace facebook::react { +UIManagerNativeAnimatedDelegateBackendImpl:: + UIManagerNativeAnimatedDelegateBackendImpl( + std::weak_ptr animationBackend) + : animationBackend_(std::move(animationBackend)) {} + +void UIManagerNativeAnimatedDelegateBackendImpl::runAnimationFrame() { + if (auto animationBackendStrong = animationBackend_.lock()) { + animationBackendStrong->onAnimationFrame( + std::chrono::steady_clock::now().time_since_epoch().count() / 1000); + } +} + static inline Props::Shared cloneProps( AnimatedProps& animatedProps, const ShadowNode& shadowNode) { @@ -83,7 +96,11 @@ AnimationBackend::AnimationBackend( stopOnRenderCallback_(std::move(stopOnRenderCallback)), directManipulationCallback_(std::move(directManipulationCallback)), fabricCommitCallback_(std::move(fabricCommitCallback)), - uiManager_(uiManager) {} + animatedPropsRegistry_(std::make_shared()), + uiManager_(uiManager), + commitHook_(std::make_unique( + uiManager, + animatedPropsRegistry_)) {} void AnimationBackend::onAnimationFrame(double timestamp) { std::unordered_map updates; @@ -98,6 +115,8 @@ void AnimationBackend::onAnimationFrame(double timestamp) { } } + animatedPropsRegistry_->update(updates, families); + if (hasAnyLayoutUpdates) { commitUpdatesWithFamilies(families, updates); } else { @@ -108,39 +127,58 @@ void AnimationBackend::onAnimationFrame(double timestamp) { void AnimationBackend::start(const Callback& callback) { callbacks.push_back(callback); // TODO: startOnRenderCallback_ should provide the timestamp from the platform - startOnRenderCallback_([this]() { - onAnimationFrame( - std::chrono::steady_clock::now().time_since_epoch().count() / 1000); - }); + if (startOnRenderCallback_) { + startOnRenderCallback_([this]() { + onAnimationFrame( + std::chrono::steady_clock::now().time_since_epoch().count() / 1000); + }); + } } void AnimationBackend::stop() { - stopOnRenderCallback_(); + if (stopOnRenderCallback_) { + stopOnRenderCallback_(); + } callbacks.clear(); } void AnimationBackend::commitUpdatesWithFamilies( const std::unordered_set& families, std::unordered_map& updates) { - uiManager_->getShadowTreeRegistry().enumerate( - [families, &updates](const ShadowTree& shadowTree, bool& /*stop*/) { - shadowTree.commit( - [families, &updates](const RootShadowNode& oldRootShadowNode) { - return std::static_pointer_cast( - oldRootShadowNode.cloneMultiple( - families, - [families, &updates]( - const ShadowNode& shadowNode, - const ShadowNodeFragment& fragment) { - auto& animatedProps = updates.at(shadowNode.getTag()); - auto newProps = cloneProps(animatedProps, shadowNode); - return shadowNode.clone( - {newProps, - fragment.children, - shadowNode.getState()}); - })); - }, - {.mountSynchronously = true}); - }); + std::unordered_map> + surfaceToFamilies; + for (auto& family : families) { + surfaceToFamilies[family->getSurfaceId()].insert(family); + } + for (const auto& [surfaceId, surfaceFamilies] : surfaceToFamilies) { + uiManager_->getShadowTreeRegistry().visit( + surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) { + shadowTree.commit( + [&surfaceFamilies, + &updates](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.cloneMultiple( + surfaceFamilies, + [&surfaceFamilies, &updates]( + const ShadowNode& shadowNode, + const ShadowNodeFragment& fragment) { + auto newProps = + ShadowNodeFragment::propsPlaceholder(); + if (surfaceFamilies.contains( + &shadowNode.getFamily())) { + auto& animatedProps = + updates.at(shadowNode.getTag()); + newProps = cloneProps(animatedProps, shadowNode); + } + return shadowNode.clone( + {.props = newProps, + .children = fragment.children, + .state = shadowNode.getState(), + .runtimeShadowNodeReference = false}); + })); + }, + {.mountSynchronously = true}); + }); + } } void AnimationBackend::synchronouslyUpdateProps( diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index b4eea2d64c3368..fa2adc52909779 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -15,9 +15,25 @@ #include #include "AnimatedProps.h" #include "AnimatedPropsBuilder.h" +#include "AnimatedPropsRegistry.h" +#include "AnimationBackendCommitHook.h" namespace facebook::react { +class AnimationBackend; + +class UIManagerNativeAnimatedDelegateBackendImpl + : public UIManagerNativeAnimatedDelegate { + public: + explicit UIManagerNativeAnimatedDelegateBackendImpl( + std::weak_ptr animationBackend); + + void runAnimationFrame() override; + + private: + std::weak_ptr animationBackend_; +}; + struct AnimationMutation { Tag tag; const ShadowNodeFamily* family; @@ -41,7 +57,9 @@ class AnimationBackend : public UIManagerAnimationBackend { const StopOnRenderCallback stopOnRenderCallback_; const DirectManipulationCallback directManipulationCallback_; const FabricCommitCallback fabricCommitCallback_; + std::shared_ptr animatedPropsRegistry_; UIManager* uiManager_; + std::unique_ptr commitHook_; AnimationBackend( StartOnRenderCallback&& startOnRenderCallback, 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 00000000000000..bda38022e170da --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp @@ -0,0 +1,100 @@ +/* + * 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) + : uiManager_(uiManager), + 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; + } + auto surfaceFamilies = + animatedPropsRegistry_->surfaceToFamilies_[shadowTree.getSurfaceId()]; + auto& updates = animatedPropsRegistry_->map_; + if (surfaceFamilies.empty()) { + return newRootShadowNode; + } + return std::static_pointer_cast< + RootShadowNode>(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())) { + 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) { + 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: + viewProps->yogaStyle.setDimension( + yoga::Dimension::Height, + snapshot.props.yogaStyle.dimension( + yoga::Dimension::Height)); + 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; + } + } + } + return shadowNode.clone( + {.props = newProps, + .children = fragment.children, + .state = shadowNode.getState(), + .runtimeShadowNodeReference = false}); + })); +} + +AnimationBackendCommitHook::~AnimationBackendCommitHook() { + // TODO: fix + // uiManager get's deallocated first, so we can't call unregisterCommitHook + // here (or need to get a shared_ptr) uiManager_->unregisterCommitHook(*this); +} + +} // 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 00000000000000..ee4907f4426781 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h @@ -0,0 +1,37 @@ +/* + * 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 { + UIManager* uiManager_; + 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 { + } + ~AnimationBackendCommitHook() override; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp index e50a06cbf35bd8..8679fa247f7e81 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -435,11 +435,7 @@ std::shared_ptr cloneMultipleRecursive( } } - ShadowNodeFragment fragment{.children = newChildren}; - if (familiesToUpdate.contains(family)) { - return callback(shadowNode, fragment); - } - return shadowNode.clone(fragment); + return callback(shadowNode, {.children = newChildren}); } } // namespace diff --git a/packages/react-native/src/private/animated/NativeAnimatedHelper.js b/packages/react-native/src/private/animated/NativeAnimatedHelper.js index 5cde2ceef75da7..14d7090771ee5e 100644 --- a/packages/react-native/src/private/animated/NativeAnimatedHelper.js +++ b/packages/react-native/src/private/animated/NativeAnimatedHelper.js @@ -83,6 +83,7 @@ function createNativeOperations(): $NonMaybeType { 'removeAnimatedEventFromView', // 19 'addListener', // 20 'removeListener', // 21 + 'connectAnimatedNodeToShadowNode', // 22 ]; const nativeOperations: { [$Values]: (...$ReadOnlyArray) => void, @@ -312,6 +313,10 @@ const API = { NativeOperations.connectAnimatedNodeToView(nodeTag, viewTag); }, + connectAnimatedNodeToShadowNode(nodeTag: number, shadowNode: {}): void { + NativeOperations.connectAnimatedNodeToShadowNode(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 91f64cdcdc3929..8e04813a5c010f 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; + +connectAnimatedNodeToShadowNode: ( + nodeTag: number, + shadowNode: Object, + ) => void; +disconnectAnimatedNodeFromView: (nodeTag: number, viewTag: number) => void; +restoreDefaultValues: (nodeTag: number) => void; +dropAnimatedNode: (tag: number) => void; diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedTurboModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedTurboModule.js index 5a20ac1539de67..788c936856c43e 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedTurboModule.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAnimatedTurboModule.js @@ -49,6 +49,10 @@ export interface Spec extends TurboModule { +flattenAnimatedNodeOffset: (nodeTag: number) => void; +extractAnimatedNodeOffset: (nodeTag: number) => void; +connectAnimatedNodeToView: (nodeTag: number, viewTag: number) => void; + +connectAnimatedNodeToShadowNode: ( + nodeTag: number, + shadowNode: Object, + ) => void; +disconnectAnimatedNodeFromView: (nodeTag: number, viewTag: number) => void; +restoreDefaultValues: (nodeTag: number) => void; +dropAnimatedNode: (tag: number) => void;