diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index 932c873eb6c..60798329cd8 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 c195025f316..db3e2f8b4e1 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm @@ -479,6 +479,24 @@ static NSString *RCTNormalizeAnimatedEventName(NSString *eventName) [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 cdf38d90dcd..4d16e500925 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -25,6 +25,11 @@ typedef void (^AnimatedOperation)(RCTNativeAnimatedNodesManager *nodesManager); 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 @@ RCT_EXPORT_MODULE(); if (self = [super init]) { _operations = [NSMutableArray new]; _preOperations = [NSMutableArray new]; + _userDrivenAnimationEndedEvents = [NSSet setWithArray:@[ @"onScrollEnded" ]]; + _shouldEmitEvent = NO; } return self; } @@ -364,7 +371,7 @@ RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArg - (NSArray *)supportedEvents { - return @[ @"onAnimatedValueUpdate" ]; + return @[ @"onAnimatedValueUpdate", @"onUserDrivenAnimationEnded" ]; } - (void)animatedNode:(RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value @@ -372,12 +379,44 @@ RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArg [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]]; + } + } }); }