From dbf04bec7b47722352afa537a4fdd060a70cac12 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Wed, 17 Jul 2024 03:06:38 -0700 Subject: [PATCH 1/2] Send onScrollEnded Event to native driver (#45382) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45382 This change is the first step to tr and solve the pressability's `onTouchMove` issue with animation driven natively in the New Architecture. The idea is to trigger a special event from native to let JS know that a scroll event has ended (`scrollViewDidEndDragging` or `scrollViewdidEndDecelerating`). When this happens, we need to send an event to JS to let him know that it has to sync the Native Tree with the Shadow Tree. Step 2 is to connect Native with JS ## Changelog: [iOS][Added] - Send onScrollEnded event to NativeTurboAnimatedModule Differential Revision: D59459989 Reviewed By: sammy-SC --- .../RCTNativeAnimatedNodesManager.mm | 9 +++++++++ .../ScrollView/RCTScrollViewComponentView.mm | 14 +++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm index b3669766ea07..c195025f3168 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm @@ -368,6 +368,15 @@ - (void)addAnimatedEventToView:(NSNumber *)viewTag [drivers addObject:driver]; _eventDrivers[key] = drivers; } + + // Handle onScrollEnded special events. + // These are triggered when the user stops dragging or when the + // scroll view stops decelerating after the user swiped + // The goal is to use this event to force a resync of the Shadow Tree + // with the Native tree + if ([eventName isEqualToString:@"onScroll"]) { + [self addAnimatedEventToView:viewTag eventName:@"onScrollEnded" eventMapping:eventMapping]; + } } - (void)removeAnimatedEventFromView:(NSNumber *)viewTag 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 98cf04959323..cc81ad3a45cd 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -26,6 +26,10 @@ using namespace facebook::react; +static NSString *kOnScrollEvent = @"onScroll"; + +static NSString *kOnScrollEndEvent = @"onScrollEnded"; + static const CGFloat kClippingLeeway = 44.0; static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(const ScrollViewProps &props) @@ -56,10 +60,11 @@ static UIScrollViewIndicatorStyle RCTUIScrollViewIndicatorStyleFromProps(const S // This is just a workaround to allow animations based on onScroll event. // This is only used to animate sticky headers in ScrollViews, and only the contentOffset and tag is used. // TODO: T116850910 [Fabric][iOS] Make Fabric not use legacy RCTEventDispatcher for native-driven AnimatedEvents -static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInteger tag) +static void +RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInteger tag, NSString *eventName) { static uint16_t coalescingKey = 0; - RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:@"onScroll" + RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName reactTag:[NSNumber numberWithInt:tag] scrollViewContentOffset:scrollView.contentOffset scrollViewContentInset:scrollView.contentInset @@ -507,7 +512,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView static_cast(*_eventEmitter).onScroll(scrollMetrics); } - RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag); + RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag, kOnScrollEvent); } } @@ -564,6 +569,7 @@ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL // ScrollView will not decelerate and `scrollViewDidEndDecelerating` will not be called. // `_isUserTriggeredScrolling` must be set to NO here. _isUserTriggeredScrolling = NO; + RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag, kOnScrollEndEvent); } } @@ -589,6 +595,8 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView static_cast(*_eventEmitter).onMomentumScrollEnd([self _scrollViewMetrics]); [self _updateStateWithContentOffset]; _isUserTriggeredScrolling = NO; + + RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag, kOnScrollEndEvent); } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView From 621543621a394cc289c2a4a7a6b71c9f55b20e0e Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Wed, 17 Jul 2024 05:43:25 -0700 Subject: [PATCH 2/2] Receive the onUserDrivenAnimationEnded event in JS (#45383) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45383 This is the second step required to fix the onTouchMove event in the new architecture. In this change, we are retrieving the list of nodes that are connected by the animation, and we are sending an event to the nodes so that we can trigger the commit. ## Changelog [iOS][Added] - retrieve the tags of the nodes connected by the animation and send them to JS Reviewed By: sammy-SC Differential Revision: D59524617 --- .../RCTNativeAnimatedNodesManager.h | 2 + .../RCTNativeAnimatedNodesManager.mm | 18 ++++++++ .../RCTNativeAnimatedTurboModule.mm | 41 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index 932c873eb6cf..60798329cd8d 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h @@ -87,6 +87,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)stopListeningToAnimatedNodeValue:(NSNumber *)tag; +- (NSSet *)getTagsOfConnectedNodesFrom:(NSNumber *)tag andEvent:(NSString *)eventName; + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm index c195025f3168..db3e2f8b4e1a 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm @@ -479,6 +479,24 @@ - (void)stepAnimations:(CADisplayLink *)displaylink [self stopAnimationLoopIfNeeded]; } +- (NSSet *)getTagsOfConnectedNodesFrom:(NSNumber *)tag andEvent:(NSString *)eventName +{ + NSMutableSet *tags = [NSMutableSet new]; + NSString *key = [NSString stringWithFormat:@"%@%@", tag, RCTNormalizeAnimatedEventName(eventName)]; + NSArray *eventAnimations = _eventDrivers[key]; + for (RCTEventAnimation *animation in eventAnimations) { + NSNumber *nodeTag = [animation.valueNode nodeTag]; + if (nodeTag) { + [tags addObject:nodeTag]; + } + for (NSNumber *childNodeKey in [animation.valueNode childNodes]) { + [tags addObject:childNodeKey]; + } + } + + return tags; +} + #pragma mark-- Updates - (void)updateAnimations diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm index cdf38d90dcd2..4d16e5009254 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -25,6 +25,11 @@ @implementation RCTNativeAnimatedTurboModule { NSMutableArray *_operations; // Operations called before views have been updated. NSMutableArray *_preOperations; + + NSSet *_userDrivenAnimationEndedEvents; + + // TODO: Remove this when https://github.com/facebook/react-native/pull/45457 lands + BOOL _shouldEmitEvent; } RCT_EXPORT_MODULE(); @@ -39,6 +44,8 @@ - (instancetype)init if (self = [super init]) { _operations = [NSMutableArray new]; _preOperations = [NSMutableArray new]; + _userDrivenAnimationEndedEvents = [NSSet setWithArray:@[ @"onScrollEnded" ]]; + _shouldEmitEvent = NO; } return self; } @@ -364,7 +371,7 @@ - (void)didMountComponentsWithRootTag:(NSInteger)rootTag - (NSArray *)supportedEvents { - return @[ @"onAnimatedValueUpdate" ]; + return @[ @"onAnimatedValueUpdate", @"onUserDrivenAnimationEnded" ]; } - (void)animatedNode:(RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value @@ -372,12 +379,44 @@ - (void)animatedNode:(RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value [self sendEventWithName:@"onAnimatedValueUpdate" body:@{@"tag" : node.nodeTag, @"value" : @(value)}]; } +// TODO: Remove this when https://github.com/facebook/react-native/pull/45457 lands +- (void)startObserving +{ + [super startObserving]; + _shouldEmitEvent = YES; +} + +- (void)stopObserving +{ + [super stopObserving]; + _shouldEmitEvent = NO; +} + +// ---- + +- (void)userDrivenAnimationEnded:(NSArray *)nodes +{ + if (!_shouldEmitEvent) { + return; + } + + [self sendEventWithName:@"onUserDrivenAnimationEnded" body:@{@"tags" : nodes}]; +} + - (void)eventDispatcherWillDispatchEvent:(id)event { // Events can be dispatched from any queue so we have to make sure handleAnimatedEvent // is run from the main queue. RCTExecuteOnMainQueue(^{ [self->_nodesManager handleAnimatedEvent:event]; + + if ([self->_userDrivenAnimationEndedEvents containsObject:event.eventName]) { + NSSet *tags = [self->_nodesManager getTagsOfConnectedNodesFrom:event.viewTag + andEvent:event.eventName]; + if (tags.count > 0) { + [self userDrivenAnimationEnded:[tags allObjects]]; + } + } }); }