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..f5d7787baa2 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -27,6 +27,7 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation { constexpr float c_scrollerLineDelta = 16.0f; +constexpr auto c_maxSnapPoints = 1000; enum class ScrollbarHitRegion : int { Unknown = -1, @@ -740,6 +741,15 @@ void ScrollViewComponentView::updateBackgroundColor(const facebook::react::Share } } +winrt::Windows::Foundation::Collections::IVector ScrollViewComponentView::CreateSnapToOffsets( + const std::vector &offsets) { + auto snapToOffsets = winrt::single_threaded_vector(); + for (const auto &offset : offsets) { + snapToOffsets.Append(offset); + } + return snapToOffsets; +} + void ScrollViewComponentView::updateProps( facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept { @@ -807,12 +817,17 @@ 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) && + (newViewProps.decelerationRate >= 0.99)) { + // Use the comprehensive updateSnapPoints method when snapToInterval is involved + // Typically used in combination with snapToAlignment and decelerationRate="fast". + updateSnapPoints(); + } else { + auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets); + m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView()); } - m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView()); } } @@ -863,6 +878,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 +1453,33 @@ 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 = CreateSnapToOffsets(viewProps.snapToOffsets); + + // snapToOffsets has priority over snapToInterval (matches React Native behavior) + 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 + int snapPointCount = 0; + + for (float offset = 0; offset <= contentLength && snapPointCount < c_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..d2eeffd5385 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; @@ -134,6 +135,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements< winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) noexcept; void updateShowsHorizontalScrollIndicator(bool value) noexcept; void updateShowsVerticalScrollIndicator(bool value) noexcept; + winrt::Windows::Foundation::Collections::IVector CreateSnapToOffsets(const std::vector &offsets); facebook::react::Size m_contentSize; winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual m_scrollVisual{nullptr};