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