Skip to content

Commit 2cde3f1

Browse files
authored
[Fabric] Implement snapToInterval property for ScrollView (microsoft#14847)
1 parent 7b7c004 commit 2cde3f1

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
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 snapToInterval property for Fabric ScrollView",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

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

2929
constexpr float c_scrollerLineDelta = 16.0f;
30+
constexpr auto c_maxSnapPoints = 1000;
3031

3132
enum class ScrollbarHitRegion : int {
3233
Unknown = -1,
@@ -740,6 +741,15 @@ void ScrollViewComponentView::updateBackgroundColor(const facebook::react::Share
740741
}
741742
}
742743

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+
743753
void ScrollViewComponentView::updateProps(
744754
facebook::react::Props::Shared const &props,
745755
facebook::react::Props::Shared const &oldProps) noexcept {
@@ -807,12 +817,17 @@ void ScrollViewComponentView::updateProps(
807817
}
808818

809819
if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd ||
810-
oldViewProps.snapToOffsets != newViewProps.snapToOffsets) {
811-
const auto snapToOffsets = winrt::single_threaded_vector<float>();
812-
for (const auto &offset : newViewProps.snapToOffsets) {
813-
snapToOffsets.Append(static_cast<float>(offset));
820+
oldViewProps.snapToOffsets != newViewProps.snapToOffsets ||
821+
oldViewProps.snapToInterval != newViewProps.snapToInterval) {
822+
if ((newViewProps.snapToInterval > 0 || oldViewProps.snapToInterval != newViewProps.snapToInterval) &&
823+
(newViewProps.decelerationRate >= 0.99)) {
824+
// Use the comprehensive updateSnapPoints method when snapToInterval is involved
825+
// Typically used in combination with snapToAlignment and decelerationRate="fast".
826+
updateSnapPoints();
827+
} else {
828+
auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets);
829+
m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView());
814830
}
815-
m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView());
816831
}
817832
}
818833

@@ -863,6 +878,9 @@ void ScrollViewComponentView::updateContentVisualSize() noexcept {
863878
m_verticalScrollbarComponent->ContentSize(contentSize);
864879
m_horizontalScrollbarComponent->ContentSize(contentSize);
865880
m_scrollVisual.ContentSize(contentSize);
881+
882+
// Update snap points if snapToInterval is being used, as content size affects the number of snap points
883+
updateSnapPoints();
866884
}
867885

868886
void ScrollViewComponentView::prepareForRecycle() noexcept {}
@@ -1435,4 +1453,33 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe
14351453
void ScrollViewComponentView::updateDecelerationRate(float value) noexcept {
14361454
m_scrollVisual.SetDecelerationRate({value, value, value});
14371455
}
1456+
1457+
void ScrollViewComponentView::updateSnapPoints() noexcept {
1458+
const auto &viewProps = *std::static_pointer_cast<const facebook::react::ScrollViewProps>(this->viewProps());
1459+
const auto snapToOffsets = CreateSnapToOffsets(viewProps.snapToOffsets);
1460+
1461+
// snapToOffsets has priority over snapToInterval (matches React Native behavior)
1462+
if (viewProps.snapToInterval > 0) {
1463+
// Generate snap points based on interval
1464+
// Calculate the content size to determine how many intervals to create
1465+
float contentLength = viewProps.horizontal
1466+
? std::max(m_contentSize.width, m_layoutMetrics.frame.size.width) * m_layoutMetrics.pointScaleFactor
1467+
: std::max(m_contentSize.height, m_layoutMetrics.frame.size.height) * m_layoutMetrics.pointScaleFactor;
1468+
1469+
float interval = static_cast<float>(viewProps.snapToInterval) * m_layoutMetrics.pointScaleFactor;
1470+
1471+
// Ensure we have a reasonable minimum interval to avoid infinite loops or excessive memory usage
1472+
if (interval >= 1.0f && contentLength > 0) {
1473+
// Generate offsets at each interval, but limit the number of snap points to avoid excessive memory usage
1474+
int snapPointCount = 0;
1475+
1476+
for (float offset = 0; offset <= contentLength && snapPointCount < c_maxSnapPoints; offset += interval) {
1477+
snapToOffsets.Append(offset);
1478+
snapPointCount++;
1479+
}
1480+
}
1481+
}
1482+
1483+
m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView());
1484+
}
14381485
} // namespace winrt::Microsoft::ReactNative::Composition::implementation

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
121121
private:
122122
void updateDecelerationRate(float value) noexcept;
123123
void updateContentVisualSize() noexcept;
124+
void updateSnapPoints() noexcept;
124125
bool scrollToEnd(bool animate) noexcept;
125126
bool scrollToStart(bool animate) noexcept;
126127
bool scrollDown(float delta, bool animate) noexcept;
@@ -134,6 +135,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
134135
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) noexcept;
135136
void updateShowsHorizontalScrollIndicator(bool value) noexcept;
136137
void updateShowsVerticalScrollIndicator(bool value) noexcept;
138+
winrt::Windows::Foundation::Collections::IVector<float> CreateSnapToOffsets(const std::vector<float> &offsets);
137139

138140
facebook::react::Size m_contentSize;
139141
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual m_scrollVisual{nullptr};

0 commit comments

Comments
 (0)