Skip to content

Commit d4e9a62

Browse files
anupriya13Copilot
andauthored
Cherrypick Scrollview parity and revert Scrollview regression PRs for stable 0.80 (#15183)
* Revert "[Fabric] Implement snapToAlignment property for ScrollView (#14841)" This reverts commit 6bd0793. * Revert "[Fabric] Implement snapToInterval property for ScrollView (#14847)" This reverts commit 2cde3f1. * 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]> --------- Co-authored-by: Copilot <[email protected]>
1 parent e923256 commit d4e9a62

File tree

6 files changed

+123
-106
lines changed

6 files changed

+123
-106
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: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ namespace Microsoft.ReactNative.Composition.Experimental
3131
SwitchThumb,
3232
};
3333

34-
enum SnapAlignment
35-
{
36-
Start,
37-
Center,
38-
End,
39-
};
40-
4134
[webhosthidden]
4235
[uuid("172def51-9e1a-4e3c-841a-e5a470065acc")] // uuid needed for empty interfaces
4336
[version(0)]
@@ -118,6 +111,8 @@ namespace Microsoft.ReactNative.Composition.Experimental
118111
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollPositionChanged;
119112
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollBeginDrag;
120113
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollEndDrag;
114+
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumBegin;
115+
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumEnd;
121116
void ContentSize(Windows.Foundation.Numerics.Vector2 size);
122117
Windows.Foundation.Numerics.Vector3 ScrollPosition { get; };
123118
void ScrollBy(Windows.Foundation.Numerics.Vector3 offset, Boolean animate);
@@ -127,7 +122,7 @@ namespace Microsoft.ReactNative.Composition.Experimental
127122
void SetMaximumZoomScale(Single maximumZoomScale);
128123
void SetMinimumZoomScale(Single minimumZoomScale);
129124
Boolean Horizontal;
130-
void SetSnapPoints(Boolean snapToStart, Boolean snapToEnd, Windows.Foundation.Collections.IVectorView<Single> offsets, SnapAlignment snapToAlignment);
125+
void SetSnapPoints(Boolean snapToStart, Boolean snapToEnd, Windows.Foundation.Collections.IVectorView<Single> offsets);
131126
}
132127

133128
[webhosthidden]

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

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727

