Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 onMomentumScrollEnd and onMomentumScrollBegin for Fabric ScrollView",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
6 changes: 6 additions & 0 deletions packages/playground/Samples/scrollViewSnapSample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ export default class Bootstrap extends React.Component<{}, any> {
onScrollEndDrag={() => {
console.log('onScrollEndDrag');
}}
onMomentumScrollBegin={() => {
console.log('onMomentumScrollBegin');
}}
onMomentumScrollEnd={() => {
console.log('onMomentumScrollEnd');
}}
onScroll={() => {
console.log('onScroll');
}}
Expand Down
11 changes: 3 additions & 8 deletions vnext/Microsoft.ReactNative/CompositionSwitcher.idl
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ 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)]
Expand Down Expand Up @@ -118,6 +111,8 @@ namespace Microsoft.ReactNative.Composition.Experimental
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollPositionChanged;
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollBeginDrag;
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollEndDrag;
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumBegin;
event Windows.Foundation.EventHandler<IScrollPositionChangedArgs> ScrollMomentumEnd;
void ContentSize(Windows.Foundation.Numerics.Vector2 size);
Windows.Foundation.Numerics.Vector3 ScrollPosition { get; };
void ScrollBy(Windows.Foundation.Numerics.Vector3 offset, Boolean animate);
Expand All @@ -127,7 +122,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<Single> offsets, SnapAlignment snapToAlignment);
void SetSnapPoints(Boolean snapToStart, Boolean snapToEnd, Windows.Foundation.Collections.IVectorView<Single> offsets);
}

[webhosthidden]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@

namespace Microsoft::ReactNative::Composition::Experimental {

using namespace winrt::Microsoft::ReactNative::Composition::Experimental;

template <typename TSpriteVisual>
struct CompositionTypeTraits {};

Expand Down Expand Up @@ -711,24 +709,50 @@ struct CompScrollerVisual : winrt::implements<
void IdleStateEntered(
typename TTypeRedirects::InteractionTracker sender,
typename TTypeRedirects::InteractionTrackerIdleStateEnteredArgs args) noexcept {
// If we were in inertia and are now idle, momentum has ended
if (m_outer->m_inertia) {
m_outer->FireScrollMomentumEnd({sender.Position().x, sender.Position().y});
}

// If we were interacting but never entered inertia (Interacting -> Idle),
// and the interaction was user-driven (requestId == 0), fire end-drag here.
// Note: if the interactionRequestId was non-zero it was caused by a Try* call
// (programmatic), so we should not fire onScrollEndDrag.
if (m_outer->m_interacting && args.RequestId() == 0) {
m_outer->FireScrollEndDrag({sender.Position().x, sender.Position().y});
}

// Clear state flags
m_outer->m_custom = false;
m_outer->m_inertia = false;
m_outer->m_interacting = false;
}
void InertiaStateEntered(
typename TTypeRedirects::InteractionTracker sender,
typename TTypeRedirects::InteractionTrackerInertiaStateEnteredArgs args) noexcept {
m_outer->m_custom = false;
m_outer->m_inertia = true;
m_outer->m_currentPosition = args.NaturalRestingPosition();
// When the user stops interacting with the object, tracker can go into two paths:
// 1. tracker goes into idle state immediately
// 2. tracker has just started gliding into Inertia state
// Fire ScrollEndDrag
m_outer->FireScrollEndDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});

if (!m_outer->m_interacting && args.RequestId() == 0) {
m_outer->FireScrollBeginDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
}

