diff --git a/change/react-native-windows-8a4ff7bf-ac06-4eb1-9a58-378dc5d976fa.json b/change/react-native-windows-8a4ff7bf-ac06-4eb1-9a58-378dc5d976fa.json new file mode 100644 index 00000000000..0f3cbb2d6da --- /dev/null +++ b/change/react-native-windows-8a4ff7bf-ac06-4eb1-9a58-378dc5d976fa.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement onMomentumScrollEnd and onMomentumScrollBegin for Fabric ScrollView", + "packageName": "react-native-windows", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/packages/playground/Samples/scrollViewSnapSample.tsx b/packages/playground/Samples/scrollViewSnapSample.tsx index 336792373dc..f31fb2e2389 100644 --- a/packages/playground/Samples/scrollViewSnapSample.tsx +++ b/packages/playground/Samples/scrollViewSnapSample.tsx @@ -288,6 +288,12 @@ export default class Bootstrap extends React.Component<{}, any> { onScrollEndDrag={() => { console.log('onScrollEndDrag'); }} + onMomentumScrollBegin={() => { + console.log('onMomentumScrollBegin'); + }} + onMomentumScrollEnd={() => { + console.log('onMomentumScrollEnd'); + }} onScroll={() => { console.log('onScroll'); }} diff --git a/vnext/Microsoft.ReactNative/CompositionSwitcher.idl b/vnext/Microsoft.ReactNative/CompositionSwitcher.idl index 002bbd03e4a..88d5084aec8 100644 --- a/vnext/Microsoft.ReactNative/CompositionSwitcher.idl +++ b/vnext/Microsoft.ReactNative/CompositionSwitcher.idl @@ -118,6 +118,8 @@ namespace Microsoft.ReactNative.Composition.Experimental event Windows.Foundation.EventHandler ScrollPositionChanged; event Windows.Foundation.EventHandler ScrollBeginDrag; event Windows.Foundation.EventHandler ScrollEndDrag; + event Windows.Foundation.EventHandler ScrollMomentumBegin; + event Windows.Foundation.EventHandler ScrollMomentumEnd; void ContentSize(Windows.Foundation.Numerics.Vector2 size); Windows.Foundation.Numerics.Vector3 ScrollPosition { get; }; void ScrollBy(Windows.Foundation.Numerics.Vector3 offset, Boolean animate); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp index f48ce08cdaa..38bd02ba907 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp @@ -711,8 +711,23 @@ struct CompScrollerVisual : winrt::implements< void IdleStateEntered( typename TTypeRedirects::InteractionTracker sender, typename TTypeRedirects::InteractionTrackerIdleStateEnteredArgs args) noexcept { + // If we were in inertia and are now idle, momentum has ended + if (m_outer->m_inertia) { + m_outer->FireScrollMomentumEnd({sender.Position().x, sender.Position().y}); + } + + // If we were interacting but never entered inertia (Interacting -> Idle), + // and the interaction was user-driven (requestId == 0), fire end-drag here. + // Note: if the interactionRequestId was non-zero it was caused by a Try* call + // (programmatic), so we should not fire onScrollEndDrag. + if (m_outer->m_interacting && args.RequestId() == 0) { + m_outer->FireScrollEndDrag({sender.Position().x, sender.Position().y}); + } + + // Clear state flags m_outer->m_custom = false; m_outer->m_inertia = false; + m_outer->m_interacting = false; } void InertiaStateEntered( typename TTypeRedirects::InteractionTracker sender, @@ -720,15 +735,26 @@ struct CompScrollerVisual : winrt::implements< m_outer->m_custom = false; m_outer->m_inertia = true; m_outer->m_currentPosition = args.NaturalRestingPosition(); - // When the user stops interacting with the object, tracker can go into two paths: - // 1. tracker goes into idle state immediately - // 2. tracker has just started gliding into Inertia state - // Fire ScrollEndDrag - m_outer->FireScrollEndDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y}); + + if (!m_outer->m_interacting && args.RequestId() == 0) { + m_outer->FireScrollBeginDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y}); + } + + // If interaction was user-driven (requestId == 0), + // fire ScrollEndDrag here (Interacting -> Inertia caused by user lift). + if (m_outer->m_interacting && args.RequestId() == 0) { + m_outer->FireScrollEndDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y}); + } + + // Fire momentum scroll begin when we enter inertia (user or programmatic) + m_outer->FireScrollMomentumBegin({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y}); } void InteractingStateEntered( typename TTypeRedirects::InteractionTracker sender, typename TTypeRedirects::InteractionTrackerInteractingStateEnteredArgs args) noexcept { + // Mark that we're now interacting and remember the requestId (user manipulations => 0) + m_outer->m_interacting = true; + // Fire when the user starts dragging the object m_outer->FireScrollBeginDrag({sender.Position().x, sender.Position().y}); } @@ -738,6 +764,10 @@ struct CompScrollerVisual : winrt::implements< void ValuesChanged( typename TTypeRedirects::InteractionTracker sender, typename TTypeRedirects::InteractionTrackerValuesChangedArgs args) noexcept { + if (!m_outer->m_interacting && args.RequestId() == 0) { + m_outer->FireScrollBeginDrag({args.Position().x, args.Position().y}); + } + m_outer->m_interacting = true; m_outer->m_currentPosition = args.Position(); m_outer->FireScrollPositionChanged({args.Position().x, args.Position().y}); } @@ -811,7 +841,7 @@ struct CompScrollerVisual : winrt::implements< m_horizontal ? TTypeRedirects::InteractionSourceMode::Disabled : TTypeRedirects::InteractionSourceMode::EnabledWithInertia); m_visualInteractionSource.ManipulationRedirectionMode( - TTypeRedirects::VisualInteractionSourceRedirectionMode::CapableTouchpadOnly); + TTypeRedirects::VisualInteractionSourceRedirectionMode::CapableTouchpadAndPointerWheel); } else { m_visualInteractionSource.PositionXSourceMode(TTypeRedirects::InteractionSourceMode::Disabled); m_visualInteractionSource.PositionYSourceMode(TTypeRedirects::InteractionSourceMode::Disabled); @@ -985,6 +1015,20 @@ struct CompScrollerVisual : winrt::implements< return m_scrollEndDragEvent.add(handler); } + winrt::event_token ScrollMomentumBegin( + winrt::Windows::Foundation::EventHandler< + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const + &handler) noexcept { + return m_scrollMomentumBeginEvent.add(handler); + } + + winrt::event_token ScrollMomentumEnd( + winrt::Windows::Foundation::EventHandler< + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const + &handler) noexcept { + return m_scrollMomentumEndEvent.add(handler); + } + void ScrollPositionChanged(winrt::event_token const &token) noexcept { m_scrollPositionChangedEvent.remove(token); } @@ -997,6 +1041,14 @@ struct CompScrollerVisual : winrt::implements< m_scrollEndDragEvent.remove(token); } + void ScrollMomentumBegin(winrt::event_token const &token) noexcept { + m_scrollMomentumBeginEvent.remove(token); + } + + void ScrollMomentumEnd(winrt::event_token const &token) noexcept { + m_scrollMomentumEndEvent.remove(token); + } + void ContentSize(winrt::Windows::Foundation::Numerics::float2 const &size) noexcept { m_contentSize = size; m_contentVisual.Size(size); @@ -1075,6 +1127,14 @@ struct CompScrollerVisual : winrt::implements< m_scrollEndDragEvent(*this, winrt::make(position)); } + void FireScrollMomentumBegin(winrt::Windows::Foundation::Numerics::float2 position) noexcept { + m_scrollMomentumBeginEvent(*this, winrt::make(position)); + } + + void FireScrollMomentumEnd(winrt::Windows::Foundation::Numerics::float2 position) noexcept { + m_scrollMomentumEndEvent(*this, winrt::make(position)); + } + void UpdateMaxPosition() noexcept { m_interactionTracker.MaxPosition( {std::max(m_contentSize.x - m_visualSize.x, 0), @@ -1250,6 +1310,7 @@ struct CompScrollerVisual : winrt::implements< SnapAlignment m_snapToAlignment{SnapAlignment::Start}; bool m_inertia{false}; bool m_custom{false}; + bool m_interacting{false}; winrt::Windows::Foundation::Numerics::float3 m_targetPosition; winrt::Windows::Foundation::Numerics::float3 m_currentPosition; winrt::Windows::Foundation::Numerics::float2 m_contentSize{0}; @@ -1263,6 +1324,12 @@ struct CompScrollerVisual : winrt::implements< winrt::event> m_scrollEndDragEvent; + winrt::event> + m_scrollMomentumBeginEvent; + winrt::event> + m_scrollMomentumEndEvent; typename TTypeRedirects::SpriteVisual m_visual{nullptr}; typename TTypeRedirects::SpriteVisual m_contentVisual{nullptr}; typename TTypeRedirects::InteractionTracker m_interactionTracker{nullptr}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index 8113b0ef4dc..41946935277 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -1354,6 +1354,32 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp } }); + m_scrollMomentumBeginRevoker = m_scrollVisual.ScrollMomentumBegin( + winrt::auto_revoke, + [this]( + winrt::IInspectable const & /*sender*/, + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) { + auto eventEmitter = GetEventEmitter(); + if (eventEmitter) { + auto scrollMetrics = getScrollMetrics(eventEmitter, args); + std::static_pointer_cast(eventEmitter) + ->onMomentumScrollBegin(scrollMetrics); + } + }); + + m_scrollMomentumEndRevoker = m_scrollVisual.ScrollMomentumEnd( + winrt::auto_revoke, + [this]( + winrt::IInspectable const & /*sender*/, + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) { + auto eventEmitter = GetEventEmitter(); + if (eventEmitter) { + auto scrollMetrics = getScrollMetrics(eventEmitter, args); + std::static_pointer_cast(eventEmitter) + ->onMomentumScrollEnd(scrollMetrics); + } + }); + return visual; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h index e22f6364607..12bdd6c009b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h @@ -148,9 +148,12 @@ struct ScrollInteractionTrackerOwner : public winrt::implements< m_scrollPositionChangedRevoker{}; winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollBeginDrag_revoker m_scrollBeginDragRevoker{}; - winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollEndDrag_revoker m_scrollEndDragRevoker{}; + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollMomentumBegin_revoker + m_scrollMomentumBeginRevoker{}; + winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollMomentumEnd_revoker + m_scrollMomentumEndRevoker{}; float m_zoomFactor{1.0f}; bool m_isScrollingFromInertia = false;