2828
namespace Microsoft::ReactNative::Composition::Experimental {
2929

30-
using namespace winrt::Microsoft::ReactNative::Composition::Experimental;
31-
3230
template <typename TSpriteVisual>
3331
struct CompositionTypeTraits {};
3432

@@ -711,24 +709,50 @@ struct CompScrollerVisual : winrt::implements<
711709
void IdleStateEntered(
712710
typename TTypeRedirects::InteractionTracker sender,
713711
typename TTypeRedirects::InteractionTrackerIdleStateEnteredArgs args) noexcept {
712+
// If we were in inertia and are now idle, momentum has ended
713+
if (m_outer->m_inertia) {
714+
m_outer->FireScrollMomentumEnd({sender.Position().x, sender.Position().y});
715+
}
716+
717+
// If we were interacting but never entered inertia (Interacting -> Idle),
718+
// and the interaction was user-driven (requestId == 0), fire end-drag here.
719+
// Note: if the interactionRequestId was non-zero it was caused by a Try* call
720+
// (programmatic), so we should not fire onScrollEndDrag.
721+
if (m_outer->m_interacting && args.RequestId() == 0) {
722+
m_outer->FireScrollEndDrag({sender.Position().x, sender.Position().y});
723+
}
724+
725+
// Clear state flags
714726
m_outer->m_custom = false;
715727
m_outer->m_inertia = false;
728+
m_outer->m_interacting = false;
716729
}
717730
void InertiaStateEntered(
718731
typename TTypeRedirects::InteractionTracker sender,
719732
typename TTypeRedirects::InteractionTrackerInertiaStateEnteredArgs args) noexcept {
720733
m_outer->m_custom = false;
721734
m_outer->m_inertia = true;
722735
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});
736+
737+
if (!m_outer->m_interacting && args.RequestId() == 0) {
738+
m_outer->FireScrollBeginDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
739+
}
740+
741+
// If interaction was user-driven (requestId == 0),
742+
// fire ScrollEndDrag here (Interacting -> Inertia caused by user lift).
743+
if (m_outer->m_interacting && args.RequestId() == 0) {
744+
m_outer->FireScrollEndDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
745+
}
746+
747+
// Fire momentum scroll begin when we enter inertia (user or programmatic)
748+
m_outer->FireScrollMomentumBegin({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
728749
}
729750
void InteractingStateEntered(
730751
typename TTypeRedirects::InteractionTracker sender,
731752
typename TTypeRedirects::InteractionTrackerInteractingStateEnteredArgs args) noexcept {
753+
// Mark that we're now interacting and remember the requestId (user manipulations => 0)
754+
m_outer->m_interacting = true;
755+
732756
// Fire when the user starts dragging the object
733757
m_outer->FireScrollBeginDrag({sender.Position().x, sender.Position().y});
734758
}
@@ -738,6 +762,10 @@ struct CompScrollerVisual : winrt::implements<
738762
void ValuesChanged(
739763
typename TTypeRedirects::InteractionTracker sender,
740764
typename TTypeRedirects::InteractionTrackerValuesChangedArgs args) noexcept {
765+
if (!m_outer->m_interacting && args.RequestId() == 0) {
766+
m_outer->FireScrollBeginDrag({args.Position().x, args.Position().y});
767+
}
768+
m_outer->m_interacting = true;
741769
m_outer->m_currentPosition = args.Position();
742770
m_outer->FireScrollPositionChanged({args.Position().x, args.Position().y});
743771
}
@@ -873,11 +901,9 @@ struct CompScrollerVisual : winrt::implements<
873901
void SetSnapPoints(
874902
bool snapToStart,
875903
bool snapToEnd,
876-
winrt::Windows::Foundation::Collections::IVectorView<float> const &offsets,
877-
SnapAlignment snapToAlignment) noexcept {
904+
winrt::Windows::Foundation::Collections::IVectorView<float> const &offsets) noexcept {
878905
m_snapToStart = snapToStart;
879906
m_snapToEnd = snapToEnd;
880-
m_snapToAlignment = snapToAlignment;
881907
m_snapToOffsets.clear();
882908
if (offsets) {
883909
for (auto const &offset : offsets) {
@@ -985,6 +1011,20 @@ struct CompScrollerVisual : winrt::implements<
9851011
return m_scrollEndDragEvent.add(handler);
9861012
}
9871013

1014+
winrt::event_token ScrollMomentumBegin(
1015+
winrt::Windows::Foundation::EventHandler<
1016+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
1017+
&handler) noexcept {
1018+
return m_scrollMomentumBeginEvent.add(handler);
1019+
}
1020+
1021+
winrt::event_token ScrollMomentumEnd(
1022+
winrt::Windows::Foundation::EventHandler<
1023+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
1024+
&handler) noexcept {
1025+
return m_scrollMomentumEndEvent.add(handler);
1026+
}
1027+
9881028
void ScrollPositionChanged(winrt::event_token const &token) noexcept {
9891029
m_scrollPositionChangedEvent.remove(token);
9901030
}
@@ -997,6 +1037,14 @@ struct CompScrollerVisual : winrt::implements<
9971037
m_scrollEndDragEvent.remove(token);
9981038
}
9991039

1040+
void ScrollMomentumBegin(winrt::event_token const &token) noexcept {
1041+
m_scrollMomentumBeginEvent.remove(token);
1042+
}
1043+
1044+
void ScrollMomentumEnd(winrt::event_token const &token) noexcept {
1045+
m_scrollMomentumEndEvent.remove(token);
1046+
}
1047+
10001048
void ContentSize(winrt::Windows::Foundation::Numerics::float2 const &size) noexcept {
10011049
m_contentSize = size;
10021050
m_contentVisual.Size(size);
@@ -1075,6 +1123,14 @@ struct CompScrollerVisual : winrt::implements<
10751123
m_scrollEndDragEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
10761124
}
10771125

1126+
void FireScrollMomentumBegin(winrt::Windows::Foundation::Numerics::float2 position) noexcept {
1127+
m_scrollMomentumBeginEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
1128+
}
1129+
1130+
void FireScrollMomentumEnd(winrt::Windows::Foundation::Numerics::float2 position) noexcept {
1131+
m_scrollMomentumEndEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
1132+
}
1133+
10781134
void UpdateMaxPosition() noexcept {
10791135
m_interactionTracker.MaxPosition(
10801136
{std::max<float>(m_contentSize.x - m_visualSize.x, 0),
@@ -1104,22 +1160,6 @@ struct CompScrollerVisual : winrt::implements<
11041160
}
11051161

11061162
snapPositions.insert(snapPositions.end(), m_snapToOffsets.begin(), m_snapToOffsets.end());
1107-
1108-
// Adjust snap positions based on alignment
1109-
const float viewportSize = m_horizontal ? visualSize.x : visualSize.y;
1110-
if (m_snapToAlignment == SnapAlignment::Center) {
1111-
// For center alignment, offset snap positions by half the viewport size
1112-
for (auto &position : snapPositions) {
1113-
position = std::max(0.0f, position - viewportSize / 2.0f);
1114-
}
1115-
} else if (m_snapToAlignment == SnapAlignment::End) {
1116-
// For end alignment, offset snap positions by the full viewport size
1117-
for (auto &position : snapPositions) {
1118-
position = std::max(0.0f, position - viewportSize);
1119-
}
1120-
}
1121-
// For Start alignment, no adjustment needed
1122-
11231163
std::sort(snapPositions.begin(), snapPositions.end());
11241164
snapPositions.erase(std::unique(snapPositions.begin(), snapPositions.end()), snapPositions.end());
11251165

@@ -1247,9 +1287,9 @@ struct CompScrollerVisual : winrt::implements<
12471287
bool m_snapToStart{true};
12481288
bool m_snapToEnd{true};
12491289
std::vector<float> m_snapToOffsets;
1250-
SnapAlignment m_snapToAlignment{SnapAlignment::Start};
12511290
bool m_inertia{false};
12521291
bool m_custom{false};
1292+
bool m_interacting{false};
12531293
winrt::Windows::Foundation::Numerics::float3 m_targetPosition;
12541294
winrt::Windows::Foundation::Numerics::float3 m_currentPosition;
12551295
winrt::Windows::Foundation::Numerics::float2 m_contentSize{0};
@@ -1263,6 +1303,12 @@ struct CompScrollerVisual : winrt::implements<
12631303
winrt::event<winrt::Windows::Foundation::EventHandler<
12641304
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
12651305
m_scrollEndDragEvent;
1306+
winrt::event<winrt::Windows::Foundation::EventHandler<
1307+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
1308+
m_scrollMomentumBeginEvent;
1309+
winrt::event<winrt::Windows::Foundation::EventHandler<
1310+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
1311+
m_scrollMomentumEndEvent;
12661312
typename TTypeRedirects::SpriteVisual m_visual{nullptr};
12671313
typename TTypeRedirects::SpriteVisual m_contentVisual{nullptr};
12681314
typename TTypeRedirects::InteractionTracker m_interactionTracker{nullptr};

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

Lines changed: 30 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
namespace winrt::Microsoft::ReactNative::Composition::implementation {
2828

2929
constexpr float c_scrollerLineDelta = 16.0f;
30-
constexpr auto c_maxSnapPoints = 1000;
3130

3231
enum class ScrollbarHitRegion : int {
3332
Unknown = -1,
@@ -741,15 +740,6 @@ void ScrollViewComponentView::updateBackgroundColor(const facebook::react::Share
741740
}
742741
}
743742

744-
winrt::Windows::Foundation::Collections::IVector<float> ScrollViewComponentView::CreateSnapToOffsets(
745-
const std::vector<float> &offsets) {
746-
auto snapToOffsets = winrt::single_threaded_vector<float>();
747-
for (const auto &offset : offsets) {
748-
snapToOffsets.Append(offset);
749-
}
750-
return snapToOffsets;
751-
}
752-
753743
void ScrollViewComponentView::updateProps(
754744
facebook::react::Props::Shared const &props,
755745
facebook::react::Props::Shared const &oldProps) noexcept {
@@ -818,13 +808,11 @@ void ScrollViewComponentView::updateProps(
818808

819809
if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd ||
820810
oldViewProps.snapToOffsets != newViewProps.snapToOffsets) {
821-
if (oldViewProps.snapToInterval != newViewProps.snapToInterval) {
822-
updateSnapPoints();
823-
} else {
824-
const auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets);
825-
m_scrollVisual.SetSnapPoints(
826-
newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView(), SnapAlignment::Center);
811+
const auto snapToOffsets = winrt::single_threaded_vector<float>();
812+
for (const auto &offset : newViewProps.snapToOffsets) {
813+
snapToOffsets.Append(static_cast<float>(offset));
827814
}
815+
m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView());
828816
}
829817
}
830818

@@ -875,9 +863,6 @@ void ScrollViewComponentView::updateContentVisualSize() noexcept {
875863
m_verticalScrollbarComponent->ContentSize(contentSize);
876864
m_horizontalScrollbarComponent->ContentSize(contentSize);
877865
m_scrollVisual.ContentSize(contentSize);
878-
879-
// Update snap points if snapToInterval is being used, as content size affects the number of snap points
880-
updateSnapPoints();
881866
}
882867

883868
void ScrollViewComponentView::prepareForRecycle() noexcept {}
@@ -1354,6 +1339,32 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
13541339
}
13551340
});
13561341

1342+
m_scrollMomentumBeginRevoker = m_scrollVisual.ScrollMomentumBegin(
1343+
winrt::auto_revoke,
1344+
[this](
1345+
winrt::IInspectable const & /*sender*/,
1346+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
1347+
auto eventEmitter = GetEventEmitter();
1348+
if (eventEmitter) {
1349+
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
1350+
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
1351+
->onMomentumScrollBegin(scrollMetrics);
1352+
}
1353+
});
1354+
1355+
m_scrollMomentumEndRevoker = m_scrollVisual.ScrollMomentumEnd(
1356+
winrt::auto_revoke,
1357+
[this](
1358+
winrt::IInspectable const & /*sender*/,
1359+
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
1360+
auto eventEmitter = GetEventEmitter();
1361+
if (eventEmitter) {
1362+
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
1363+
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
1364+
->onMomentumScrollEnd(scrollMetrics);
1365+
}
1366+
});
1367+
13571368
return visual;
13581369
}
13591370

