Skip to content

Commit 34d1771

Browse files
Copilotanupriya13
andauthored
Implement onMomentumScrollEnd and onMomentumScrollBegin for Fabric ScrollView (#15104)
* Initial plan * Implement onMomentumScrollEnd and onMomentumScrollBegin for Fabric ScrollView Co-authored-by: anupriya13 <[email protected]> * Add change file for momentum scroll implementation Co-authored-by: anupriya13 <[email protected]> * Add momentum scroll events test to playground ScrollView sample Co-authored-by: anupriya13 <[email protected]> * Remove vnext/codegen directory changes as requested Co-authored-by: anupriya13 <[email protected]> * Update CompositionContextHelper.cpp * Update CompositionContextHelper.cpp * Update CompositionContextHelper.cpp --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: anupriya13 <[email protected]>
1 parent 7b9f8d4 commit 34d1771

File tree

6 files changed

+118
-7
lines changed

6 files changed

+118
-7
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Implement onMomentumScrollEnd and onMomentumScrollBegin for Fabric ScrollView",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/playground/Samples/scrollViewSnapSample.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@ export default class Bootstrap extends React.Component<{}, any> {
288288
onScrollEndDrag={() => {
289289
console.log('onScrollEndDrag');
290290
}}
291+
onMomentumScrollBegin={() => {
292+
console.log('onMomentumScrollBegin');
293+
}}
294+
onMomentumScrollEnd={() => {
295+
console.log('onMomentumScrollEnd');
296+
}}
291297
onScroll={() => {
292298
console.log('onScroll');
293299
}}

vnext/Microsoft.ReactNative/CompositionSwitcher.idl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ namespace Microsoft.ReactNative.Composition.Experimental
118118
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollPositionChanged;
119119
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollBeginDrag;
120120
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollEndDrag;
121+
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumBegin;
122+
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumEnd;
121123
void ContentSize(Windows.Foundation.Numerics.Vector2 size);
122124
Windows.Foundation.Numerics.Vector3 ScrollPosition { get; };
123125
void ScrollBy(Windows.Foundation.Numerics.Vector3 offset, Boolean animate);

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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};

vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,32 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
13541354
}
13551355
});
13561356

1357+
m_scrollMomentumBeginRevoker = m_scrollVisual.ScrollMomentumBegin(
1358+
winrt::auto_revoke,
1359+
[this](
1360+
winrt::IInspectable const & /*sender*/,
1361+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
1362+
auto eventEmitter = GetEventEmitter();
1363+
if (eventEmitter) {
1364+
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
1365+
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
1366+
->onMomentumScrollBegin(scrollMetrics);
1367+
}
1368+
});
1369+
1370+
m_scrollMomentumEndRevoker = m_scrollVisual.ScrollMomentumEnd(
1371+
winrt::auto_revoke,
1372+
[this](
1373+
winrt::IInspectable const & /*sender*/,
1374+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
1375+
auto eventEmitter = GetEventEmitter();
1376+
if (eventEmitter) {
1377+
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
1378+
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
1379+
->onMomentumScrollEnd(scrollMetrics);
1380+
}
1381+
});
1382+
13571383
return visual;
13581384
}
13591385

vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,12 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
148148
m_scrollPositionChangedRevoker{};
149149
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollBeginDrag_revoker
150150
m_scrollBeginDragRevoker{};
151-
152151
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollEndDrag_revoker
153152
m_scrollEndDragRevoker{};
153+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollMomentumBegin_revoker
154+
m_scrollMomentumBeginRevoker{};
155+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual::ScrollMomentumEnd_revoker
156+
m_scrollMomentumEndRevoker{};
154157

155158
float m_zoomFactor{1.0f};
156159
bool m_isScrollingFromInertia = false;

0 commit comments

Comments
 (0)