#import "RNGestureHandler.h" #import "Handlers/RNNativeViewHandler.h" #import #import @interface UIGestureRecognizer (GestureHandler) @property (nonatomic, readonly) RNGestureHandler *gestureHandler; @end @implementation UIGestureRecognizer (GestureHandler) - (RNGestureHandler *)gestureHandler { id delegate = self.delegate; if ([delegate isKindOfClass:[RNGestureHandler class]]) { return (RNGestureHandler *)delegate; } return nil; } @end typedef struct RNGHHitSlop { CGFloat top, left, bottom, right, width, height; } RNGHHitSlop; static RNGHHitSlop RNGHHitSlopEmpty = { NAN, NAN, NAN, NAN, NAN, NAN }; #define RNGH_HIT_SLOP_GET(key) (prop[key] == nil ? NAN : [prop[key] doubleValue]) #define RNGH_HIT_SLOP_IS_SET(hitSlop) (!isnan(hitSlop.left) || !isnan(hitSlop.right) || \ !isnan(hitSlop.top) || !isnan(hitSlop.bottom)) #define RNGH_HIT_SLOP_INSET(key) (isnan(hitSlop.key) ? 0. : hitSlop.key) CGRect RNGHHitSlopInsetRect(CGRect rect, RNGHHitSlop hitSlop) { rect.origin.x -= RNGH_HIT_SLOP_INSET(left); rect.origin.y -= RNGH_HIT_SLOP_INSET(top); if (!isnan(hitSlop.width)) { if (!isnan(hitSlop.right)) { rect.origin.x = rect.size.width - hitSlop.width + RNGH_HIT_SLOP_INSET(right); } rect.size.width = hitSlop.width; } else { rect.size.width += (RNGH_HIT_SLOP_INSET(left) + RNGH_HIT_SLOP_INSET(right)); } if (!isnan(hitSlop.height)) { if (!isnan(hitSlop.bottom)) { rect.origin.y = rect.size.height - hitSlop.height + RNGH_HIT_SLOP_INSET(bottom); } rect.size.height = hitSlop.height; } else { rect.size.height += (RNGH_HIT_SLOP_INSET(top) + RNGH_HIT_SLOP_INSET(bottom)); } return rect; } static NSHashTable *allGestureHandlers; @implementation RNGestureHandler { NSArray *_handlersToWaitFor; NSArray *_simultaneousHandlers; RNGHHitSlop _hitSlop; uint16_t _eventCoalescingKey; } - (instancetype)initWithTag:(NSNumber *)tag { if ((self = [super init])) { _tag = tag; _lastState = RNGestureHandlerStateUndetermined; _hitSlop = RNGHHitSlopEmpty; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ allGestureHandlers = [NSHashTable weakObjectsHashTable]; }); [allGestureHandlers addObject:self]; } return self; } - (void)configure:(NSDictionary *)config { _handlersToWaitFor = [RCTConvert NSNumberArray:config[@"waitFor"]]; _simultaneousHandlers = [RCTConvert NSNumberArray:config[@"simultaneousHandlers"]]; id prop = config[@"enabled"]; if (prop != nil) { self.enabled = [RCTConvert BOOL:prop]; } else { self.enabled = YES; } prop = config[@"shouldCancelWhenOutside"]; if (prop != nil) { _shouldCancelWhenOutside = [RCTConvert BOOL:prop]; } else { _shouldCancelWhenOutside = NO; } prop = config[@"hitSlop"]; if ([prop isKindOfClass:[NSNumber class]]) { _hitSlop.left = _hitSlop.right = _hitSlop.top = _hitSlop.bottom = [prop doubleValue]; } else if (prop != nil) { _hitSlop.left = _hitSlop.right = RNGH_HIT_SLOP_GET(@"horizontal"); _hitSlop.top = _hitSlop.bottom = RNGH_HIT_SLOP_GET(@"vertical"); _hitSlop.left = RNGH_HIT_SLOP_GET(@"left"); _hitSlop.right = RNGH_HIT_SLOP_GET(@"right"); _hitSlop.top = RNGH_HIT_SLOP_GET(@"top"); _hitSlop.bottom = RNGH_HIT_SLOP_GET(@"bottom"); _hitSlop.width = RNGH_HIT_SLOP_GET(@"width"); _hitSlop.height = RNGH_HIT_SLOP_GET(@"height"); if (isnan(_hitSlop.left) && isnan(_hitSlop.right) && !isnan(_hitSlop.width)) { RCTLogError(@"When width is set one of left or right pads need to be defined"); } if (!isnan(_hitSlop.width) && !isnan(_hitSlop.left) && !isnan(_hitSlop.right)) { RCTLogError(@"Cannot have all of left, right and width defined"); } if (isnan(_hitSlop.top) && isnan(_hitSlop.bottom) && !isnan(_hitSlop.height)) { RCTLogError(@"When height is set one of top or bottom pads need to be defined"); } if (!isnan(_hitSlop.height) && !isnan(_hitSlop.top) && !isnan(_hitSlop.bottom)) { RCTLogError(@"Cannot have all of top, bottom and height defined"); } } } - (void)setEnabled:(BOOL)enabled { _enabled = enabled; self.recognizer.enabled = enabled; } - (void)bindToView:(UIView *)view { view.userInteractionEnabled = YES; self.recognizer.delegate = self; [view addGestureRecognizer:self.recognizer]; } - (void)unbindFromView { [self.recognizer.view removeGestureRecognizer:self.recognizer]; self.recognizer.delegate = nil; } - (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer { return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] withAbsolutePosition:[recognizer locationInView:recognizer.view.window] withNumberOfTouches:recognizer.numberOfTouches]; } - (void)handleGesture:(UIGestureRecognizer *)recognizer { RNGestureHandlerEventExtraData *eventData = [self eventExtraData:recognizer]; [self sendEventsInState:self.state forViewWithTag:recognizer.view.reactTag withExtraData:eventData]; } - (void)sendEventsInState:(RNGestureHandlerState)state forViewWithTag:(nonnull NSNumber *)reactTag withExtraData:(RNGestureHandlerEventExtraData *)extraData { if (state != _lastState) { if (state == RNGestureHandlerStateActive) { // Generate a unique coalescing-key each time the gesture-handler becomes active. All events will have // the same coalescing-key allowing RCTEventDispatcher to coalesce RNGestureHandlerEvents when events are // generated faster than they can be treated by JS thread static uint16_t nextEventCoalescingKey = 0; self->_eventCoalescingKey = nextEventCoalescingKey++; } else if (state == RNGestureHandlerStateEnd && _lastState != RNGestureHandlerStateActive) { [self.emitter sendStateChangeEvent:[[RNGestureHandlerStateChange alloc] initWithReactTag:reactTag handlerTag:_tag state:RNGestureHandlerStateActive prevState:_lastState extraData:extraData]]; _lastState = RNGestureHandlerStateActive; } id stateEvent = [[RNGestureHandlerStateChange alloc] initWithReactTag:reactTag handlerTag:_tag state:state prevState:_lastState extraData:extraData]; [self.emitter sendStateChangeEvent:stateEvent]; _lastState = state; } if (state == RNGestureHandlerStateActive) { id touchEvent = [[RNGestureHandlerEvent alloc] initWithReactTag:reactTag handlerTag:_tag state:state extraData:extraData coalescingKey:self->_eventCoalescingKey]; [self.emitter sendTouchEvent:touchEvent]; } } - (RNGestureHandlerState)state { switch (_recognizer.state) { case UIGestureRecognizerStateBegan: case UIGestureRecognizerStatePossible: return RNGestureHandlerStateBegan; case UIGestureRecognizerStateEnded: return RNGestureHandlerStateEnd; case UIGestureRecognizerStateFailed: return RNGestureHandlerStateFailed; case UIGestureRecognizerStateCancelled: return RNGestureHandlerStateCancelled; case UIGestureRecognizerStateChanged: return RNGestureHandlerStateActive; } return RNGestureHandlerStateUndetermined; } #pragma mark UIGestureRecognizerDelegate + (RNGestureHandler *)findGestureHandlerByRecognizer:(UIGestureRecognizer *)recognizer { RNGestureHandler *handler = recognizer.gestureHandler; if (handler != nil) { return handler; } // We may try to extract "DummyGestureHandler" in case when "otherGestureRecognizer" belongs to // a native view being wrapped with "NativeViewGestureHandler" UIView *reactView = recognizer.view; while (reactView != nil && reactView.reactTag == nil) { reactView = reactView.superview; } for (UIGestureRecognizer *recognizer in reactView.gestureRecognizers) { if ([recognizer isKindOfClass:[RNDummyGestureRecognizer class]]) { return recognizer.gestureHandler; } } return nil; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; if ([handler isKindOfClass:[RNNativeViewGestureHandler class]]) { for (NSNumber *handlerTag in handler->_handlersToWaitFor) { if ([_tag isEqual:handlerTag]) { return YES; } } } return NO; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if ([_handlersToWaitFor count]) { RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; if (handler != nil) { for (NSNumber *handlerTag in _handlersToWaitFor) { if ([handler.tag isEqual:handlerTag]) { return YES; } } } } return NO; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if (_recognizer.state == UIGestureRecognizerStateBegan && _recognizer.state == UIGestureRecognizerStatePossible) { return YES; } if ([_simultaneousHandlers count]) { RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; if (handler != nil) { for (NSNumber *handlerTag in _simultaneousHandlers) { if ([handler.tag isEqual:handlerTag]) { return YES; } } } } return NO; } - (void)reset { _lastState = RNGestureHandlerStateUndetermined; } - (BOOL)containsPointInView { CGPoint pt = [_recognizer locationInView:_recognizer.view]; CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); return CGRectContainsPoint(hitFrame, pt); } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([_handlersToWaitFor count]) { for (RNGestureHandler *handler in [allGestureHandlers allObjects]) { if (handler != nil && (handler.state == RNGestureHandlerStateActive || handler->_recognizer.state == UIGestureRecognizerStateBegan)) { for (NSNumber *handlerTag in _handlersToWaitFor) { if ([handler.tag isEqual:handlerTag]) { return NO; } } } } } [self reset]; return YES; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { // If hitSlop is set we use it to determine if a given gesture recognizer should start processing // touch stream. This only works for negative values of hitSlop as this method won't be triggered // unless touch startes in the bounds of the attached view. To acheve similar effect with positive // values of hitSlop one should set hitSlop for the underlying view. This limitation is due to the // fact that hitTest method is only available at the level of UIView if (RNGH_HIT_SLOP_IS_SET(_hitSlop)) { CGPoint location = [touch locationInView:gestureRecognizer.view]; CGRect hitFrame = RNGHHitSlopInsetRect(gestureRecognizer.view.bounds, _hitSlop); return CGRectContainsPoint(hitFrame, location); } return YES; } @end