1#import "RNGestureHandler.h" 2#import "RNManualActivationRecognizer.h" 3 4#import "Handlers/RNNativeViewHandler.h" 5 6#import <UIKit/UIGestureRecognizerSubclass.h> 7 8#import <React/UIView+React.h> 9 10@interface UIGestureRecognizer (GestureHandler) 11@property (nonatomic, readonly) RNGestureHandler *gestureHandler; 12@end 13 14@implementation UIGestureRecognizer (GestureHandler) 15 16- (RNGestureHandler *)gestureHandler 17{ 18 id delegate = self.delegate; 19 if ([delegate isKindOfClass:[RNGestureHandler class]]) { 20 return (RNGestureHandler *)delegate; 21 } 22 return nil; 23} 24 25@end 26 27typedef struct RNGHHitSlop { 28 CGFloat top, left, bottom, right, width, height; 29} RNGHHitSlop; 30 31static RNGHHitSlop RNGHHitSlopEmpty = {NAN, NAN, NAN, NAN, NAN, NAN}; 32 33#define RNGH_HIT_SLOP_GET(key) (prop[key] == nil ? NAN : [prop[key] doubleValue]) 34#define RNGH_HIT_SLOP_IS_SET(hitSlop) \ 35 (!isnan(hitSlop.left) || !isnan(hitSlop.right) || !isnan(hitSlop.top) || !isnan(hitSlop.bottom)) 36#define RNGH_HIT_SLOP_INSET(key) (isnan(hitSlop.key) ? 0. : hitSlop.key) 37 38CGRect RNGHHitSlopInsetRect(CGRect rect, RNGHHitSlop hitSlop) 39{ 40 rect.origin.x -= RNGH_HIT_SLOP_INSET(left); 41 rect.origin.y -= RNGH_HIT_SLOP_INSET(top); 42 43 if (!isnan(hitSlop.width)) { 44 if (!isnan(hitSlop.right)) { 45 rect.origin.x = rect.size.width - hitSlop.width + RNGH_HIT_SLOP_INSET(right); 46 } 47 rect.size.width = hitSlop.width; 48 } else { 49 rect.size.width += (RNGH_HIT_SLOP_INSET(left) + RNGH_HIT_SLOP_INSET(right)); 50 } 51 if (!isnan(hitSlop.height)) { 52 if (!isnan(hitSlop.bottom)) { 53 rect.origin.y = rect.size.height - hitSlop.height + RNGH_HIT_SLOP_INSET(bottom); 54 } 55 rect.size.height = hitSlop.height; 56 } else { 57 rect.size.height += (RNGH_HIT_SLOP_INSET(top) + RNGH_HIT_SLOP_INSET(bottom)); 58 } 59 return rect; 60} 61 62static NSHashTable<RNGestureHandler *> *allGestureHandlers; 63 64@implementation RNGestureHandler { 65 RNGestureHandlerPointerTracker *_pointerTracker; 66 RNGestureHandlerState _state; 67 RNManualActivationRecognizer *_manualActivationRecognizer; 68 NSArray<NSNumber *> *_handlersToWaitFor; 69 NSArray<NSNumber *> *_simultaneousHandlers; 70 RNGHHitSlop _hitSlop; 71 uint16_t _eventCoalescingKey; 72} 73 74- (instancetype)initWithTag:(NSNumber *)tag 75{ 76 if ((self = [super init])) { 77 _pointerTracker = [[RNGestureHandlerPointerTracker alloc] initWithGestureHandler:self]; 78 _tag = tag; 79 _lastState = RNGestureHandlerStateUndetermined; 80 _hitSlop = RNGHHitSlopEmpty; 81 _state = RNGestureHandlerStateBegan; 82 _manualActivationRecognizer = nil; 83 84 static dispatch_once_t onceToken; 85 dispatch_once(&onceToken, ^{ 86 allGestureHandlers = [NSHashTable weakObjectsHashTable]; 87 }); 88 89 [allGestureHandlers addObject:self]; 90 } 91 return self; 92} 93 94- (void)resetConfig 95{ 96 self.enabled = YES; 97 self.manualActivation = NO; 98 _shouldCancelWhenOutside = NO; 99 _handlersToWaitFor = nil; 100 _simultaneousHandlers = nil; 101 _hitSlop = RNGHHitSlopEmpty; 102 _needsPointerData = NO; 103 104 _recognizer.cancelsTouchesInView = YES; 105} 106 107- (void)configure:(NSDictionary *)config 108{ 109 [self resetConfig]; 110 _handlersToWaitFor = [RCTConvert NSNumberArray:config[@"waitFor"]]; 111 _simultaneousHandlers = [RCTConvert NSNumberArray:config[@"simultaneousHandlers"]]; 112 113 id prop = config[@"enabled"]; 114 if (prop != nil) { 115 self.enabled = [RCTConvert BOOL:prop]; 116 } 117 118 prop = config[@"shouldCancelWhenOutside"]; 119 if (prop != nil) { 120 _shouldCancelWhenOutside = [RCTConvert BOOL:prop]; 121 } 122 123 prop = config[@"cancelsTouchesInView"]; 124 if (prop != nil) { 125 _recognizer.cancelsTouchesInView = [RCTConvert BOOL:prop]; 126 } 127 128 prop = config[@"needsPointerData"]; 129 if (prop != nil) { 130 _needsPointerData = [RCTConvert BOOL:prop]; 131 } 132 133 prop = config[@"manualActivation"]; 134 if (prop != nil) { 135 self.manualActivation = [RCTConvert BOOL:prop]; 136 } 137 138 prop = config[@"hitSlop"]; 139 if ([prop isKindOfClass:[NSNumber class]]) { 140 _hitSlop.left = _hitSlop.right = _hitSlop.top = _hitSlop.bottom = [prop doubleValue]; 141 } else if (prop != nil) { 142 _hitSlop.left = _hitSlop.right = RNGH_HIT_SLOP_GET(@"horizontal"); 143 _hitSlop.top = _hitSlop.bottom = RNGH_HIT_SLOP_GET(@"vertical"); 144 _hitSlop.left = RNGH_HIT_SLOP_GET(@"left"); 145 _hitSlop.right = RNGH_HIT_SLOP_GET(@"right"); 146 _hitSlop.top = RNGH_HIT_SLOP_GET(@"top"); 147 _hitSlop.bottom = RNGH_HIT_SLOP_GET(@"bottom"); 148 _hitSlop.width = RNGH_HIT_SLOP_GET(@"width"); 149 _hitSlop.height = RNGH_HIT_SLOP_GET(@"height"); 150 if (isnan(_hitSlop.left) && isnan(_hitSlop.right) && !isnan(_hitSlop.width)) { 151 RCTLogError(@"When width is set one of left or right pads need to be defined"); 152 } 153 if (!isnan(_hitSlop.width) && !isnan(_hitSlop.left) && !isnan(_hitSlop.right)) { 154 RCTLogError(@"Cannot have all of left, right and width defined"); 155 } 156 if (isnan(_hitSlop.top) && isnan(_hitSlop.bottom) && !isnan(_hitSlop.height)) { 157 RCTLogError(@"When height is set one of top or bottom pads need to be defined"); 158 } 159 if (!isnan(_hitSlop.height) && !isnan(_hitSlop.top) && !isnan(_hitSlop.bottom)) { 160 RCTLogError(@"Cannot have all of top, bottom and height defined"); 161 } 162 } 163} 164 165- (void)setEnabled:(BOOL)enabled 166{ 167 _enabled = enabled; 168 self.recognizer.enabled = enabled; 169} 170 171- (void)bindToView:(UIView *)view 172{ 173 view.userInteractionEnabled = YES; 174 self.recognizer.delegate = self; 175 [view addGestureRecognizer:self.recognizer]; 176 177 [self bindManualActivationToView:view]; 178} 179 180- (void)unbindFromView 181{ 182 [self.recognizer.view removeGestureRecognizer:self.recognizer]; 183 self.recognizer.delegate = nil; 184 185 [self unbindManualActivation]; 186} 187 188- (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer 189{ 190 return [RNGestureHandlerEventExtraData forPosition:[recognizer locationInView:recognizer.view] 191 withAbsolutePosition:[recognizer locationInView:recognizer.view.window] 192 withNumberOfTouches:recognizer.numberOfTouches]; 193} 194 195- (void)handleGesture:(UIGestureRecognizer *)recognizer 196{ 197 // it may happen that the gesture recognizer is reset after it's been unbound from the view, 198 // it that recognizer tried to send event, the app would crash because the target of the event 199 // would be nil. 200 if (recognizer.view.reactTag == nil) { 201 return; 202 } 203 204 _state = [self recognizerState]; 205 [self handleGesture:recognizer inState:_state]; 206} 207 208- (void)handleGesture:(UIGestureRecognizer *)recognizer inState:(RNGestureHandlerState)state 209{ 210 _state = state; 211 RNGestureHandlerEventExtraData *eventData = [self eventExtraData:recognizer]; 212 [self sendEventsInState:self.state forViewWithTag:recognizer.view.reactTag withExtraData:eventData]; 213} 214 215- (void)sendEventsInState:(RNGestureHandlerState)state 216 forViewWithTag:(nonnull NSNumber *)reactTag 217 withExtraData:(RNGestureHandlerEventExtraData *)extraData 218{ 219 if (state != _lastState) { 220 // don't send change events from END to FAILED or CANCELLED, this may happen when gesture is ended in `onTouchesUp` 221 // callback 222 if (_lastState == RNGestureHandlerStateEnd && 223 (state == RNGestureHandlerStateFailed || state == RNGestureHandlerStateCancelled)) { 224 return; 225 } 226 227 if (state == RNGestureHandlerStateActive) { 228 // Generate a unique coalescing-key each time the gesture-handler becomes active. All events will have 229 // the same coalescing-key allowing RCTEventDispatcher to coalesce RNGestureHandlerEvents when events are 230 // generated faster than they can be treated by JS thread 231 static uint16_t nextEventCoalescingKey = 0; 232 self->_eventCoalescingKey = nextEventCoalescingKey++; 233 234 } else if (state == RNGestureHandlerStateEnd && _lastState != RNGestureHandlerStateActive && !_manualActivation) { 235 id event = [[RNGestureHandlerStateChange alloc] initWithReactTag:reactTag 236 handlerTag:_tag 237 state:RNGestureHandlerStateActive 238 prevState:_lastState 239 extraData:extraData]; 240 [self sendEvent:event]; 241 _lastState = RNGestureHandlerStateActive; 242 } 243 id stateEvent = [[RNGestureHandlerStateChange alloc] initWithReactTag:reactTag 244 handlerTag:_tag 245 state:state 246 prevState:_lastState 247 extraData:extraData]; 248 [self sendEvent:stateEvent]; 249 _lastState = state; 250 } 251 252 if (state == RNGestureHandlerStateActive) { 253 id touchEvent = [[RNGestureHandlerEvent alloc] initWithReactTag:reactTag 254 handlerTag:_tag 255 state:state 256 extraData:extraData 257 coalescingKey:self->_eventCoalescingKey]; 258 [self sendEvent:touchEvent]; 259 } 260} 261 262- (void)sendEvent:(RNGestureHandlerStateChange *)event 263{ 264 [self.emitter sendEvent:event withActionType:self.actionType]; 265} 266 267- (void)sendTouchEventInState:(RNGestureHandlerState)state forViewWithTag:(NSNumber *)reactTag 268{ 269 id extraData = [RNGestureHandlerEventExtraData forEventType:_pointerTracker.eventType 270 withChangedPointers:_pointerTracker.changedPointersData 271 withAllPointers:_pointerTracker.allPointersData 272 withNumberOfTouches:_pointerTracker.trackedPointersCount]; 273 id event = [[RNGestureHandlerEvent alloc] initWithReactTag:reactTag 274 handlerTag:_tag 275 state:state 276 extraData:extraData 277 coalescingKey:[_tag intValue]]; 278 279 [self.emitter sendEvent:event withActionType:self.actionType]; 280} 281 282- (RNGestureHandlerState)recognizerState 283{ 284 switch (_recognizer.state) { 285 case UIGestureRecognizerStateBegan: 286 case UIGestureRecognizerStatePossible: 287 return RNGestureHandlerStateBegan; 288 case UIGestureRecognizerStateEnded: 289 return RNGestureHandlerStateEnd; 290 case UIGestureRecognizerStateFailed: 291 return RNGestureHandlerStateFailed; 292 case UIGestureRecognizerStateCancelled: 293 return RNGestureHandlerStateCancelled; 294 case UIGestureRecognizerStateChanged: 295 return RNGestureHandlerStateActive; 296 } 297 return RNGestureHandlerStateUndetermined; 298} 299 300- (RNGestureHandlerState)state 301{ 302 // instead of mapping state of the recognizer directly, use value mapped when handleGesture was 303 // called, making it correct while awaiting for another handler failure 304 return _state; 305} 306 307#pragma mark Manual activation 308 309- (void)stopActivationBlocker 310{ 311 if (_manualActivationRecognizer != nil) { 312 [_manualActivationRecognizer fail]; 313 } 314} 315 316- (void)setManualActivation:(BOOL)manualActivation 317{ 318 _manualActivation = manualActivation; 319 320 if (manualActivation) { 321 _manualActivationRecognizer = [[RNManualActivationRecognizer alloc] initWithGestureHandler:self]; 322 323 if (_recognizer.view != nil) { 324 [_recognizer.view addGestureRecognizer:_manualActivationRecognizer]; 325 } 326 } else if (_manualActivationRecognizer != nil) { 327 [_manualActivationRecognizer.view removeGestureRecognizer:_manualActivationRecognizer]; 328 _manualActivationRecognizer = nil; 329 } 330} 331 332- (void)bindManualActivationToView:(UIView *)view 333{ 334 if (_manualActivationRecognizer != nil) { 335 [view addGestureRecognizer:_manualActivationRecognizer]; 336 } 337} 338 339- (void)unbindManualActivation 340{ 341 if (_manualActivationRecognizer != nil) { 342 [_manualActivationRecognizer.view removeGestureRecognizer:_manualActivationRecognizer]; 343 } 344} 345 346#pragma mark UIGestureRecognizerDelegate 347 348+ (RNGestureHandler *)findGestureHandlerByRecognizer:(UIGestureRecognizer *)recognizer 349{ 350 RNGestureHandler *handler = recognizer.gestureHandler; 351 if (handler != nil) { 352 return handler; 353 } 354 355 // We may try to extract "DummyGestureHandler" in case when "otherGestureRecognizer" belongs to 356 // a native view being wrapped with "NativeViewGestureHandler" 357 UIView *reactView = recognizer.view; 358 while (reactView != nil && reactView.reactTag == nil) { 359 reactView = reactView.superview; 360 } 361 362 for (UIGestureRecognizer *recognizer in reactView.gestureRecognizers) { 363 if ([recognizer isKindOfClass:[RNDummyGestureRecognizer class]]) { 364 return recognizer.gestureHandler; 365 } 366 } 367 368 return nil; 369} 370 371- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 372 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 373{ 374 RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; 375 if ([handler isKindOfClass:[RNNativeViewGestureHandler class]]) { 376 for (NSNumber *handlerTag in handler->_handlersToWaitFor) { 377 if ([_tag isEqual:handlerTag]) { 378 return YES; 379 } 380 } 381 } 382 383 return NO; 384} 385 386- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 387 shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 388{ 389 if ([_handlersToWaitFor count]) { 390 RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; 391 if (handler != nil) { 392 for (NSNumber *handlerTag in _handlersToWaitFor) { 393 if ([handler.tag isEqual:handlerTag]) { 394 return YES; 395 } 396 } 397 } 398 } 399 return NO; 400} 401 402- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 403 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 404{ 405 if (_recognizer.state == UIGestureRecognizerStateBegan && _recognizer.state == UIGestureRecognizerStatePossible) { 406 return YES; 407 } 408 409 RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer]; 410 if (handler != nil) { 411 if ([_simultaneousHandlers count]) { 412 for (NSNumber *handlerTag in _simultaneousHandlers) { 413 if ([handler.tag isEqual:handlerTag]) { 414 return YES; 415 } 416 } 417 } else if (handler->_simultaneousHandlers) { 418 for (NSNumber *handlerTag in handler->_simultaneousHandlers) { 419 if ([self.tag isEqual:handlerTag]) { 420 return YES; 421 } 422 } 423 } 424 } 425 return NO; 426} 427 428- (void)reset 429{ 430 // do not reset states while gesture is tracking pointers, as gestureRecognizerShouldBegin 431 // might be called after some pointers are down, and after state manipulation by the user. 432 // Pointer tracker calls this method when it resets, and in that case it no longer tracks 433 // any pointers, thus entering this if 434 if (!_needsPointerData || _pointerTracker.trackedPointersCount == 0) { 435 _lastState = RNGestureHandlerStateUndetermined; 436 _state = RNGestureHandlerStateBegan; 437 } 438} 439 440- (BOOL)containsPointInView 441{ 442 CGPoint pt = [_recognizer locationInView:_recognizer.view]; 443 CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); 444 return CGRectContainsPoint(hitFrame, pt); 445} 446 447- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 448{ 449 if ([_handlersToWaitFor count]) { 450 for (RNGestureHandler *handler in [allGestureHandlers allObjects]) { 451 if (handler != nil && 452 (handler.state == RNGestureHandlerStateActive || 453 handler->_recognizer.state == UIGestureRecognizerStateBegan)) { 454 for (NSNumber *handlerTag in _handlersToWaitFor) { 455 if ([handler.tag isEqual:handlerTag]) { 456 return NO; 457 } 458 } 459 } 460 } 461 } 462 463 [self reset]; 464 return YES; 465} 466 467- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 468{ 469 // If hitSlop is set we use it to determine if a given gesture recognizer should start processing 470 // touch stream. This only works for negative values of hitSlop as this method won't be triggered 471 // unless touch startes in the bounds of the attached view. To acheve similar effect with positive 472 // values of hitSlop one should set hitSlop for the underlying view. This limitation is due to the 473 // fact that hitTest method is only available at the level of UIView 474 if (RNGH_HIT_SLOP_IS_SET(_hitSlop)) { 475 CGPoint location = [touch locationInView:gestureRecognizer.view]; 476 CGRect hitFrame = RNGHHitSlopInsetRect(gestureRecognizer.view.bounds, _hitSlop); 477 return CGRectContainsPoint(hitFrame, location); 478 } 479 return YES; 480} 481 482@end 483