1//
2//  RNTapHandler.m
3//  RNGestureHandler
4//
5//  Created by Krzysztof Magiera on 12/10/2017.
6//  Copyright © 2017 Software Mansion. All rights reserved.
7//
8
9#import "RNTapHandler.h"
10
11#import <UIKit/UIGestureRecognizerSubclass.h>
12
13#import <React/RCTConvert.h>
14
15// RNBetterTapGestureRecognizer extends UIGestureRecognizer instead of UITapGestureRecognizer
16// because the latter does not allow for parameters like maxDelay, maxDuration, minPointers,
17// maxDelta to be configured. Using our custom implementation of tap recognizer we are able
18// to support these.
19
20@interface RNBetterTapGestureRecognizer : UIGestureRecognizer
21
22@property (nonatomic) NSUInteger numberOfTaps;
23@property (nonatomic) NSTimeInterval maxDelay;
24@property (nonatomic) NSTimeInterval maxDuration;
25@property (nonatomic) CGFloat maxDistSq;
26@property (nonatomic) CGFloat maxDeltaX;
27@property (nonatomic) CGFloat maxDeltaY;
28@property (nonatomic) NSInteger minPointers;
29
30- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler;
31
32@end
33
34@implementation RNBetterTapGestureRecognizer {
35  __weak RNGestureHandler *_gestureHandler;
36  NSUInteger _tapsSoFar;
37  CGPoint _initPosition;
38  NSInteger _maxNumberOfTouches;
39}
40
41static const NSUInteger defaultNumberOfTaps = 1;
42static const NSInteger defaultMinPointers = 1;
43static const CGFloat defaultMaxDelay = 0.2;
44static const NSTimeInterval defaultMaxDuration = 0.5;
45
46- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler
47{
48  if ((self = [super initWithTarget:gestureHandler action:@selector(handleGesture:)])) {
49    _gestureHandler = gestureHandler;
50    _tapsSoFar = 0;
51    _numberOfTaps = defaultNumberOfTaps;
52    _minPointers = defaultMinPointers;
53    _maxDelay = defaultMaxDelay;
54    _maxDuration = defaultMaxDuration;
55    _maxDeltaX = NAN;
56    _maxDeltaY = NAN;
57    _maxDistSq = NAN;
58  }
59  return self;
60}
61
62- (void)triggerAction
63{
64  [_gestureHandler handleGesture:self];
65}
66
67- (void)cancel
68{
69  self.enabled = NO;
70}
71
72- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
73{
74  [super touchesBegan:touches withEvent:event];
75  [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event];
76
77  if (_tapsSoFar == 0) {
78    // this recognizer sends UNDETERMINED -> BEGAN state change event before gestureRecognizerShouldBegin
79    // is called (it resets the gesture handler), making it send whatever the last known state as oldState
80    // in the event. If we reset it here it correctly sends UNDETERMINED as oldState.
81    [_gestureHandler reset];
82    _initPosition = [self locationInView:self.view.window];
83  }
84  _tapsSoFar++;
85  if (_tapsSoFar) {
86    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cancel) object:nil];
87  }
88  NSInteger numberOfTouches = [touches count];
89  if (numberOfTouches > _maxNumberOfTouches) {
90    _maxNumberOfTouches = numberOfTouches;
91  }
92  if (!isnan(_maxDuration)) {
93    [self performSelector:@selector(cancel) withObject:nil afterDelay:_maxDuration];
94  }
95  self.state = UIGestureRecognizerStatePossible;
96  [self triggerAction];
97}
98
99- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
100{
101  [super touchesMoved:touches withEvent:event];
102  [_gestureHandler.pointerTracker touchesMoved:touches withEvent:event];
103
104  NSInteger numberOfTouches = [touches count];
105  if (numberOfTouches > _maxNumberOfTouches) {
106    _maxNumberOfTouches = numberOfTouches;
107  }
108
109  if (self.state != UIGestureRecognizerStatePossible) {
110    return;
111  }
112
113  if ([self shouldFailUnderCustomCriteria]) {
114    self.state = UIGestureRecognizerStateFailed;
115    [self triggerAction];
116    [self reset];
117    return;
118  }
119
120  self.state = UIGestureRecognizerStatePossible;
121  [self triggerAction];
122}
123
124- (CGPoint)translationInView
125{
126  CGPoint currentPosition = [self locationInView:self.view.window];
127  return CGPointMake(currentPosition.x - _initPosition.x, currentPosition.y - _initPosition.y);
128}
129
130- (BOOL)shouldFailUnderCustomCriteria
131{
132  if (_gestureHandler.shouldCancelWhenOutside) {
133    if (![_gestureHandler containsPointInView]) {
134      return YES;
135    }
136  }
137
138  CGPoint trans = [self translationInView];
139  if (TEST_MAX_IF_NOT_NAN(fabs(trans.x), _maxDeltaX)) {
140    return YES;
141  }
142  if (TEST_MAX_IF_NOT_NAN(fabs(trans.y), _maxDeltaY)) {
143    return YES;
144  }
145  if (TEST_MAX_IF_NOT_NAN(fabs(trans.y * trans.y + trans.x * trans.x), _maxDistSq)) {
146    return YES;
147  }
148  return NO;
149}
150
151- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
152{
153  [super touchesEnded:touches withEvent:event];
154  [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
155
156  if (_numberOfTaps == _tapsSoFar && _maxNumberOfTouches >= _minPointers) {
157    self.state = UIGestureRecognizerStateEnded;
158    [self reset];
159  } else {
160    [self performSelector:@selector(cancel) withObject:nil afterDelay:_maxDelay];
161  }
162}
163
164- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
165{
166  [super touchesCancelled:touches withEvent:event];
167  [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
168
169  self.state = UIGestureRecognizerStateCancelled;
170  [self reset];
171}
172
173- (void)reset
174{
175  if (self.state == UIGestureRecognizerStateFailed) {
176    [self triggerAction];
177  }
178  [_gestureHandler.pointerTracker reset];
179
180  [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cancel) object:nil];
181  _tapsSoFar = 0;
182  _maxNumberOfTouches = 0;
183  self.enabled = YES;
184  [super reset];
185}
186
187@end
188
189@implementation RNTapGestureHandler {
190  RNGestureHandlerEventExtraData *_lastData;
191}
192
193- (instancetype)initWithTag:(NSNumber *)tag
194{
195  if ((self = [super initWithTag:tag])) {
196    _recognizer = [[RNBetterTapGestureRecognizer alloc] initWithGestureHandler:self];
197  }
198  return self;
199}
200
201- (void)resetConfig
202{
203  [super resetConfig];
204  RNBetterTapGestureRecognizer *recognizer = (RNBetterTapGestureRecognizer *)_recognizer;
205
206  recognizer.numberOfTaps = defaultNumberOfTaps;
207  recognizer.minPointers = defaultMinPointers;
208  recognizer.maxDeltaX = NAN;
209  recognizer.maxDeltaY = NAN;
210  recognizer.maxDelay = defaultMaxDelay;
211  recognizer.maxDuration = defaultMaxDuration;
212  recognizer.maxDistSq = NAN;
213}
214
215- (void)configure:(NSDictionary *)config
216{
217  [super configure:config];
218  RNBetterTapGestureRecognizer *recognizer = (RNBetterTapGestureRecognizer *)_recognizer;
219
220  APPLY_INT_PROP(numberOfTaps);
221  APPLY_INT_PROP(minPointers);
222  APPLY_FLOAT_PROP(maxDeltaX);
223  APPLY_FLOAT_PROP(maxDeltaY);
224
225  id prop = config[@"maxDelayMs"];
226  if (prop != nil) {
227    recognizer.maxDelay = [RCTConvert CGFloat:prop] / 1000.0;
228  }
229
230  prop = config[@"maxDurationMs"];
231  if (prop != nil) {
232    recognizer.maxDuration = [RCTConvert CGFloat:prop] / 1000.0;
233  }
234
235  prop = config[@"maxDist"];
236  if (prop != nil) {
237    CGFloat dist = [RCTConvert CGFloat:prop];
238    recognizer.maxDistSq = dist * dist;
239  }
240}
241
242- (RNGestureHandlerEventExtraData *)eventExtraData:(UIGestureRecognizer *)recognizer
243{
244  if (recognizer.state == UIGestureRecognizerStateEnded) {
245    return _lastData;
246  }
247
248  _lastData = [super eventExtraData:recognizer];
249  return _lastData;
250}
251
252- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
253{
254  // UNDETERMINED -> BEGAN state change event is sent before this method is called,
255  // in RNGestureHandler it resets _lastSatate variable, making is seem like handler
256  // went from UNDETERMINED to BEGAN and then from UNDETERMINED to ACTIVE.
257  // This way we preserve _lastState between events and keep correct state flow.
258  RNGestureHandlerState savedState = _lastState;
259  BOOL shouldBegin = [super gestureRecognizerShouldBegin:gestureRecognizer];
260  _lastState = savedState;
261
262  return shouldBegin;
263}
264
265@end
266