diff --git a/change/react-native-windows-ae44eda2-ce08-4405-916b-db6f08ee6786.json b/change/react-native-windows-ae44eda2-ce08-4405-916b-db6f08ee6786.json new file mode 100644 index 00000000000..c7b809a2e37 --- /dev/null +++ b/change/react-native-windows-ae44eda2-ce08-4405-916b-db6f08ee6786.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement snapToAlignment support for Fabric ScrollView - interface and prop handling", + "packageName": "react-native-windows", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/CompositionSwitcher.idl b/vnext/Microsoft.ReactNative/CompositionSwitcher.idl index fe102e92c4b..002bbd03e4a 100644 --- a/vnext/Microsoft.ReactNative/CompositionSwitcher.idl +++ b/vnext/Microsoft.ReactNative/CompositionSwitcher.idl @@ -31,6 +31,13 @@ namespace Microsoft.ReactNative.Composition.Experimental SwitchThumb, }; + enum SnapAlignment + { + Start, + Center, + End, + }; + [webhosthidden] [uuid("172def51-9e1a-4e3c-841a-e5a470065acc")] // uuid needed for empty interfaces [version(0)] @@ -120,7 +127,7 @@ namespace Microsoft.ReactNative.Composition.Experimental void SetMaximumZoomScale(Single maximumZoomScale); void SetMinimumZoomScale(Single minimumZoomScale); Boolean Horizontal; - void SetSnapPoints(Boolean snapToStart, Boolean snapToEnd, Windows.Foundation.Collections.IVectorView offsets); + void SetSnapPoints(Boolean snapToStart, Boolean snapToEnd, Windows.Foundation.Collections.IVectorView offsets, SnapAlignment snapToAlignment); } [webhosthidden] diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp index 0db57ff401f..d9f59900e4c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp @@ -871,9 +871,11 @@ struct CompScrollerVisual : winrt::implements< void SetSnapPoints( bool snapToStart, bool snapToEnd, - winrt::Windows::Foundation::Collections::IVectorView const &offsets) noexcept { + winrt::Windows::Foundation::Collections::IVectorView const &offsets, + winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment snapToAlignment) noexcept { m_snapToStart = snapToStart; m_snapToEnd = snapToEnd; + m_snapToAlignment = snapToAlignment; m_snapToOffsets.clear(); if (offsets) { for (auto const &offset : offsets) { @@ -1100,6 +1102,22 @@ struct CompScrollerVisual : winrt::implements< } snapPositions.insert(snapPositions.end(), m_snapToOffsets.begin(), m_snapToOffsets.end()); + + // Adjust snap positions based on alignment + const float viewportSize = m_horizontal ? visualSize.x : visualSize.y; + if (m_snapToAlignment == winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::Center) { + // For center alignment, offset snap positions by half the viewport size + for (auto &position : snapPositions) { + position = std::max(0.0f, position - viewportSize / 2.0f); + } + } else if (m_snapToAlignment == winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::End) { + // For end alignment, offset snap positions by the full viewport size + for (auto &position : snapPositions) { + position = std::max(0.0f, position - viewportSize); + } + } + // For Start alignment, no adjustment needed + std::sort(snapPositions.begin(), snapPositions.end()); snapPositions.erase(std::unique(snapPositions.begin(), snapPositions.end()), snapPositions.end()); @@ -1227,6 +1245,8 @@ struct CompScrollerVisual : winrt::implements< bool m_snapToStart{true}; bool m_snapToEnd{true}; std::vector m_snapToOffsets; + winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment m_snapToAlignment{ + winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::Start}; bool m_inertia{false}; bool m_custom{false}; winrt::Windows::Foundation::Numerics::float3 m_targetPosition; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp index 2146211e326..fcdf90e50d9 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -807,12 +807,17 @@ void ScrollViewComponentView::updateProps( } if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd || - oldViewProps.snapToOffsets != newViewProps.snapToOffsets) { + oldViewProps.snapToOffsets != newViewProps.snapToOffsets || + oldViewProps.snapToAlignment != newViewProps.snapToAlignment) { 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()); + + auto snapAlignment = convertSnapToAlignment(newViewProps.snapToAlignment); + + m_scrollVisual.SetSnapPoints( + newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView(), snapAlignment); } } @@ -1435,4 +1440,17 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe void ScrollViewComponentView::updateDecelerationRate(float value) noexcept { m_scrollVisual.SetDecelerationRate({value, value, value}); } + +winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment ScrollViewComponentView::convertSnapToAlignment( + facebook::react::ScrollViewSnapToAlignment alignment) noexcept { + switch (alignment) { + case facebook::react::ScrollViewSnapToAlignment::Center: + return winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::Center; + case facebook::react::ScrollViewSnapToAlignment::End: + return winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::End; + case facebook::react::ScrollViewSnapToAlignment::Start: + default: + return winrt::Microsoft::ReactNative::Composition::Experimental::SnapAlignment::Start; + } +} } // 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..de10da2f143 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h @@ -134,6 +134,8 @@ 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::Microsoft::ReactNative::Composition::Experimental::SnapAlignment convertSnapToAlignment( + facebook::react::ScrollViewSnapToAlignment alignment) noexcept; facebook::react::Size m_contentSize; winrt::Microsoft::ReactNative::Composition::Experimental::IScrollVisual m_scrollVisual{nullptr};