Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
112 changes: 56 additions & 56 deletions vnext/codegen/rnwcoreJSI-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -946,41 +946,6 @@ NativeAnimatedTurboModuleCxxSpecJSI::NativeAnimatedTurboModuleCxxSpecJSI(std::sh
methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAnimatedTurboModuleCxxSpecJSI_removeListeners};
methodMap_["queueAndExecuteBatchedOperations"] = MethodMetadata {1, __hostFunction_NativeAnimatedTurboModuleCxxSpecJSI_queueAndExecuteBatchedOperations};
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_getColorScheme(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
auto result = static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->getColorScheme(
rt
);
return result ? jsi::Value(std::move(*result)) : jsi::Value::null();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_setColorScheme(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->setColorScheme(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
);
return jsi::Value::undefined();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_addListener(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->addListener(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
);
return jsi::Value::undefined();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_removeListeners(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->removeListeners(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
);
return jsi::Value::undefined();
}

NativeAppearanceCxxSpecJSI::NativeAppearanceCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("Appearance", jsInvoker) {
methodMap_["getColorScheme"] = MethodMetadata {0, __hostFunction_NativeAppearanceCxxSpecJSI_getColorScheme};
methodMap_["setColorScheme"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_setColorScheme};
methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_addListener};
methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_removeListeners};
}
static jsi::Value __hostFunction_NativeAppStateCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeAppStateCxxSpecJSI *>(&turboModule)->getConstants(
rt
Expand Down Expand Up @@ -1026,6 +991,41 @@ NativeAppThemeCxxSpecJSI::NativeAppThemeCxxSpecJSI(std::shared_ptr<CallInvoker>
: TurboModule("AppTheme", jsInvoker) {
methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeAppThemeCxxSpecJSI_getConstants};
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_getColorScheme(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
auto result = static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->getColorScheme(
rt
);
return result ? jsi::Value(std::move(*result)) : jsi::Value::null();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_setColorScheme(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->setColorScheme(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
);
return jsi::Value::undefined();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_addListener(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->addListener(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
);
return jsi::Value::undefined();
}
static jsi::Value __hostFunction_NativeAppearanceCxxSpecJSI_removeListeners(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeAppearanceCxxSpecJSI *>(&turboModule)->removeListeners(
rt,
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
);
return jsi::Value::undefined();
}

NativeAppearanceCxxSpecJSI::NativeAppearanceCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("Appearance", jsInvoker) {
methodMap_["getColorScheme"] = MethodMetadata {0, __hostFunction_NativeAppearanceCxxSpecJSI_getColorScheme};
methodMap_["setColorScheme"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_setColorScheme};
methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_addListener};
methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAppearanceCxxSpecJSI_removeListeners};
}
static jsi::Value __hostFunction_NativeBlobModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeBlobModuleCxxSpecJSI *>(&turboModule)->getConstants(
rt
Expand Down Expand Up @@ -1129,27 +1129,6 @@ NativeClipboardCxxSpecJSI::NativeClipboardCxxSpecJSI(std::shared_ptr<CallInvoker
methodMap_["getString"] = MethodMetadata {0, __hostFunction_NativeClipboardCxxSpecJSI_getString};
methodMap_["setString"] = MethodMetadata {1, __hostFunction_NativeClipboardCxxSpecJSI_setString};
}
static jsi::Value __hostFunction_NativeDeviceEventManagerCxxSpecJSI_invokeDefaultBackPressHandler(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeDeviceEventManagerCxxSpecJSI *>(&turboModule)->invokeDefaultBackPressHandler(
rt
);
return jsi::Value::undefined();
}

NativeDeviceEventManagerCxxSpecJSI::NativeDeviceEventManagerCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("DeviceEventManager", jsInvoker) {
methodMap_["invokeDefaultBackPressHandler"] = MethodMetadata {0, __hostFunction_NativeDeviceEventManagerCxxSpecJSI_invokeDefaultBackPressHandler};
}
static jsi::Value __hostFunction_NativeDeviceInfoCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeDeviceInfoCxxSpecJSI *>(&turboModule)->getConstants(
rt
);
}

NativeDeviceInfoCxxSpecJSI::NativeDeviceInfoCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("DeviceInfo", jsInvoker) {
methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeDeviceInfoCxxSpecJSI_getConstants};
}
static jsi::Value __hostFunction_NativeDevLoadingViewCxxSpecJSI_showMessage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeDevLoadingViewCxxSpecJSI *>(&turboModule)->showMessage(
rt,
Expand Down Expand Up @@ -1293,6 +1272,27 @@ NativeDevSettingsCxxSpecJSI::NativeDevSettingsCxxSpecJSI(std::shared_ptr<CallInv
methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeDevSettingsCxxSpecJSI_removeListeners};
methodMap_["setIsShakeToShowDevMenuEnabled"] = MethodMetadata {1, __hostFunction_NativeDevSettingsCxxSpecJSI_setIsShakeToShowDevMenuEnabled};
}
static jsi::Value __hostFunction_NativeDeviceEventManagerCxxSpecJSI_invokeDefaultBackPressHandler(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
static_cast<NativeDeviceEventManagerCxxSpecJSI *>(&turboModule)->invokeDefaultBackPressHandler(
rt
);
return jsi::Value::undefined();
}

NativeDeviceEventManagerCxxSpecJSI::NativeDeviceEventManagerCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("DeviceEventManager", jsInvoker) {
methodMap_["invokeDefaultBackPressHandler"] = MethodMetadata {0, __hostFunction_NativeDeviceEventManagerCxxSpecJSI_invokeDefaultBackPressHandler};
}
static jsi::Value __hostFunction_NativeDeviceInfoCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeDeviceInfoCxxSpecJSI *>(&turboModule)->getConstants(
rt
);
}

NativeDeviceInfoCxxSpecJSI::NativeDeviceInfoCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("DeviceInfo", jsInvoker) {
methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeDeviceInfoCxxSpecJSI_getConstants};
}
static jsi::Value __hostFunction_NativeDialogManagerAndroidCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeDialogManagerAndroidCxxSpecJSI *>(&turboModule)->getConstants(
rt
Expand Down
Loading