Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 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 snapToEnd using scroll event handlers instead of inertia modifiers",
"packageName": "react-native-windows",
"email": "198982749+Copilot@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe('ScrollView Tests', () => {
const dump = await dumpVisualTree('scroll_to_end_button');
expect(dump).toMatchSnapshot();
});

// Disable tests where testID is not found.
/*test('ScrollViews can have sticky headers', async () => {
const component = await app.findElementByTestID('scroll_sticky_header');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,8 @@ exports[`ScrollView Tests ScrollViews can scroll an item list horizontally 1`] =
}
`;



exports[`ScrollView Tests ScrollViews has flash scroll indicators 1`] = `
{
"Automation Tree": {
Expand Down
1 change: 1 addition & 0 deletions vnext/Microsoft.ReactNative/CompositionSwitcher.idl
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ namespace Microsoft.ReactNative.Composition.Experimental
void SetDecelerationRate(Windows.Foundation.Numerics.Vector3 decelerationRate);
void SetMaximumZoomScale(Single maximumZoomScale);
void SetMinimumZoomScale(Single minimumZoomScale);
void ConfigureSnapToEnd(Boolean snapToEnd, Boolean horizontal);
Boolean Horizontal;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <Windows.Graphics.Interop.h>
#include <windows.ui.composition.interop.h>
#include <winrt/Microsoft.ReactNative.Composition.Input.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
#include <winrt/Windows.UI.Composition.h>
#include <winrt/Windows.UI.Composition.interactions.h>
Expand Down Expand Up @@ -74,6 +75,10 @@ struct CompositionTypeTraits<WindowsTypeTag> {
winrt::Windows::UI::Composition::Interactions::InteractionTrackerRequestIgnoredArgs;
using InteractionTrackerValuesChangedArgs =
winrt::Windows::UI::Composition::Interactions::InteractionTrackerValuesChangedArgs;
using InteractionTrackerInertiaModifier =
winrt::Windows::UI::Composition::Interactions::InteractionTrackerInertiaModifier;
using InteractionTrackerInertiaRestingValue =
winrt::Windows::UI::Composition::Interactions::InteractionTrackerInertiaRestingValue;
using ScalarKeyFrameAnimation = winrt::Windows::UI::Composition::ScalarKeyFrameAnimation;
using ShapeVisual = winrt::Windows::UI::Composition::ShapeVisual;
using SpriteVisual = winrt::Windows::UI::Composition::SpriteVisual;
Expand Down Expand Up @@ -143,6 +148,10 @@ struct CompositionTypeTraits<MicrosoftTypeTag> {
winrt::Microsoft::UI::Composition::Interactions::InteractionTrackerRequestIgnoredArgs;
using InteractionTrackerValuesChangedArgs =
winrt::Microsoft::UI::Composition::Interactions::InteractionTrackerValuesChangedArgs;
using InteractionTrackerInertiaModifier =
winrt::Microsoft::UI::Composition::Interactions::InteractionTrackerInertiaModifier;
using InteractionTrackerInertiaRestingValue =
winrt::Microsoft::UI::Composition::Interactions::InteractionTrackerInertiaRestingValue;
using ScalarKeyFrameAnimation = winrt::Microsoft::UI::Composition::ScalarKeyFrameAnimation;
using ShapeVisual = winrt::Microsoft::UI::Composition::ShapeVisual;
using SpriteVisual = winrt::Microsoft::UI::Composition::SpriteVisual;
Expand Down Expand Up @@ -1030,6 +1039,44 @@ struct CompScrollerVisual : winrt::implements<
SetAnimationClass<TTypeRedirects>(value, m_visual);
}

void ConfigureSnapToEnd(bool snapToEnd, bool horizontal) noexcept {
// Clear existing inertia modifiers
m_interactionTracker.ConfigurePositionXInertiaModifiers({});
m_interactionTracker.ConfigurePositionYInertiaModifiers({});

if (snapToEnd) {
auto compositor = m_visual.Compositor();

if (horizontal) {
// Create horizontal snap to end inertia modifier
auto horizontalModifier = typename TTypeRedirects::InteractionTrackerInertiaRestingValue::Create(compositor);
// Snap to the end when we're past 80% of the maximum scroll position
horizontalModifier.Condition(
compositor.CreateExpressionAnimation(L"tracker.NaturalRestingPosition.x >= tracker.MaxPosition.x * 0.8"));
horizontalModifier.RestingValue(compositor.CreateExpressionAnimation(L"tracker.MaxPosition.x"));
horizontalModifier.Condition().SetReferenceParameter(L"tracker", m_interactionTracker);
horizontalModifier.RestingValue().SetReferenceParameter(L"tracker", m_interactionTracker);

auto modifiers = winrt::single_threaded_vector<typename TTypeRedirects::InteractionTrackerInertiaModifier>();
modifiers.Append(horizontalModifier);
m_interactionTracker.ConfigurePositionXInertiaModifiers(modifiers);
} else {
// Create vertical snap to end inertia modifier
auto verticalModifier = typename TTypeRedirects::InteractionTrackerInertiaRestingValue::Create(compositor);
// Snap to the end when we're past 80% of the maximum scroll position
verticalModifier.Condition(
compositor.CreateExpressionAnimation(L"tracker.NaturalRestingPosition.y >= tracker.MaxPosition.y * 0.8"));
verticalModifier.RestingValue(compositor.CreateExpressionAnimation(L"tracker.MaxPosition.y"));
verticalModifier.Condition().SetReferenceParameter(L"tracker", m_interactionTracker);
verticalModifier.RestingValue().SetReferenceParameter(L"tracker", m_interactionTracker);

auto modifiers = winrt::single_threaded_vector<typename TTypeRedirects::InteractionTrackerInertiaModifier>();
modifiers.Append(verticalModifier);
m_interactionTracker.ConfigurePositionYInertiaModifiers(modifiers);
}
}
}

private:
void FireScrollPositionChanged(winrt::Windows::Foundation::Numerics::float2 position) noexcept {
m_scrollPositionChangedEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,10 @@ void ScrollViewComponentView::updateProps(

if (!oldProps || oldViewProps.horizontal != newViewProps.horizontal) {
m_scrollVisual.Horizontal(newViewProps.horizontal);
// Reconfigure snap behavior for new scroll direction if snapToEnd is enabled
if (m_snapToEnd) {
m_scrollVisual.ConfigureSnapToEnd(m_snapToEnd, newViewProps.horizontal);
}
}

if (!oldProps || oldViewProps.showsHorizontalScrollIndicator != newViewProps.showsHorizontalScrollIndicator) {
Expand Down Expand Up @@ -805,6 +809,13 @@ void ScrollViewComponentView::updateProps(
if (oldViewProps.zoomScale != newViewProps.zoomScale) {
m_scrollVisual.Scale({newViewProps.zoomScale, newViewProps.zoomScale, newViewProps.zoomScale});
}

if (!oldProps || oldViewProps.snapToEnd != newViewProps.snapToEnd) {
// snapToEnd property controls whether the end of the scroll content
// should be treated as a snap point using Windows Composition inertia modifiers
m_snapToEnd = newViewProps.snapToEnd;
m_scrollVisual.ConfigureSnapToEnd(m_snapToEnd, newViewProps.horizontal);
}
}

void ScrollViewComponentView::updateState(
Expand Down Expand Up @@ -854,6 +865,12 @@ void ScrollViewComponentView::updateContentVisualSize() noexcept {
m_verticalScrollbarComponent->ContentSize(contentSize);
m_horizontalScrollbarComponent->ContentSize(contentSize);
m_scrollVisual.ContentSize(contentSize);

// Reconfigure snap behavior when content size changes if snapToEnd is enabled
if (m_snapToEnd) {
const auto &viewProps = *std::static_pointer_cast<const facebook::react::ScrollViewProps>(this->viewProps());
m_scrollVisual.ConfigureSnapToEnd(m_snapToEnd, viewProps.horizontal);
}
}

void ScrollViewComponentView::prepareForRecycle() noexcept {}
Expand Down Expand Up @@ -1322,6 +1339,7 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
winrt::IInspectable const & /*sender*/,
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
updateStateWithContentOffset();

auto eventEmitter = GetEventEmitter();
if (eventEmitter) {
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ struct ScrollInteractionTrackerOwner : public winrt::implements<
bool m_dismissKeyboardOnDrag = false;
double m_scrollEventThrottle{0.0};
bool m_allowNextScrollNoMatterWhat{false};
bool m_snapToEnd{true}; // Default to true per React Native documentation
std::chrono::steady_clock::time_point m_lastScrollEventTime{};
std::shared_ptr<facebook::react::ScrollViewShadowNode::ConcreteState const> m_state;
};
Expand Down
Loading
Loading