diff --git a/change/react-native-windows-c0f0469c-88ea-48c1-9ab5-7fc144979446.json b/change/react-native-windows-c0f0469c-88ea-48c1-9ab5-7fc144979446.json new file mode 100644 index 00000000000..33cae5a2bba --- /dev/null +++ b/change/react-native-windows-c0f0469c-88ea-48c1-9ab5-7fc144979446.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement snapToInterval property for Fabric ScrollView", + "packageName": "react-native-windows", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index 2146211e326..fce8c421d5d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -807,12 +807,22 @@ void ScrollViewComponentView::updateProps( } if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd || - oldViewProps.snapToOffsets != newViewProps.snapToOffsets) { - const auto snapToOffsets = winrt::single_threaded_vector(); - for (const auto &offset : newViewProps.snapToOffsets) { - snapToOffsets.Append(static_cast(offset)); + oldViewProps.snapToOffsets != newViewProps.snapToOffsets || + oldViewProps.snapToInterval != newViewProps.snapToInterval) { + if (newViewProps.snapToInterval > 0 || oldViewProps.snapToInterval != newViewProps.snapToInterval) { + // Use the comprehensive updateSnapPoints method when snapToInterval is involved + // Typically used in combination with snapToAlignment and decelerationRate="fast". + if (newViewProps.decelerationRate >= 0.99) { + updateSnapPoints(); + } + } else { + // Keep original inline logic for the basic snapToOffsets case + const auto snapToOffsets = winrt::single_threaded_vector(); + for (const auto &offset : newViewProps.snapToOffsets) { + snapToOffsets.Append(static_cast(offset)); + } + m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView()); } - m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView()); } } @@ -863,6 +873,9 @@ void ScrollViewComponentView::updateContentVisualSize() noexcept { m_verticalScrollbarComponent->ContentSize(contentSize); m_horizontalScrollbarComponent->ContentSize(contentSize); m_scrollVisual.ContentSize(contentSize); + + // Update snap points if snapToInterval is being used, as content size affects the number of snap points + updateSnapPoints(); } void ScrollViewComponentView::prepareForRecycle() noexcept {} @@ -1435,4 +1448,39 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe void ScrollViewComponentView::updateDecelerationRate(float value) noexcept { m_scrollVisual.SetDecelerationRate({value, value, value}); } + +void ScrollViewComponentView::updateSnapPoints() noexcept { + const auto &viewProps = *std::static_pointer_cast(this->viewProps()); + const auto snapToOffsets = winrt::single_threaded_vector(); + + // snapToOffsets has priority over snapToInterval (matches React Native behavior) + if (viewProps.snapToOffsets.size() > 0) { + // Use explicit snapToOffsets + for (const auto &offset : viewProps.snapToOffsets) { + snapToOffsets.Append(static_cast(offset)); + } + } else if (viewProps.snapToInterval > 0) { + // Generate snap points based on interval + // Calculate the content size to determine how many intervals to create + float contentLength = viewProps.horizontal + ? std::max(m_contentSize.width, m_layoutMetrics.frame.size.width) * m_layoutMetrics.pointScaleFactor + : std::max(m_contentSize.height, m_layoutMetrics.frame.size.height) * m_layoutMetrics.pointScaleFactor; + + float interval = static_cast(viewProps.snapToInterval) * m_layoutMetrics.pointScaleFactor; + + // Ensure we have a reasonable minimum interval to avoid infinite loops or excessive memory usage + if (interval >= 1.0f && contentLength > 0) { + // Generate offsets at each interval, but limit the number of snap points to avoid excessive memory usage + const int maxSnapPoints = 1000; // Reasonable limit + int snapPointCount = 0; + + for (float offset = 0; offset <= contentLength && snapPointCount < maxSnapPoints; offset += interval) { + snapToOffsets.Append(offset); + snapPointCount++; + } + } + } + + m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView()); +} } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h index 1f5b861abe7..37db2ac6ac1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h @@ -121,6 +121,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements< private: void updateDecelerationRate(float value) noexcept; void updateContentVisualSize() noexcept; + void updateSnapPoints() noexcept; bool scrollToEnd(bool animate) noexcept; bool scrollToStart(bool animate) noexcept; bool scrollDown(float delta, bool animate) noexcept;