diff --git a/change/react-native-windows-1ea926cf-a734-42b3-84e6-d1254a7676f1.json b/change/react-native-windows-1ea926cf-a734-42b3-84e6-d1254a7676f1.json new file mode 100644 index 00000000000..7c8e3196445 --- /dev/null +++ b/change/react-native-windows-1ea926cf-a734-42b3-84e6-d1254a7676f1.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fixes issue with restarting animations", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp index 9caf25db30d..257c40ef6cc 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.cpp @@ -42,6 +42,7 @@ AnimationDriver::~AnimationDriver() { } void AnimationDriver::StartAnimation() { + m_started = true; const auto [animation, scopedBatch] = MakeAnimation(m_config); if (auto const animatedValue = GetAnimatedValue()) { animatedValue->PropertySet().StartAnimation(ValueAnimatedNode::s_valueName, animation); @@ -51,15 +52,23 @@ void AnimationDriver::StartAnimation() { m_scopedBatchCompletedToken = scopedBatch.Completed( [weakSelf = weak_from_this(), weakManager = m_manager, id = m_id, tag = m_animatedValueTag](auto sender, auto) { - if (const auto strongSelf = weakSelf.lock()) { - strongSelf->DoCallback(true); - } - + const auto strongSelf = weakSelf.lock(); + const auto ignoreCompletedHandlers = strongSelf && strongSelf->m_ignoreCompletedHandlers; if (auto manager = weakManager.lock()) { - if (auto const animatedValue = manager->GetValueAnimatedNode(tag)) { - animatedValue->RemoveActiveAnimation(id); + // If the animation was stopped for a tracking node, do not clean up the active animation state. + if (!ignoreCompletedHandlers) { + if (const auto animatedValue = manager->GetValueAnimatedNode(tag)) { + animatedValue->RemoveActiveAnimation(id); + } + manager->RemoveActiveAnimation(id); } - manager->RemoveActiveAnimation(id); + + // Always update the stopped animations in case any animations are deferred for the same value. + manager->RemoveStoppedAnimation(id, manager); + } + + if (strongSelf && !ignoreCompletedHandlers) { + strongSelf->DoCallback(!strongSelf->m_stopped); } }); @@ -68,17 +77,15 @@ void AnimationDriver::StartAnimation() { } void AnimationDriver::StopAnimation(bool ignoreCompletedHandlers) { - if (const auto animatedValue = GetAnimatedValue()) { + if (!m_started) { + // The animation may have been deferred and never started. In this case, + // we will never get a scoped batch completion, so we need to fire the + // callback synchronously. + DoCallback(false); + } else if (const auto animatedValue = GetAnimatedValue()) { animatedValue->PropertySet().StopAnimation(ValueAnimatedNode::s_valueName); - if (!ignoreCompletedHandlers) { - animatedValue->RemoveActiveAnimation(m_id); - - if (m_scopedBatch) { - DoCallback(false); - m_scopedBatch.Completed(m_scopedBatchCompletedToken); - m_scopedBatch = nullptr; - } - } + m_stopped = true; + m_ignoreCompletedHandlers = ignoreCompletedHandlers; } } diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h index d24c7422c49..e8055acb8dc 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/AnimationDriver.h @@ -73,5 +73,8 @@ class AnimationDriver : public std::enable_shared_from_this { // auto revoker for scopedBatch.Completed is broken, tracked by internal bug // #22399779 winrt::event_token m_scopedBatchCompletedToken{}; + bool m_started{false}; + bool m_stopped{false}; + bool m_ignoreCompletedHandlers{false}; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp index 25c9451b44e..64775eadfef 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.cpp @@ -120,10 +120,37 @@ void NativeAnimatedNodeManager::DisconnectAnimatedNode(int64_t parentNodeTag, in } } -void NativeAnimatedNodeManager::StopAnimation(int64_t animationId) { +void NativeAnimatedNodeManager::StopAnimation(int64_t animationId, bool isTrackingAnimation) { if (m_activeAnimations.count(animationId)) { - if (const auto animation = m_activeAnimations.at(animationId).get()) { - animation->StopAnimation(); + if (const auto animation = m_activeAnimations.at(animationId)) { + animation->StopAnimation(isTrackingAnimation); + + // Insert the animation into the pending completion set to ensure it is + // not destroyed before the callback occurs. It's safe to assume the + // scoped batch completion callback has not run, since if it had, the + // animation would have been removed from the set of active animations. + m_pendingCompletionAnimations.insert({animationId, animation}); + + const auto nodeTag = animation->AnimatedValueTag(); + if (nodeTag != -1) { + const auto deferredAnimation = m_deferredAnimationForValues.find(nodeTag); + if (deferredAnimation != m_deferredAnimationForValues.end() && deferredAnimation->second == animationId) { + // If the animation is deferred, just remove the deferred animation + // entry as two animations cannot animate the same value concurrently. + m_deferredAnimationForValues.erase(nodeTag); + } else { + // Since only one animation can be active at a time, there shouldn't + // be any stopped animations for the value node if the animation has + // not been deferred. + assert(!m_valuesWithStoppedAnimation.count(nodeTag)); + // In this case, add the value tag to the set of values with stopped + // animations. This is used to optimize the lookup when determining + // if an animation needs to be deferred (rather than iterating over + // the map of pending completion animations). + m_valuesWithStoppedAnimation.insert(nodeTag); + } + } + m_activeAnimations.erase(animationId); } } @@ -241,13 +268,17 @@ void NativeAnimatedNodeManager::StartAnimatingNode( break; } + // If the animated value node has any stopped animations, defer start until + // all stopped animations fire completion callback and have latest values. if (m_activeAnimations.count(animationId)) { - m_activeAnimations.at(animationId)->StartAnimation(); - - for (auto const &trackingAndLead : m_trackingAndLeadNodeTags) { - if (std::get<1>(trackingAndLead) == animatedNodeTag) { - RestartTrackingAnimatedNode(std::get<0>(trackingAndLead), std::get<1>(trackingAndLead), manager); - } + if (m_valuesWithStoppedAnimation.count(animatedNodeTag)) { + // Since only one animation can be active per value at a time, there will + // not be any other deferred animations for the value node. + assert(!m_deferredAnimationForValues.count(animatedNodeTag)); + // Add the animation to the deferred animation map for the value tag. + m_deferredAnimationForValues.insert({animatedNodeTag, animationId}); + } else { + StartAnimationAndTrackingNodes(animationId, animatedNodeTag, manager); } } } @@ -406,4 +437,43 @@ TrackingAnimatedNode *NativeAnimatedNodeManager::GetTrackingAnimatedNode(int64_t void NativeAnimatedNodeManager::RemoveActiveAnimation(int64_t tag) { m_activeAnimations.erase(tag); } + +void NativeAnimatedNodeManager::RemoveStoppedAnimation( + int64_t tag, + const std::shared_ptr &manager) { + if (m_pendingCompletionAnimations.count(tag)) { + // Remove stopped animation for value node entry + const auto animation = m_pendingCompletionAnimations.at(tag); + const auto nodeTag = animation->AnimatedValueTag(); + // If the animation was stopped, attempt to start deferred animations. + if (m_valuesWithStoppedAnimation.erase(nodeTag)) { + StartDeferredAnimationsForValueNode(nodeTag, manager); + } + m_pendingCompletionAnimations.erase(tag); + } +} + +void NativeAnimatedNodeManager::StartDeferredAnimationsForValueNode( + int64_t tag, + const std::shared_ptr &manager) { + if (m_deferredAnimationForValues.count(tag)) { + const auto deferredAnimationTag = m_deferredAnimationForValues.at(tag); + StartAnimationAndTrackingNodes(deferredAnimationTag, tag, manager); + m_deferredAnimationForValues.erase(tag); + } +} + +void NativeAnimatedNodeManager::StartAnimationAndTrackingNodes( + int64_t tag, + int64_t nodeTag, + const std::shared_ptr &manager) { + if (m_activeAnimations.count(tag)) { + m_activeAnimations.at(tag)->StartAnimation(); + for (auto const &trackingAndLead : m_trackingAndLeadNodeTags) { + if (std::get<1>(trackingAndLead) == nodeTag) { + RestartTrackingAnimatedNode(std::get<0>(trackingAndLead), std::get<1>(trackingAndLead), manager); + } + } + } +} } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h index 8cae2a1d426..7f37fbf66cc 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h +++ b/vnext/Microsoft.ReactNative/Modules/Animated/NativeAnimatedNodeManager.h @@ -49,7 +49,7 @@ class NativeAnimatedNodeManager { void DisconnectAnimatedNodeToView(int64_t propsNodeTag, int64_t viewTag); void ConnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag); void DisconnectAnimatedNode(int64_t parentNodeTag, int64_t childNodeTag); - void StopAnimation(int64_t animationId); + void StopAnimation(int64_t animationId, bool isTrackingAnimation = false); void RestartTrackingAnimatedNode( int64_t animationId, int64_t animatedToValueTag, @@ -88,7 +88,16 @@ class NativeAnimatedNodeManager { StyleAnimatedNode *GetStyleAnimatedNode(int64_t tag); TransformAnimatedNode *GetTransformAnimatedNode(int64_t tag); TrackingAnimatedNode *GetTrackingAnimatedNode(int64_t tag); + void RemoveActiveAnimation(int64_t tag); + void RemoveStoppedAnimation(int64_t tag, const std::shared_ptr &manager); + void StartDeferredAnimationsForValueNode( + int64_t valueNodeTag, + const std::shared_ptr &manager); + void StartAnimationAndTrackingNodes( + int64_t tag, + int64_t nodeTag, + const std::shared_ptr &manager); private: std::unordered_map> m_valueNodes{}; @@ -99,6 +108,9 @@ class NativeAnimatedNodeManager { std::unordered_map, std::vector>> m_eventDrivers{}; std::unordered_map> m_activeAnimations{}; + std::unordered_map> m_pendingCompletionAnimations{}; + std::unordered_set m_valuesWithStoppedAnimation{}; + std::unordered_map m_deferredAnimationForValues{}; std::vector> m_trackingAndLeadNodeTags{}; std::vector m_delayedPropsNodes{}; diff --git a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp index 9b9073ad3a7..12a0153cd4e 100644 --- a/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp +++ b/vnext/Microsoft.ReactNative/Modules/Animated/TrackingAnimatedNode.cpp @@ -27,6 +27,9 @@ void TrackingAnimatedNode::Update() { void TrackingAnimatedNode::StartAnimation() { if (auto const strongManager = m_manager.lock()) { if (auto const toValueNode = strongManager->GetValueAnimatedNode(m_toValueId)) { + // In case the animation is already running, we need to stop it to free up the + // animationId key in the active animations map in the animation manager. + strongManager->StopAnimation(m_animationId, true); toValueNode->AddActiveTrackingNode(m_tag); m_animationConfig.insert(static_cast(s_toValueIdName), toValueNode->Value()); strongManager->StartTrackingAnimatedNode(