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..e849d14934d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp @@ -27,6 +27,8 @@ namespace Microsoft::ReactNative::Composition::Experimental { +using namespace winrt::Microsoft::ReactNative::Composition::Experimental; + template struct CompositionTypeTraits {}; @@ -871,9 +873,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, + SnapAlignment snapToAlignment) noexcept { m_snapToStart = snapToStart; m_snapToEnd = snapToEnd; + m_snapToAlignment = snapToAlignment; m_snapToOffsets.clear(); if (offsets) { for (auto const &offset : offsets) { @@ -1100,6 +1104,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 == 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 == 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 +1247,7 @@ struct CompScrollerVisual : winrt::implements< bool m_snapToStart{true}; bool m_snapToEnd{true}; std::vector m_snapToOffsets; + SnapAlignment m_snapToAlignment{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 f5d7787baa2..8113b0ef4dc 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp @@ -817,16 +817,13 @@ void ScrollViewComponentView::updateProps( } if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd || - 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". + oldViewProps.snapToOffsets != newViewProps.snapToOffsets) { + if (oldViewProps.snapToInterval != newViewProps.snapToInterval) { updateSnapPoints(); } else { - auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets); - m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView()); + const auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets); + m_scrollVisual.SetSnapPoints( + newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView(), SnapAlignment::Center); } } } @@ -1454,12 +1451,29 @@ void ScrollViewComponentView::updateDecelerationRate(float value) noexcept { m_scrollVisual.SetDecelerationRate({value, value, value}); } +SnapAlignment ScrollViewComponentView::convertSnapToAlignment( + facebook::react::ScrollViewSnapToAlignment alignment) noexcept { + switch (alignment) { + case facebook::react::ScrollViewSnapToAlignment::Center: + return SnapAlignment::Center; + case facebook::react::ScrollViewSnapToAlignment::End: + return SnapAlignment::End; + case facebook::react::ScrollViewSnapToAlignment::Start: + default: + return SnapAlignment::Start; + } +} + void ScrollViewComponentView::updateSnapPoints() noexcept { const auto &viewProps = *std::static_pointer_cast(this->viewProps()); const auto snapToOffsets = CreateSnapToOffsets(viewProps.snapToOffsets); + // Typically used in combination with snapToAlignment and decelerationRate="fast" + auto snapAlignment = SnapAlignment::Center; + auto decelerationRate = viewProps.decelerationRate; // snapToOffsets has priority over snapToInterval (matches React Native behavior) - if (viewProps.snapToInterval > 0) { + if (viewProps.snapToInterval > 0 && decelerationRate >= 0.99) { + snapAlignment = convertSnapToAlignment(viewProps.snapToAlignment); // Generate snap points based on interval // Calculate the content size to determine how many intervals to create float contentLength = viewProps.horizontal @@ -1480,6 +1494,6 @@ void ScrollViewComponentView::updateSnapPoints() noexcept { } } - m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView()); + m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView(), snapAlignment); } } // 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 d2eeffd5385..e22f6364607 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.h @@ -18,6 +18,8 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation { +using namespace Microsoft::ReactNative::Composition::Experimental; + struct ScrollBarComponent; struct ScrollViewComponentView : ScrollViewComponentViewT { @@ -135,6 +137,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; + SnapAlignment convertSnapToAlignment(facebook::react::ScrollViewSnapToAlignment alignment) noexcept; winrt::Windows::Foundation::Collections::IVector CreateSnapToOffsets(const std::vector &offsets); facebook::react::Size m_contentSize;