@@ -1450,50 +1461,4 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe
14501461
void ScrollViewComponentView::updateDecelerationRate(float value) noexcept {
14511462
m_scrollVisual.SetDecelerationRate({value, value, value});
14521463
}
1453-
1454-
SnapAlignment ScrollViewComponentView::convertSnapToAlignment(
1455-
facebook::react::ScrollViewSnapToAlignment alignment) noexcept {
1456-
switch (alignment) {
1457-
case facebook::react::ScrollViewSnapToAlignment::Center:
1458-
return SnapAlignment::Center;
1459-
case facebook::react::ScrollViewSnapToAlignment::End:
1460-
return SnapAlignment::End;
1461-
case facebook::react::ScrollViewSnapToAlignment::Start:
1462-
default:
1463-
return SnapAlignment::Start;
1464-
}
1465-
}
1466-
1467-
void ScrollViewComponentView::updateSnapPoints() noexcept {
1468-
const auto &viewProps = *std::static_pointer_cast<const facebook::react::ScrollViewProps>(this->viewProps());
1469-
const auto snapToOffsets = CreateSnapToOffsets(viewProps.snapToOffsets);
1470-
// Typically used in combination with snapToAlignment and decelerationRate="fast"
1471-
auto snapAlignment = SnapAlignment::Center;
1472-
auto decelerationRate = viewProps.decelerationRate;
1473-
1474-
// snapToOffsets has priority over snapToInterval (matches React Native behavior)
1475-
if (viewProps.snapToInterval > 0 && decelerationRate >= 0.99) {
1476-
snapAlignment = convertSnapToAlignment(viewProps.snapToAlignment);
1477-
// Generate snap points based on interval
1478-
// Calculate the content size to determine how many intervals to create
1479-
float contentLength = viewProps.horizontal
1480-
? std::max(m_contentSize.width, m_layoutMetrics.frame.size.width) * m_layoutMetrics.pointScaleFactor
1481-
: std::max(m_contentSize.height, m_layoutMetrics.frame.size.height) * m_layoutMetrics.pointScaleFactor;
1482-
1483-
float interval = static_cast<float>(viewProps.snapToInterval) * m_layoutMetrics.pointScaleFactor;
1484-
1485-
// Ensure we have a reasonable minimum interval to avoid infinite loops or excessive memory usage
1486-
if (interval >= 1.0f && contentLength > 0) {
1487-
// Generate offsets at each interval, but limit the number of snap points to avoid excessive memory usage
1488-
int snapPointCount = 0;
1489-
1490-
for (float offset = 0; offset <= contentLength && snapPointCount < c_maxSnapPoints; offset += interval) {
1491-
snapToOffsets.Append(offset);
1492-
snapPointCount++;
1493-
}
1494-
}
1495-
}
1496-
1497-
m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView(), snapAlignment);
1498-
}
14991464
} // namespace winrt::Microsoft::ReactNative::Composition::implementation

0 commit comments

Comments
 (0)