Skip to content

Commit

Permalink
Fix onMomentumScrollBegin not dispatching from animations (facebook#4…
Browse files Browse the repository at this point in the history
…7468)

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
  • Loading branch information
Abbondanzo authored and facebook-github-bot committed Nov 6, 2024
1 parent 147e6c3 commit 125e541
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<const ScrollViewEventEmitter &>(*_eventEmitter).onMomentumScrollBegin([self _scrollViewMetrics]);
}
}

Expand Down
70 changes: 38 additions & 32 deletions packages/react-native/React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIScrollViewDelegate> * 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) {
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -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<UIScrollViewDelegate> * 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)
Expand Down
12 changes: 11 additions & 1 deletion packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -855,11 +855,21 @@ const OnScrollOptions = () => {
};

const OnMomentumScroll = () => {
const ref = useRef<?React.ElementRef<typeof ScrollView>>(null);
const [scroll, setScroll] = useState('none');
return (
<View>
<RNTesterText>Scroll State: {scroll}</RNTesterText>
<Button
label="scrollTo top (animated)"
onPress={() => ref.current?.scrollTo({x: 0, y: 0, animated: true})}
/>
<Button
label="scrollTo top (not animated)"
onPress={() => ref.current?.scrollTo({x: 0, y: 0, animated: false})}
/>
<ScrollView
ref={ref}
style={[styles.scrollView, {height: 200}]}
onMomentumScrollBegin={() => setScroll('onMomentumScrollBegin')}
onMomentumScrollEnd={() => setScroll('onMomentumScrollEnd')}
Expand Down

0 comments on commit 125e541

Please sign in to comment.