#import "RNGestureHandlerManager.h" #import #import #import #import #import #import #import #import "RNGestureHandler.h" #import "RNGestureHandlerActionType.h" #import "RNGestureHandlerRegistry.h" #import "RNGestureHandlerState.h" #import "RNRootViewGestureRecognizer.h" #ifdef RN_FABRIC_ENABLED #import #import #else #import #endif // RN_FABRIC_ENABLED #import "Handlers/RNFlingHandler.h" #import "Handlers/RNForceTouchHandler.h" #import "Handlers/RNLongPressHandler.h" #import "Handlers/RNManualHandler.h" #import "Handlers/RNNativeViewHandler.h" #import "Handlers/RNPanHandler.h" #import "Handlers/RNPinchHandler.h" #import "Handlers/RNRotationHandler.h" #import "Handlers/RNTapHandler.h" // We use the method below instead of RCTLog because we log out messages after the bridge gets // turned down in some cases. Which normally with RCTLog would cause a crash in DEBUG mode #define RCTLifecycleLog(...) \ RCTDefaultLogFunction( \ RCTLogLevelInfo, RCTLogSourceNative, @(__FILE__), @(__LINE__), [NSString stringWithFormat:__VA_ARGS__]) @interface RNGestureHandlerManager () @end @implementation RNGestureHandlerManager { RNGestureHandlerRegistry *_registry; RCTUIManager *_uiManager; NSHashTable *_rootViewGestureRecognizers; RCTEventDispatcher *_eventDispatcher; id _reanimatedModule; } - (instancetype)initWithUIManager:(RCTUIManager *)uiManager eventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super init])) { _uiManager = uiManager; _eventDispatcher = eventDispatcher; _registry = [RNGestureHandlerRegistry new]; _rootViewGestureRecognizers = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory]; _reanimatedModule = nil; } return self; } - (void)createGestureHandler:(NSString *)handlerName tag:(NSNumber *)handlerTag config:(NSDictionary *)config { static NSDictionary *map; static dispatch_once_t mapToken; dispatch_once(&mapToken, ^{ map = @{ @"PanGestureHandler" : [RNPanGestureHandler class], @"TapGestureHandler" : [RNTapGestureHandler class], @"FlingGestureHandler" : [RNFlingGestureHandler class], @"LongPressGestureHandler" : [RNLongPressGestureHandler class], @"NativeViewGestureHandler" : [RNNativeViewGestureHandler class], @"PinchGestureHandler" : [RNPinchGestureHandler class], @"RotationGestureHandler" : [RNRotationGestureHandler class], @"ForceTouchGestureHandler" : [RNForceTouchHandler class], @"ManualGestureHandler" : [RNManualGestureHandler class], }; }); Class nodeClass = map[handlerName]; if (!nodeClass) { RCTLogError(@"Gesture handler type %@ is not supported", handlerName); return; } RNGestureHandler *gestureHandler = [[nodeClass alloc] initWithTag:handlerTag]; [gestureHandler configure:config]; [_registry registerGestureHandler:gestureHandler]; __weak id emitter = self; gestureHandler.emitter = emitter; } - (void)attachGestureHandler:(nonnull NSNumber *)handlerTag toViewWithTag:(nonnull NSNumber *)viewTag withActionType:(RNGestureHandlerActionType)actionType { UIView *view = [_uiManager viewForReactTag:viewTag]; #ifdef RN_FABRIC_ENABLED if (view == nil) { // Happens when the view with given tag has been flattened. // We cannot attach gesture handler to a non-existent view. return; } // I think it should be moved to RNNativeViewHandler, but that would require // additional logic for setting contentView.reactTag, this works for now if ([view isKindOfClass:[RCTViewComponentView class]]) { RCTViewComponentView *componentView = (RCTViewComponentView *)view; if (componentView.contentView != nil) { view = componentView.contentView; } } view.reactTag = viewTag; // necessary for RNReanimated eventHash (e.g. "42onGestureHandlerEvent"), also will be // returned as event.target #endif // RN_FABRIC_ENABLED [_registry attachHandlerWithTag:handlerTag toView:view withActionType:actionType]; // register view if not already there [self registerViewWithGestureRecognizerAttachedIfNeeded:view]; } - (void)updateGestureHandler:(NSNumber *)handlerTag config:(NSDictionary *)config { RNGestureHandler *handler = [_registry handlerWithTag:handlerTag]; [handler configure:config]; } - (void)dropGestureHandler:(NSNumber *)handlerTag { [_registry dropHandlerWithTag:handlerTag]; } - (void)dropAllGestureHandlers { [_registry dropAllHandlers]; } - (void)handleSetJSResponder:(NSNumber *)viewTag blockNativeResponder:(NSNumber *)blockNativeResponder { if ([blockNativeResponder boolValue]) { for (RNRootViewGestureRecognizer *recognizer in _rootViewGestureRecognizers) { [recognizer blockOtherRecognizers]; } } } - (void)handleClearJSResponder { // ignore... } - (id)handlerWithTag:(NSNumber *)handlerTag { return [_registry handlerWithTag:handlerTag]; } #pragma mark Root Views Management - (void)registerViewWithGestureRecognizerAttachedIfNeeded:(UIView *)childView { UIView *parent = childView; while (parent != nil && ![parent respondsToSelector:@selector(touchHandler)]) parent = parent.superview; // Many views can return the same touchHandler so we check if the one we want to register // is not already present in the set. UIView *touchHandlerView = [[parent performSelector:@selector(touchHandler)] view]; if (touchHandlerView == nil) { return; } for (UIGestureRecognizer *recognizer in touchHandlerView.gestureRecognizers) { if ([recognizer isKindOfClass:[RNRootViewGestureRecognizer class]]) { return; } } RCTLifecycleLog(@"[GESTURE HANDLER] Initialize gesture handler for view %@", touchHandlerView); RNRootViewGestureRecognizer *recognizer = [RNRootViewGestureRecognizer new]; recognizer.delegate = self; touchHandlerView.userInteractionEnabled = YES; [touchHandlerView addGestureRecognizer:recognizer]; [_rootViewGestureRecognizers addObject:recognizer]; } - (void)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer didActivateInViewWithTouchHandler:(UIView *)viewWithTouchHandler { // Cancel touches in RN's root view in order to cancel all in-js recognizers // As scroll events are special-cased in RN responder implementation and sending them would // trigger JS responder change, we don't cancel touches if the handler that got activated is // a scroll recognizer. This way root view will keep sending touchMove and touchEnd events // and therefore allow JS responder to properly release the responder at the end of the touch // stream. // NOTE: this is not a proper fix and solving this problem requires upstream fixes to RN. In // particular if we have one PanHandler and ScrollView that can work simultaniously then when // the Pan handler activates it would still tigger cancel events. // Once the upstream fix lands the line below along with this comment can be removed if ([gestureRecognizer.view isKindOfClass:[UIScrollView class]]) return; #ifdef RN_FABRIC_ENABLED RCTSurfaceTouchHandler *touchHandler = [viewWithTouchHandler performSelector:@selector(touchHandler)]; #else RCTTouchHandler *touchHandler = [viewWithTouchHandler performSelector:@selector(touchHandler)]; #endif [touchHandler setEnabled:NO]; [touchHandler setEnabled:YES]; } #pragma mark Events - (void)sendEvent:(RNGestureHandlerStateChange *)event withActionType:(RNGestureHandlerActionType)actionType { switch (actionType) { case RNGestureHandlerActionTypeReanimatedWorklet: [self sendEventForReanimated:event]; break; case RNGestureHandlerActionTypeNativeAnimatedEvent: if ([event.eventName isEqualToString:@"onGestureHandlerEvent"]) { [self sendEventForNativeAnimatedEvent:event]; } else { // Although onGestureEvent prop is an Animated.event with useNativeDriver: true, // onHandlerStateChange prop is still a regular JS function. // Also, Animated.event is only supported with old API. [self sendEventForJSFunctionOldAPI:event]; } break; case RNGestureHandlerActionTypeJSFunctionOldAPI: [self sendEventForJSFunctionOldAPI:event]; break; case RNGestureHandlerActionTypeJSFunctionNewAPI: [self sendEventForJSFunctionNewAPI:event]; break; } } - (void)sendEventForReanimated:(RNGestureHandlerStateChange *)event { // Delivers the event to Reanimated. #ifdef RN_FABRIC_ENABLED // Send event directly to Reanimated if (_reanimatedModule == nil) { _reanimatedModule = [_uiManager.bridge moduleForName:@"ReanimatedModule"]; } [_reanimatedModule eventDispatcherWillDispatchEvent:event]; #else // In the old architecture, Reanimated overwrites RCTEventDispatcher // with REAEventDispatcher and intercepts all direct events. [self sendEventForDirectEvent:event]; #endif // RN_FABRIC_ENABLED } - (void)sendEventForNativeAnimatedEvent:(RNGestureHandlerStateChange *)event { // Delivers the event to NativeAnimatedModule. // Currently, NativeAnimated[Turbo]Module is RCTEventDispatcherObserver so we can // simply send a direct event which is handled by the observer but ignored on JS side. // TODO: send event directly to NativeAnimated[Turbo]Module [self sendEventForDirectEvent:event]; } - (void)sendEventForJSFunctionOldAPI:(RNGestureHandlerStateChange *)event { // Delivers the event to JS (old RNGH API). #ifdef RN_FABRIC_ENABLED [self sendEventForDeviceEvent:event]; #else [self sendEventForDirectEvent:event]; #endif // RN_FABRIC_ENABLED } - (void)sendEventForJSFunctionNewAPI:(RNGestureHandlerStateChange *)event { // Delivers the event to JS (new RNGH API). [self sendEventForDeviceEvent:event]; } - (void)sendEventForDirectEvent:(RNGestureHandlerStateChange *)event { // Delivers the event to JS as a direct event. [_eventDispatcher sendEvent:event]; } - (void)sendEventForDeviceEvent:(RNGestureHandlerStateChange *)event { // Delivers the event to JS as a device event. NSMutableDictionary *body = [[event arguments] objectAtIndex:2]; [_eventDispatcher sendDeviceEventWithName:@"onGestureHandlerStateChange" body:body]; } @end