From 125e541485911fdfb40f07e557539312ff4afa5c Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 6 Nov 2024 13:45:57 -0800 Subject: [PATCH] Fix onMomentumScrollBegin not dispatching from animations (#47468) Summary: Across our scroll view implementations on iOS, we fire `onMomentumScrollEnd` whenever the scroll view finishes decelerating, whether it comes from a user's touch or call to `setContentOffset` with animations. But we omit dispatching the `onMomentumScrollBegin` event in the latter cases. This change updates both old and new architecture to dispatch `onMomentumScrollBegin` when a view-command-driven scroll occurs with animation, like `scrollTo` or `scrollToEnd`. Changelog: [iOS][Fixed] - Fixed `onMomentumScrollBegin` event not firing on command-driven scroll events Differential Revision: D65556000 --- .../ScrollView/RCTScrollViewComponentView.mm | 2 + .../React/Views/ScrollView/RCTScrollView.m | 70 ++++++++++--------- .../examples/ScrollView/ScrollViewExample.js | 12 +++- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index bb63afb3c3d7f7..486c8f9a84f00a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -916,6 +916,8 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated // When not animated, the expected workflow in ``scrollViewDidEndScrollingAnimation`` after scrolling is not going // to get triggered. We will need to manually execute here. [self _handleFinishedScrolling:_scrollView]; + } else if (_eventEmitter) { + static_cast(*_eventEmitter).onMomentumScrollBegin([self _scrollViewMetrics]); } } diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.m b/packages/react-native/React/Views/ScrollView/RCTScrollView.m index e83b91de6756fa..714a7a77b6af74 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -572,6 +572,38 @@ - (void)scrollToOffset:(CGPoint)offset [self scrollToOffset:offset animated:YES]; } +- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated +{ + [_scrollView zoomToRect:rect animated:animated]; +} + +- (void)refreshContentInset +{ + [RCTView autoAdjustInsetsForView:self withScrollView:_scrollView updateOffset:YES]; +} + +#pragma mark - ScrollView delegate + +#define RCT_SEND_SCROLL_EVENT(_eventName, _userData) \ + { \ + NSString *eventName = NSStringFromSelector(@selector(_eventName)); \ + [self sendScrollEventWithName:eventName scrollView:_scrollView userData:_userData]; \ + } + +#define RCT_FORWARD_SCROLL_EVENT(call) \ + for (NSObject * scrollViewListener in _scrollListeners) { \ + if ([scrollViewListener respondsToSelector:_cmd]) { \ + [scrollViewListener call]; \ + } \ + } + +#define RCT_SCROLL_EVENT_HANDLER(delegateMethod, eventName) \ + -(void)delegateMethod : (UIScrollView *)scrollView \ + { \ + RCT_SEND_SCROLL_EVENT(eventName, nil); \ + RCT_FORWARD_SCROLL_EVENT(delegateMethod : scrollView); \ + } + - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated { if ([self reactLayoutDirection] == UIUserInterfaceLayoutDirectionRightToLeft) { @@ -600,6 +632,9 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated offset = CGPointMake(x, y); } [_scrollView setContentOffset:offset animated:animated]; + if (animated) { + RCT_SEND_SCROLL_EVENT(onMomentumScrollBegin, nil); + } } } @@ -622,41 +657,12 @@ - (void)scrollToEnd:(BOOL)animated // Ensure at least one scroll event will fire _allowNextScrollNoMatterWhat = YES; [_scrollView setContentOffset:offset animated:animated]; + if (animated) { + RCT_SEND_SCROLL_EVENT(onMomentumScrollBegin, nil); + } } } -- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated -{ - [_scrollView zoomToRect:rect animated:animated]; -} - -- (void)refreshContentInset -{ - [RCTView autoAdjustInsetsForView:self withScrollView:_scrollView updateOffset:YES]; -} - -#pragma mark - ScrollView delegate - -#define RCT_SEND_SCROLL_EVENT(_eventName, _userData) \ - { \ - NSString *eventName = NSStringFromSelector(@selector(_eventName)); \ - [self sendScrollEventWithName:eventName scrollView:_scrollView userData:_userData]; \ - } - -#define RCT_FORWARD_SCROLL_EVENT(call) \ - for (NSObject * scrollViewListener in _scrollListeners) { \ - if ([scrollViewListener respondsToSelector:_cmd]) { \ - [scrollViewListener call]; \ - } \ - } - -#define RCT_SCROLL_EVENT_HANDLER(delegateMethod, eventName) \ - -(void)delegateMethod : (UIScrollView *)scrollView \ - { \ - RCT_SEND_SCROLL_EVENT(eventName, nil); \ - RCT_FORWARD_SCROLL_EVENT(delegateMethod : scrollView); \ - } - RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin) RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll) RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop) diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index ae0b34365c5675..074cd7130e3af4 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -15,7 +15,7 @@ import RNTesterText from '../../components/RNTesterText'; import ScrollViewPressableStickyHeaderExample from './ScrollViewPressableStickyHeaderExample'; import nullthrows from 'nullthrows'; import * as React from 'react'; -import {useCallback, useState} from 'react'; +import {useCallback, useRef, useState} from 'react'; import { Platform, RefreshControl, @@ -855,11 +855,21 @@ const OnScrollOptions = () => { }; const OnMomentumScroll = () => { + const ref = useRef>(null); const [scroll, setScroll] = useState('none'); return ( Scroll State: {scroll} +