// If interaction was user-driven (requestId == 0),
// fire ScrollEndDrag here (Interacting -> Inertia caused by user lift).
if (m_outer->m_interacting && args.RequestId() == 0) {
m_outer->FireScrollEndDrag({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
}

// Fire momentum scroll begin when we enter inertia (user or programmatic)
m_outer->FireScrollMomentumBegin({args.NaturalRestingPosition().x, args.NaturalRestingPosition().y});
}
void InteractingStateEntered(
typename TTypeRedirects::InteractionTracker sender,
typename TTypeRedirects::InteractionTrackerInteractingStateEnteredArgs args) noexcept {
// Mark that we're now interacting and remember the requestId (user manipulations => 0)
m_outer->m_interacting = true;

// Fire when the user starts dragging the object
m_outer->FireScrollBeginDrag({sender.Position().x, sender.Position().y});
}
Expand All @@ -738,6 +762,10 @@ struct CompScrollerVisual : winrt::implements<
void ValuesChanged(
typename TTypeRedirects::InteractionTracker sender,
typename TTypeRedirects::InteractionTrackerValuesChangedArgs args) noexcept {
if (!m_outer->m_interacting && args.RequestId() == 0) {
m_outer->FireScrollBeginDrag({args.Position().x, args.Position().y});
}
m_outer->m_interacting = true;
m_outer->m_currentPosition = args.Position();
m_outer->FireScrollPositionChanged({args.Position().x, args.Position().y});
}
Expand Down Expand Up @@ -873,11 +901,9 @@ struct CompScrollerVisual : winrt::implements<
void SetSnapPoints(
bool snapToStart,
bool snapToEnd,
winrt::Windows::Foundation::Collections::IVectorView<float> const &offsets,
SnapAlignment snapToAlignment) noexcept {
winrt::Windows::Foundation::Collections::IVectorView<float> const &offsets) noexcept {
m_snapToStart = snapToStart;
m_snapToEnd = snapToEnd;
m_snapToAlignment = snapToAlignment;
m_snapToOffsets.clear();
if (offsets) {
for (auto const &offset : offsets) {
Expand Down Expand Up @@ -985,6 +1011,20 @@ struct CompScrollerVisual : winrt::implements<
return m_scrollEndDragEvent.add(handler);
}

winrt::event_token ScrollMomentumBegin(
winrt::Windows::Foundation::EventHandler<
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
&handler) noexcept {
return m_scrollMomentumBeginEvent.add(handler);
}

winrt::event_token ScrollMomentumEnd(
winrt::Windows::Foundation::EventHandler<
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs> const
&handler) noexcept {
return m_scrollMomentumEndEvent.add(handler);
}

void ScrollPositionChanged(winrt::event_token const &token) noexcept {
m_scrollPositionChangedEvent.remove(token);
}
Expand All @@ -997,6 +1037,14 @@ struct CompScrollerVisual : winrt::implements<
m_scrollEndDragEvent.remove(token);
}

void ScrollMomentumBegin(winrt::event_token const &token) noexcept {
m_scrollMomentumBeginEvent.remove(token);
}

void ScrollMomentumEnd(winrt::event_token const &token) noexcept {
m_scrollMomentumEndEvent.remove(token);
}

void ContentSize(winrt::Windows::Foundation::Numerics::float2 const &size) noexcept {
m_contentSize = size;
m_contentVisual.Size(size);
Expand Down Expand Up @@ -1075,6 +1123,14 @@ struct CompScrollerVisual : winrt::implements<
m_scrollEndDragEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
}

void FireScrollMomentumBegin(winrt::Windows::Foundation::Numerics::float2 position) noexcept {
m_scrollMomentumBeginEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
}

void FireScrollMomentumEnd(winrt::Windows::Foundation::Numerics::float2 position) noexcept {
m_scrollMomentumEndEvent(*this, winrt::make<CompScrollPositionChangedArgs>(position));
}

void UpdateMaxPosition() noexcept {
m_interactionTracker.MaxPosition(
{std::max<float>(m_contentSize.x - m_visualSize.x, 0),
Expand Down Expand Up @@ -1104,22 +1160,6 @@ 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());

Expand Down Expand Up @@ -1247,9 +1287,9 @@ struct CompScrollerVisual : winrt::implements<
bool m_snapToStart{true};
bool m_snapToEnd{true};
std::vector<float> m_snapToOffsets;
SnapAlignment m_snapToAlignment{SnapAlignment::Start};
bool m_inertia{false};
bool m_custom{false};
bool m_interacting{false};
winrt::Windows::Foundation::Numerics::float3 m_targetPosition;
winrt::Windows::Foundation::Numerics::float3 m_currentPosition;
winrt::Windows::Foundation::Numerics::float2 m_contentSize{0};
Expand All @@ -1263,6 +1303,12 @@ struct CompScrollerVisual : winrt::implements<
winrt::event<winrt::Windows::Foundation::EventHandler<
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
m_scrollEndDragEvent;
winrt::event<winrt::Windows::Foundation::EventHandler<
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
m_scrollMomentumBeginEvent;
winrt::event<winrt::Windows::Foundation::EventHandler<
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs>>
m_scrollMomentumEndEvent;
typename TTypeRedirects::SpriteVisual m_visual{nullptr};
typename TTypeRedirects::SpriteVisual m_contentVisual{nullptr};
typename TTypeRedirects::InteractionTracker m_interactionTracker{nullptr};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
namespace winrt::Microsoft::ReactNative::Composition::implementation {

constexpr float c_scrollerLineDelta = 16.0f;
constexpr auto c_maxSnapPoints = 1000;

enum class ScrollbarHitRegion : int {
Unknown = -1,
Expand Down Expand Up @@ -741,15 +740,6 @@ void ScrollViewComponentView::updateBackgroundColor(const facebook::react::Share
}
}

winrt::Windows::Foundation::Collections::IVector<float> ScrollViewComponentView::CreateSnapToOffsets(
const std::vector<float> &offsets) {
auto snapToOffsets = winrt::single_threaded_vector<float>();
for (const auto &offset : offsets) {
snapToOffsets.Append(offset);
}
return snapToOffsets;
}

void ScrollViewComponentView::updateProps(
facebook::react::Props::Shared const &props,
facebook::react::Props::Shared const &oldProps) noexcept {
Expand Down Expand Up @@ -818,13 +808,11 @@ void ScrollViewComponentView::updateProps(

if (oldViewProps.snapToStart != newViewProps.snapToStart || oldViewProps.snapToEnd != newViewProps.snapToEnd ||
oldViewProps.snapToOffsets != newViewProps.snapToOffsets) {
if (oldViewProps.snapToInterval != newViewProps.snapToInterval) {
updateSnapPoints();
} else {
const auto snapToOffsets = CreateSnapToOffsets(newViewProps.snapToOffsets);
m_scrollVisual.SetSnapPoints(
newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView(), SnapAlignment::Center);
const auto snapToOffsets = winrt::single_threaded_vector<float>();
for (const auto &offset : newViewProps.snapToOffsets) {
snapToOffsets.Append(static_cast<float>(offset));
}
m_scrollVisual.SetSnapPoints(newViewProps.snapToStart, newViewProps.snapToEnd, snapToOffsets.GetView());
}
}

Expand Down Expand Up @@ -875,9 +863,6 @@ 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 @@ -1354,6 +1339,32 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
}
});

m_scrollMomentumBeginRevoker = m_scrollVisual.ScrollMomentumBegin(
winrt::auto_revoke,
[this](
winrt::IInspectable const & /*sender*/,
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
auto eventEmitter = GetEventEmitter();
if (eventEmitter) {
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
->onMomentumScrollBegin(scrollMetrics);
}
});

m_scrollMomentumEndRevoker = m_scrollVisual.ScrollMomentumEnd(
winrt::auto_revoke,
[this](
winrt::IInspectable const & /*sender*/,
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
auto eventEmitter = GetEventEmitter();
if (eventEmitter) {
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
std::static_pointer_cast<facebook::react::ScrollViewEventEmitter const>(eventEmitter)
->onMomentumScrollEnd(scrollMetrics);
}
});

return visual;
}

Expand Down Expand Up @@ -1450,50 +1461,4 @@ void ScrollViewComponentView::updateShowsVerticalScrollIndicator(bool value) noe
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<const facebook::react::ScrollViewProps>(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 && 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
? 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;

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
int snapPointCount = 0;

for (float offset = 0; offset <= contentLength && snapPointCount < c_maxSnapPoints; offset += interval) {
snapToOffsets.Append(offset);
snapPointCount++;
}
}
}

m_scrollVisual.SetSnapPoints(viewProps.snapToStart, viewProps.snapToEnd, snapToOffsets.GetView(), snapAlignment);
}
} // namespace winrt::Microsoft::ReactNative::Composition::implementation
Loading
Loading