@@ -711,24 +711,50 @@ struct CompScrollerVisual : winrt::implements<
711711 void IdleStateEntered (
712712 typename TTypeRedirects::InteractionTracker sender,
713713 typename TTypeRedirects::InteractionTrackerIdleStateEnteredArgs args) noexcept {
714+ // If we were in inertia and are now idle, momentum has ended
715+ if (m_outer->m_inertia ) {
716+ m_outer->FireScrollMomentumEnd ({sender.Position ().x , sender.Position ().y });
717+ }
718+
719+ // If we were interacting but never entered inertia (Interacting -> Idle),
720+ // and the interaction was user-driven (requestId == 0), fire end-drag here.
721+ // Note: if the interactionRequestId was non-zero it was caused by a Try* call
722+ // (programmatic), so we should not fire onScrollEndDrag.
723+ if (m_outer->m_interacting && args.RequestId () == 0 ) {
724+ m_outer->FireScrollEndDrag ({sender.Position ().x , sender.Position ().y });
725+ }
726+
727+ // Clear state flags
714728 m_outer->m_custom = false ;
715729 m_outer->m_inertia = false ;
730+ m_outer->m_interacting = false ;
716731 }
717732 void InertiaStateEntered (
718733 typename TTypeRedirects::InteractionTracker sender,
719734 typename TTypeRedirects::InteractionTrackerInertiaStateEnteredArgs args) noexcept {
720735 m_outer->m_custom = false ;
721736 m_outer->m_inertia = true ;
722737 m_outer->m_currentPosition = args.NaturalRestingPosition ();
723- // When the user stops interacting with the object, tracker can go into two paths:
724- // 1. tracker goes into idle state immediately
725- // 2. tracker has just started gliding into Inertia state
726- // Fire ScrollEndDrag
727- m_outer->FireScrollEndDrag ({args.NaturalRestingPosition ().x , args.NaturalRestingPosition ().y });
738+
739+ if (!m_outer->m_interacting && args.RequestId () == 0 ) {
740+ m_outer->FireScrollBeginDrag ({args.NaturalRestingPosition ().x , args.NaturalRestingPosition ().y });
741+ }
742+
743+ // If interaction was user-driven (requestId == 0),
744+ // fire ScrollEndDrag here (Interacting -> Inertia caused by user lift).
745+ if (m_outer->m_interacting && args.RequestId () == 0 ) {
746+ m_outer->FireScrollEndDrag ({args.NaturalRestingPosition ().x , args.NaturalRestingPosition ().y });
747+ }
748+
749+ // Fire momentum scroll begin when we enter inertia (user or programmatic)
750+ m_outer->FireScrollMomentumBegin ({args.NaturalRestingPosition ().x , args.NaturalRestingPosition ().y });
728751 }
729752 void InteractingStateEntered (
730753 typename TTypeRedirects::InteractionTracker sender,
731754 typename TTypeRedirects::InteractionTrackerInteractingStateEnteredArgs args) noexcept {
755+ // Mark that we're now interacting and remember the requestId (user manipulations => 0)
756+ m_outer->m_interacting = true ;
757+
732758 // Fire when the user starts dragging the object
733759 m_outer->FireScrollBeginDrag ({sender.Position ().x , sender.Position ().y });
734760 }
@@ -738,6 +764,10 @@ struct CompScrollerVisual : winrt::implements<
738764 void ValuesChanged (
739765 typename TTypeRedirects::InteractionTracker sender,
740766 typename TTypeRedirects::InteractionTrackerValuesChangedArgs args) noexcept {
767+ if (!m_outer->m_interacting && args.RequestId () == 0 ) {
768+ m_outer->FireScrollBeginDrag ({args.Position ().x , args.Position ().y });
769+ }
770+ m_outer->m_interacting = true ;
741771 m_outer->m_currentPosition = args.Position ();
742772 m_outer->FireScrollPositionChanged ({args.Position ().x , args.Position ().y });
743773 }
@@ -811,7 +841,7 @@ struct CompScrollerVisual : winrt::implements<
811841 m_horizontal ? TTypeRedirects::InteractionSourceMode::Disabled
812842 : TTypeRedirects::InteractionSourceMode::EnabledWithInertia);
813843 m_visualInteractionSource.ManipulationRedirectionMode (
814- TTypeRedirects::VisualInteractionSourceRedirectionMode::CapableTouchpadOnly );
844+ TTypeRedirects::VisualInteractionSourceRedirectionMode::CapableTouchpadAndPointerWheel );
815845 } else {
816846 m_visualInteractionSource.PositionXSourceMode (TTypeRedirects::InteractionSourceMode::Disabled);
817847 m_visualInteractionSource.PositionYSourceMode (TTypeRedirects::InteractionSourceMode::Disabled);
@@ -985,6 +1015,20 @@ struct CompScrollerVisual : winrt::implements<
9851015 return m_scrollEndDragEvent.add (handler);
9861016 }
9871017
1018+ winrt::event_token ScrollMomentumBegin (
1019+ winrt::Windows::Foundation::EventHandler<
1020+ winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
1021+ &handler) noexcept {
1022+ return m_scrollMomentumBeginEvent.add (handler);
1023+ }
1024+
1025+ winrt::event_token ScrollMomentumEnd (
1026+ winrt::Windows::Foundation::EventHandler<
1027+ winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
1028+ &handler) noexcept {
1029+ return m_scrollMomentumEndEvent.add (handler);
1030+ }
1031+
9881032 void ScrollPositionChanged (winrt::event_token const &token) noexcept {
9891033 m_scrollPositionChangedEvent.remove (token);
9901034 }
@@ -997,6 +1041,14 @@ struct CompScrollerVisual : winrt::implements<
9971041 m_scrollEndDragEvent.remove (token);
9981042 }
9991043
1044+ void ScrollMomentumBegin (winrt::event_token const &token) noexcept {
1045+ m_scrollMomentumBeginEvent.remove (token);
1046+ }
1047+
1048+ void ScrollMomentumEnd (winrt::event_token const &token) noexcept {
1049+ m_scrollMomentumEndEvent.remove (token);
1050+ }
1051+
10001052 void ContentSize (winrt::Windows::Foundation::Numerics::float2 const &size) noexcept {
10011053 m_contentSize = size;
10021054 m_contentVisual.Size (size);
@@ -1075,6 +1127,14 @@ struct CompScrollerVisual : winrt::implements<
10751127 m_scrollEndDragEvent (*this , winrt::make<CompScrollPositionChangedArgs>(position));
10761128 }
10771129
1130+ void FireScrollMomentumBegin (winrt::Windows::Foundation::Numerics::float2 position) noexcept {
1131+ m_scrollMomentumBeginEvent (*this , winrt::make<CompScrollPositionChangedArgs>(position));
1132+ }
1133+
1134+ void FireScrollMomentumEnd (winrt::Windows::Foundation::Numerics::float2 position) noexcept {
1135+ m_scrollMomentumEndEvent (*this , winrt::make<CompScrollPositionChangedArgs>(position));
1136+ }
1137+
10781138 void UpdateMaxPosition () noexcept {
10791139 m_interactionTracker.MaxPosition (
10801140 {std::max<float >(m_contentSize.x - m_visualSize.x , 0 ),
@@ -1250,6 +1310,7 @@ struct CompScrollerVisual : winrt::implements<
12501310 SnapAlignment m_snapToAlignment{SnapAlignment::Start};
12511311 bool m_inertia{false };
12521312 bool m_custom{false };
1313+ bool m_interacting{false };
12531314 winrt::Windows::Foundation::Numerics::float3 m_targetPosition;
12541315 winrt::Windows::Foundation::Numerics::float3 m_currentPosition;
12551316 winrt::Windows::Foundation::Numerics::float2 m_contentSize{0 };
@@ -1263,6 +1324,12 @@ struct CompScrollerVisual : winrt::implements<
12631324 winrt::event<winrt::Windows::Foundation::EventHandler<
12641325 winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
12651326 m_scrollEndDragEvent;
1327+ winrt::event<winrt::Windows::Foundation::EventHandler<
1328+ winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
1329+ m_scrollMomentumBeginEvent;
1330+ winrt::event<winrt::Windows::Foundation::EventHandler<
1331+ winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
1332+ m_scrollMomentumEndEvent;
12661333 typename TTypeRedirects::SpriteVisual m_visual{nullptr };
12671334 typename TTypeRedirects::SpriteVisual m_contentVisual{nullptr };
12681335 typename TTypeRedirects::InteractionTracker m_interactionTracker{nullptr };
0 commit comments