diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 4006d01a4b86dd..05659b8bb8ab2b 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -22,13 +22,13 @@ @interface RCTScrollViewComponentView () -@property (nonatomic, assign) CGFloat scrollEventThrottle; - @end @implementation RCTScrollViewComponentView { ScrollViewShadowNode::ConcreteState::Shared _state; CGSize _contentSize; + NSTimeInterval _lastScrollEventDispatchTime; + NSTimeInterval _scrollEventThrottle; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -59,6 +59,8 @@ - (instancetype)initWithFrame:(CGRect)frame }]; [_scrollViewDelegateSplitter addDelegate:self]; + + _scrollEventThrottle = INFINITY; } return self; @@ -107,7 +109,17 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & MAP_SCROLL_VIEW_PROP(scrollsToTop); MAP_SCROLL_VIEW_PROP(showsHorizontalScrollIndicator); MAP_SCROLL_VIEW_PROP(showsVerticalScrollIndicator); - MAP_VIEW_PROP(scrollEventThrottle); + + if (oldScrollViewProps.scrollEventThrottle != newScrollViewProps.scrollEventThrottle) { + // Zero means "send value only once per significant logical event". + // Prop value is in milliseconds. + // iOS implementation uses `NSTimeInterval` (in seconds). + // 16 ms is the minimum allowed value. + _scrollEventThrottle = newScrollViewProps.scrollEventThrottle <= 0 + ? INFINITY + : std::max(newScrollViewProps.scrollEventThrottle / 1000.0, 1.0 / 60.0); + } + MAP_SCROLL_VIEW_PROP(zoomScale); if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) { @@ -184,87 +196,108 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView return; } - std::static_pointer_cast(_eventEmitter)->onScroll([self _scrollViewMetrics]); + NSTimeInterval now = CACurrentMediaTime(); + if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { + _lastScrollEventDispatchTime = now; + std::static_pointer_cast(_eventEmitter)->onScroll([self _scrollViewMetrics]); + } } - (void)scrollViewDidZoom:(UIScrollView *)scrollView { - if (!_eventEmitter) { - return; - } - - std::static_pointer_cast(_eventEmitter)->onScroll([self _scrollViewMetrics]); + [self scrollViewDidScroll:scrollView]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onScrollBeginDrag([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onScrollBeginDrag([self _scrollViewMetrics]); } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onScrollEndDrag([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onScrollEndDrag([self _scrollViewMetrics]); } - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter) + std::static_pointer_cast(_eventEmitter) ->onMomentumScrollBegin([self _scrollViewMetrics]); } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onMomentumScrollEnd([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onMomentumScrollEnd([self _scrollViewMetrics]); [self _updateStateWithContentOffset]; } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onMomentumScrollEnd([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onMomentumScrollEnd([self _scrollViewMetrics]); [self _updateStateWithContentOffset]; } - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onScrollBeginDrag([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onScrollBeginDrag([self _scrollViewMetrics]); } - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale { + [self _forceDispatchNextScrollEvent]; + if (!_eventEmitter) { return; } - std::static_pointer_cast(_eventEmitter)->onScrollEndDrag([self _scrollViewMetrics]); + std::static_pointer_cast(_eventEmitter)->onScrollEndDrag([self _scrollViewMetrics]); [self _updateStateWithContentOffset]; } +#pragma mark - UIScrollViewDelegate + +- (void)_forceDispatchNextScrollEvent +{ + _lastScrollEventDispatchTime = 0; +} + @end @implementation RCTScrollViewComponentView (ScrollableProtocol) @@ -276,11 +309,13 @@ - (CGSize)contentSize - (void)scrollToOffset:(CGPoint)offset { + [self _forceDispatchNextScrollEvent]; [self scrollToOffset:offset animated:YES]; } - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated { + [self _forceDispatchNextScrollEvent]; [self.scrollView setContentOffset:offset animated:animated]; } diff --git a/ReactCommon/fabric/components/scrollview/ScrollViewProps.h b/ReactCommon/fabric/components/scrollview/ScrollViewProps.h index 4dc57b79db0bee..1f787f91977b18 100644 --- a/ReactCommon/fabric/components/scrollview/ScrollViewProps.h +++ b/ReactCommon/fabric/components/scrollview/ScrollViewProps.h @@ -40,7 +40,7 @@ class ScrollViewProps final : public ViewProps { const bool scrollsToTop{true}; const bool showsHorizontalScrollIndicator{true}; const bool showsVerticalScrollIndicator{true}; - const Float scrollEventThrottle{}; + const int scrollEventThrottle{}; const Float zoomScale{1.0}; const EdgeInsets contentInset{}; const EdgeInsets scrollIndicatorInsets{};