Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Implement snapToInterval property for Fabric ScrollView",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -807,12 +807,9 @@ void ScrollViewComponentView::updateProps(
}

if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd ||
oldViewProps.snapToOffsets != newViewProps.snapToOffsets) {
const auto snapToOffsets = winrt::single_threaded_vector<float>();
for (const auto &offset : newViewProps.snapToOffsets) {
snapToOffsets.Append(static_cast<float>(offset));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't remove this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the original inline snap points logic as requested. The code now preserves the existing logic for basic snapToOffsets cases and only uses the new updateSnapPoints() method when snapToInterval is involved. Commit 0424e18.

}
m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView());
oldViewProps.snapToOffsets != newViewProps.snapToOffsets ||
oldViewProps.snapToInterval != newViewProps.snapToInterval) {
updateSnapPoints();
}
}

Expand Down Expand Up @@ -863,6 +860,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 {}
Expand Down Expand Up @@ -1435,4 +1435,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<const facebook::react::ScrollViewProps>(this->viewProps());
const auto snapToOffsets = winrt::single_threaded_vector<float>();

// 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<float>(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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this snappoint is dependent on the size, you'll have to ensure that this function is rerun when the size changes.. we should do a special InteractionTrackerInertiaRestingValue for the end snap point, which uses some references to the size so we dont need to constantly recalculate all of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right about the performance concern. The current implementation recalculates all snap points on every content size change, which is inefficient.

I'll implement the optimization using InteractionTrackerInertiaRestingValue with expression animations (similar to how snapToEnd works in CompositionContextHelper.cpp). This will let the composition system dynamically calculate snap points based on size references rather than pre-calculating them.

Working on this now.


float interval = static_cast<float>(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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down