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