diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index 0fd23272409d42..e36beafdfe382f 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -191,15 +191,126 @@ test('animate layout props and rerender', () => { _setWidth(200); }); + // 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( + , + ); + + Fantom.unstable_produceFramesForDuration(500); + // TODO: this shouldn't be neccessary since animation should be stopped after duration Fantom.runTask(() => { _heightAnimation?.stop(); }); + expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual( + , + ); + + Fantom.runTask(() => { + _setWidth(300); + }); + + expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual( + , + ); +}); + +test('animate non-layout props and rerender', () => { + const viewRef = createRef(); + + let _animatedOpacity; + let _opacityAnimation; + let _setWidth; + + function MyApp() { + const animatedOpacity = useAnimatedValue(0); + const [width, setWidth] = useState(100); + _animatedOpacity = animatedOpacity; + _setWidth = setWidth; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const viewElement = ensureInstance(viewRef.current, ReactNativeElement); + + Fantom.runTask(() => { + _opacityAnimation = Animated.timing(_animatedOpacity, { + toValue: 0.5, + duration: 1000, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500); + + // TODO: rendered output should be at this point, but synchronous updates are not captured by fantom + expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual( + , + ); + + expect( + Fantom.unstable_getDirectManipulationProps(viewElement).opacity, + ).toBeCloseTo(0.25, 0.001); + + // Re-render + Fantom.runTask(() => { + _setWidth(150); + }); + + expect(root.getRenderedOutput({props: ['opacity', 'width']}).toJSX()).toEqual( + , + ); + + Fantom.runTask(() => { + _setWidth(200); + }); + // 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( - , + expect(root.getRenderedOutput({props: ['opacity', 'width']}).toJSX()).toEqual( + , + ); + + Fantom.unstable_produceFramesForDuration(500); + + // TODO: this shouldn't be neccessary since animation should be stopped after duration + Fantom.runTask(() => { + _opacityAnimation?.stop(); + }); + + // TODO: T246961305 rendered output should be at this point + expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual( + , + ); + + expect(Fantom.unstable_getDirectManipulationProps(viewElement).opacity).toBe( + 0.5, + ); + + // Re-render + Fantom.runTask(() => { + _setWidth(300); + }); + + expect(root.getRenderedOutput({props: ['opacity', 'width']}).toJSX()).toEqual( + , ); }); @@ -298,3 +409,72 @@ test('animate layout props and rerender in many components', () => { , ); }); + +test('animate width, height and opacity at once', () => { + const viewRef = createRef(); + allowStyleProp('width'); + allowStyleProp('height'); + + let _animatedWidth; + let _animatedHeight; + let _animatedOpacity; + let _parallelAnimation; + + function MyApp() { + const animatedWidth = useAnimatedValue(100); + const animatedHeight = useAnimatedValue(100); + const animatedOpacity = useAnimatedValue(1); + _animatedWidth = animatedWidth; + _animatedHeight = animatedHeight; + _animatedOpacity = animatedOpacity; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + _parallelAnimation = Animated.parallel([ + Animated.timing(_animatedWidth, { + toValue: 200, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(_animatedHeight, { + toValue: 200, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(_animatedOpacity, { + toValue: 0.5, + duration: 100, + useNativeDriver: true, + }), + ]).start(); + }); + + Fantom.unstable_produceFramesForDuration(100); + + // TODO: this shouldn't be neccessary since animation should be stopped after duration + Fantom.runTask(() => { + _parallelAnimation?.stop(); + }); + + expect( + root.getRenderedOutput({props: ['width', 'height', 'opacity']}).toJSX(), + ).toEqual(); +}); diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index a17966d285d402..7341352e0cc336 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -1007,39 +1007,41 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() { } } - for (auto& [tag, props] : updateViewPropsDirect_) { - propsBuilder.storeDynamic(props); - mutations.push_back( - AnimationMutation{tag, nullptr, propsBuilder.get()}); - containsChange = true; - } { std::lock_guard lock(tagToShadowNodeFamilyMutex_); + for (auto& [tag, props] : updateViewPropsDirect_) { + auto familyIt = tagToShadowNodeFamily_.find(tag); + if (familyIt == tagToShadowNodeFamily_.end()) { + continue; + } + + auto weakFamily = familyIt->second; + if (auto family = weakFamily.lock()) { + propsBuilder.storeDynamic(props); + mutations.batch.push_back( + AnimationMutation{ + .tag = tag, + .family = family, + .props = propsBuilder.get(), + }); + } + containsChange = true; + } 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( + + auto weakFamily = familyIt->second; + if (auto family = weakFamily.lock()) { + propsBuilder.storeDynamic(props); + mutations.batch.push_back( AnimationMutation{ .tag = tag, .family = family, .props = propsBuilder.get(), + .hasLayoutUpdates = true, }); } containsChange = true; @@ -1074,33 +1076,46 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() { isEventAnimationInProgress_ = false; - for (auto& [tag, props] : updateViewPropsDirect_) { - propsBuilder.storeDynamic(props); - mutations.push_back( - AnimationMutation{ - .tag = tag, - .family = nullptr, - .props = propsBuilder.get(), - }); - } { std::lock_guard lock(tagToShadowNodeFamilyMutex_); + for (auto& [tag, props] : updateViewPropsDirect_) { + auto familyIt = tagToShadowNodeFamily_.find(tag); + if (familyIt == tagToShadowNodeFamily_.end()) { + continue; + } + + auto weakFamily = familyIt->second; + if (auto family = weakFamily.lock()) { + propsBuilder.storeDynamic(props); + mutations.batch.push_back( + AnimationMutation{ + .tag = tag, + .family = family, + .props = propsBuilder.get(), + }); + } + } for (auto& [tag, props] : updateViewProps_) { auto familyIt = tagToShadowNodeFamily_.find(tag); if (familyIt == tagToShadowNodeFamily_.end()) { continue; } - if (auto family = familyIt->second.lock()) { + + auto weakFamily = familyIt->second; + if (auto family = weakFamily.lock()) { propsBuilder.storeDynamic(props); - mutations.push_back( + mutations.batch.push_back( AnimationMutation{ .tag = tag, .family = family, .props = propsBuilder.get(), + .hasLayoutUpdates = true, }); } } } + updateViewProps_.clear(); + updateViewPropsDirect_.clear(); } } else { // There is no active animation. Stop the render callback. diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index 569f662d9753d4..bf8bad41ba96c2 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -145,28 +145,30 @@ NativeAnimatedNodesManagerProvider::getOrCreate( uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); - 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()) { + 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/animationbackend/AnimatedPropsRegistry.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp index bfddaeaf427c72..09ee3620c6fd53 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -35,6 +35,18 @@ void AnimatedPropsRegistry::update( auto& snapshot = it->second; auto& viewProps = snapshot->props; + if (animatedProps.rawProps) { + const auto& newRawProps = *animatedProps.rawProps; + auto& currentRawProps = snapshot->rawProps; + + if (currentRawProps) { + auto newRawPropsDynamic = newRawProps.toDynamic(); + currentRawProps->merge_patch(newRawPropsDynamic); + } else { + currentRawProps = + std::make_unique(newRawProps.toDynamic()); + } + } for (const auto& animatedProp : animatedProps.props) { snapshot->propNames.insert(animatedProp->propName); cloneProp(viewProps, *animatedProp); @@ -58,6 +70,13 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) { map.insert_or_assign(tag, std::move(propsSnapshot)); } else { auto& currentSnapshot = currentIt->second; + if (propsSnapshot->rawProps) { + if (currentSnapshot->rawProps) { + currentSnapshot->rawProps->merge_patch(*propsSnapshot->rawProps); + } else { + currentSnapshot->rawProps = std::move(propsSnapshot->rawProps); + } + } for (auto& propName : propsSnapshot->propNames) { currentSnapshot->propNames.insert(propName); updateProp(propName, currentSnapshot->props, *propsSnapshot); diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h index 3a982ef2245c2f..6df1dc6fb088ce 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h @@ -19,6 +19,7 @@ namespace facebook::react { struct PropsSnapshot { BaseViewProps props; std::unordered_set propNames; + std::unique_ptr rawProps; }; struct SurfaceContext { @@ -29,6 +30,7 @@ struct SurfaceContext { struct SurfaceUpdates { std::unordered_set families; std::unordered_map propsMap; + bool hasLayoutUpdates{false}; }; using SnapshotMap = std::unordered_map>; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index ddf079c3800fbe..b06fe1d92fb121 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -6,6 +6,7 @@ */ #include "AnimationBackend.h" +#include #include #include #include @@ -60,17 +61,6 @@ static inline Props::Shared cloneProps( return newProps; } -static inline bool mutationHasLayoutUpdates( - facebook::react::AnimationMutation& mutation) { - for (auto& animatedProp : mutation.props.props) { - // TODO: there should also be a check for the dynamic part - if (layoutProps.contains(animatedProp->propName)) { - return true; - } - } - return false; -} - AnimationBackend::AnimationBackend( StartOnRenderCallback&& startOnRenderCallback, StopOnRenderCallback&& stopOnRenderCallback, @@ -86,31 +76,30 @@ AnimationBackend::AnimationBackend( commitHook_(uiManager, animatedPropsRegistry_) {} void AnimationBackend::onAnimationFrame(double timestamp) { - std::unordered_map synchronousUpdates; std::unordered_map surfaceUpdates; - bool hasAnyLayoutUpdates = false; for (auto& callback : callbacks) { auto muatations = callback(static_cast(timestamp)); - for (auto& mutation : muatations) { - hasAnyLayoutUpdates |= mutationHasLayoutUpdates(mutation); + for (auto& mutation : muatations.batch) { const auto family = mutation.family; - if (family != nullptr) { - 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); - } + react_native_assert(family != nullptr); + + auto& [families, updates, hasLayoutUpdates] = + surfaceUpdates[family->getSurfaceId()]; + hasLayoutUpdates |= mutation.hasLayoutUpdates; + families.insert(family.get()); + updates[mutation.tag] = std::move(mutation.props); } } animatedPropsRegistry_->update(surfaceUpdates); - if (hasAnyLayoutUpdates) { - commitUpdates(surfaceUpdates); - } else { - synchronouslyUpdateProps(synchronousUpdates); + for (auto& [surfaceId, updates] : surfaceUpdates) { + if (updates.hasLayoutUpdates) { + commitUpdates(surfaceId, updates); + } else { + synchronouslyUpdateProps(updates.propsMap); + } } } @@ -136,47 +125,42 @@ void AnimationBackend::stop(bool isAsync) { } void AnimationBackend::commitUpdates( - std::unordered_map& surfaceUpdates) { - for (auto& surfaceEntry : surfaceUpdates) { - const auto& surfaceId = surfaceEntry.first; - const auto& surfaceFamilies = surfaceEntry.second.families; - auto& updates = surfaceEntry.second.propsMap; - 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}); - }); - } + SurfaceId surfaceId, + SurfaceUpdates& surfaceUpdates) { + auto& surfaceFamilies = surfaceUpdates.families; + auto& updates = surfaceUpdates.propsMap; + 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( const std::unordered_map& updates) { for (auto& [tag, animatedProps] : updates) { - // TODO: We shouldn't repack it into dynamic, but for that a rewrite of - // directManipulationCallback_ is needed + // TODO: We shouldn't repack it into dynamic, but for that a rewrite + // of directManipulationCallback_ is needed auto dyn = animationbackend::packAnimatedProps(animatedProps); directManipulationCallback_(tag, std::move(dyn)); } diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index d8f7e3a326d9f1..7e0bd9bbe81fb8 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -36,9 +36,12 @@ struct AnimationMutation { Tag tag; std::shared_ptr family; AnimatedProps props; + bool hasLayoutUpdates{false}; }; -using AnimationMutations = std::vector; +struct AnimationMutations { + std::vector batch; +}; class AnimationBackend : public UIManagerAnimationBackend { public: @@ -63,7 +66,7 @@ class AnimationBackend : public UIManagerAnimationBackend { DirectManipulationCallback &&directManipulationCallback, FabricCommitCallback &&fabricCommitCallback, UIManager *uiManager); - void commitUpdates(std::unordered_map &surfaceUpdates); + void commitUpdates(SurfaceId surfaceId, SurfaceUpdates &surfaceUpdates); void synchronouslyUpdateProps(const std::unordered_map &updates); void clearRegistry(SurfaceId surfaceId) override; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp index 5cb990fb68ade4..6d359ea8c1c38e 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp @@ -43,13 +43,19 @@ RootShadowNode::Unshared AnimationBackendCommitHook::shadowTreeWillCommit( if (surfaceFamilies.contains(&shadowNode.getFamily()) && updates.contains(shadowNode.getTag())) { auto& snapshot = updates.at(shadowNode.getTag()); - if (!snapshot->propNames.empty()) { + if (!snapshot->propNames.empty() || snapshot->rawProps) { PropsParserContext propsParserContext{ shadowNode.getSurfaceId(), *shadowNode.getContextContainer()}; - - newProps = shadowNode.getComponentDescriptor().cloneProps( - propsParserContext, shadowNode.getProps(), {}); + if (snapshot->rawProps) { + newProps = shadowNode.getComponentDescriptor().cloneProps( + propsParserContext, + shadowNode.getProps(), + RawProps(*snapshot->rawProps)); + } else { + newProps = shadowNode.getComponentDescriptor().cloneProps( + propsParserContext, shadowNode.getProps(), {}); + } viewProps = std::const_pointer_cast( std::static_pointer_cast(newProps)); }