1// 2// RNPanHandler.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 "RNPanHandler.h" 10 11#import <UIKit/UIGestureRecognizerSubclass.h> 12 13@interface RNBetterPanGestureRecognizer : UIPanGestureRecognizer 14 15@property (nonatomic) CGFloat minDistSq; 16@property (nonatomic) CGFloat minVelocityX; 17@property (nonatomic) CGFloat minVelocityY; 18@property (nonatomic) CGFloat minVelocitySq; 19@property (nonatomic) CGFloat activeOffsetXStart; 20@property (nonatomic) CGFloat activeOffsetXEnd; 21@property (nonatomic) CGFloat failOffsetXStart; 22@property (nonatomic) CGFloat failOffsetXEnd; 23@property (nonatomic) CGFloat activeOffsetYStart; 24@property (nonatomic) CGFloat activeOffsetYEnd; 25@property (nonatomic) CGFloat failOffsetYStart; 26@property (nonatomic) CGFloat failOffsetYEnd; 27@property (nonatomic) CGFloat activateAfterLongPress; 28 29- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler; 30 31@end 32 33@implementation RNBetterPanGestureRecognizer { 34 __weak RNGestureHandler *_gestureHandler; 35 NSUInteger _realMinimumNumberOfTouches; 36 BOOL _hasCustomActivationCriteria; 37} 38 39- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler 40{ 41 if ((self = [super initWithTarget:gestureHandler action:@selector(handleGesture:)])) { 42 _gestureHandler = gestureHandler; 43 _minDistSq = NAN; 44 _minVelocityX = NAN; 45 _minVelocityY = NAN; 46 _minVelocitySq = NAN; 47 _activeOffsetXStart = NAN; 48 _activeOffsetXEnd = NAN; 49 _failOffsetXStart = NAN; 50 _failOffsetXEnd = NAN; 51 _activeOffsetYStart = NAN; 52 _activeOffsetYEnd = NAN; 53 _failOffsetYStart = NAN; 54 _failOffsetYEnd = NAN; 55 _activateAfterLongPress = NAN; 56 _hasCustomActivationCriteria = NO; 57#if !TARGET_OS_TV 58 _realMinimumNumberOfTouches = self.minimumNumberOfTouches; 59#endif 60 } 61 return self; 62} 63 64- (void)triggerAction 65{ 66 [_gestureHandler handleGesture:self]; 67} 68 69- (void)setMinimumNumberOfTouches:(NSUInteger)minimumNumberOfTouches 70{ 71 _realMinimumNumberOfTouches = minimumNumberOfTouches; 72} 73 74- (void)activateAfterLongPress 75{ 76 self.state = UIGestureRecognizerStateBegan; 77 // Send event in ACTIVE state because UIGestureRecognizerStateBegan is mapped to RNGestureHandlerStateBegan 78 [_gestureHandler handleGesture:self inState:RNGestureHandlerStateActive]; 79} 80 81- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 82{ 83 if ([self numberOfTouches] == 0) { 84 [_gestureHandler reset]; 85 } 86#if !TARGET_OS_TV 87 if (_hasCustomActivationCriteria) { 88 // We use "minimumNumberOfTouches" property to prevent pan handler from recognizing 89 // the gesture too early before we are sure that all criteria (e.g. minimum distance 90 // etc. are met) 91 super.minimumNumberOfTouches = 20; 92 } else { 93 super.minimumNumberOfTouches = _realMinimumNumberOfTouches; 94 } 95#endif 96 [super touchesBegan:touches withEvent:event]; 97 [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; 98 [self triggerAction]; 99 100 if (!isnan(_activateAfterLongPress)) { 101 [self performSelector:@selector(activateAfterLongPress) withObject:nil afterDelay:_activateAfterLongPress]; 102 } 103} 104 105- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 106{ 107 [super touchesMoved:touches withEvent:event]; 108 [_gestureHandler.pointerTracker touchesMoved:touches withEvent:event]; 109 110 if (self.state == UIGestureRecognizerStatePossible && [self shouldFailUnderCustomCriteria]) { 111 self.state = UIGestureRecognizerStateFailed; 112 return; 113 } 114 if ((self.state == UIGestureRecognizerStatePossible || self.state == UIGestureRecognizerStateChanged)) { 115 if (_gestureHandler.shouldCancelWhenOutside && ![_gestureHandler containsPointInView]) { 116 // If the previous recognizer state is UIGestureRecognizerStateChanged 117 // then UIGestureRecognizer's sate machine will only transition to 118 // UIGestureRecognizerStateCancelled even if you set the state to 119 // UIGestureRecognizerStateFailed here. Making the behavior explicit. 120 self.state = (self.state == UIGestureRecognizerStatePossible) ? UIGestureRecognizerStateFailed 121 : UIGestureRecognizerStateCancelled; 122 [self reset]; 123 return; 124 } 125 } 126 if (_hasCustomActivationCriteria && self.state == UIGestureRecognizerStatePossible && 127 [self shouldActivateUnderCustomCriteria]) { 128#if !TARGET_OS_TV 129 super.minimumNumberOfTouches = _realMinimumNumberOfTouches; 130 if ([self numberOfTouches] >= _realMinimumNumberOfTouches) { 131 self.state = UIGestureRecognizerStateBegan; 132 [self setTranslation:CGPointMake(0, 0) inView:self.view]; 133 } 134#endif 135 } 136} 137 138- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 139{ 140 [super touchesEnded:touches withEvent:event]; 141 [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; 142} 143 144- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 145{ 146 [super touchesCancelled:touches withEvent:event]; 147 [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; 148} 149 150- (void)reset 151{ 152 [self triggerAction]; 153 [_gestureHandler.pointerTracker reset]; 154 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(activateAfterLongPress) object:nil]; 155 self.enabled = YES; 156 [super reset]; 157} 158 159- (void)updateHasCustomActivationCriteria 160{ 161 _hasCustomActivationCriteria = !isnan(_minDistSq) || !isnan(_minVelocityX) || !isnan(_minVelocityY) || 162 !isnan(_minVelocitySq) || !isnan(_activeOffsetXStart) || !isnan(_activeOffsetXEnd) || 163 !isnan(_activeOffsetYStart) || !isnan(_activeOffsetYEnd); 164} 165 166- (BOOL)shouldFailUnderCustomCriteria 167{ 168 CGPoint trans = [self translationInView:self.view.window]; 169 // Apple docs say that 10 units is the default allowable movement for UILongPressGestureRecognizer 170 if (!isnan(_activateAfterLongPress) && trans.x * trans.x + trans.y * trans.y > 100) { 171 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(activateAfterLongPress) object:nil]; 172 return YES; 173 } 174 175 if (!isnan(_failOffsetXStart) && trans.x < _failOffsetXStart) { 176 return YES; 177 } 178 if (!isnan(_failOffsetXEnd) && trans.x > _failOffsetXEnd) { 179 return YES; 180 } 181 if (!isnan(_failOffsetYStart) && trans.y < _failOffsetYStart) { 182 return YES; 183 } 184 if (!isnan(_failOffsetYEnd) && trans.y > _failOffsetYEnd) { 185 return YES; 186 } 187 return NO; 188} 189 190- (BOOL)shouldActivateUnderCustomCriteria 191{ 192 CGPoint trans = [self translationInView:self.view.window]; 193 if (!isnan(_activeOffsetXStart) && trans.x < _activeOffsetXStart) { 194 return YES; 195 } 196 if (!isnan(_activeOffsetXEnd) && trans.x > _activeOffsetXEnd) { 197 return YES; 198 } 199 if (!isnan(_activeOffsetYStart) && trans.y < _activeOffsetYStart) { 200 return YES; 201 } 202 if (!isnan(_activeOffsetYEnd) && trans.y > _activeOffsetYEnd) { 203 return YES; 204 } 205 206 if (TEST_MIN_IF_NOT_NAN(VEC_LEN_SQ(trans), _minDistSq)) { 207 return YES; 208 } 209 210 CGPoint velocity = [self velocityInView:self.view]; 211 if (TEST_MIN_IF_NOT_NAN(velocity.x, _minVelocityX)) { 212 return YES; 213 } 214 if (TEST_MIN_IF_NOT_NAN(velocity.y, _minVelocityY)) { 215 return YES; 216 } 217 if (TEST_MIN_IF_NOT_NAN(VEC_LEN_SQ(velocity), _minVelocitySq)) { 218 return YES; 219 } 220 221 return NO; 222} 223 224@end 225 226@implementation RNPanGestureHandler 227 228- (instancetype)initWithTag:(NSNumber *)tag 229{ 230 if ((self = [super initWithTag:tag])) { 231 _recognizer = [[RNBetterPanGestureRecognizer alloc] initWithGestureHandler:self]; 232 } 233 return self; 234} 235 236- (void)resetConfig 237{ 238 [super resetConfig]; 239 RNBetterPanGestureRecognizer *recognizer = (RNBetterPanGestureRecognizer *)_recognizer; 240 recognizer.minVelocityX = NAN; 241 recognizer.minVelocityY = NAN; 242 recognizer.activeOffsetXStart = NAN; 243 recognizer.activeOffsetXEnd = NAN; 244 recognizer.failOffsetXStart = NAN; 245 recognizer.failOffsetXEnd = NAN; 246 recognizer.activeOffsetYStart = NAN; 247 recognizer.activeOffsetYEnd = NAN; 248 recognizer.failOffsetYStart = NAN; 249 recognizer.failOffsetYStart = NAN; 250 recognizer.failOffsetYEnd = NAN; 251#if !TARGET_OS_TV && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400 252 if (@available(iOS 13.4, *)) { 253 recognizer.allowedScrollTypesMask = 0; 254 } 255#endif 256#if !TARGET_OS_TV 257 recognizer.minimumNumberOfTouches = 1; 258 recognizer.maximumNumberOfTouches = NSUIntegerMax; 259#endif 260 recognizer.minDistSq = NAN; 261 recognizer.minVelocitySq = NAN; 262 recognizer.activateAfterLongPress = NAN; 263} 264 265- (void)configure:(NSDictionary *)config 266{ 267 [super configure:config]; 268 RNBetterPanGestureRecognizer *recognizer = (RNBetterPanGestureRecognizer *)_recognizer; 269 270 APPLY_FLOAT_PROP(minVelocityX); 271 APPLY_FLOAT_PROP(minVelocityY); 272 APPLY_FLOAT_PROP(activeOffsetXStart); 273 APPLY_FLOAT_PROP(activeOffsetXEnd); 274 APPLY_FLOAT_PROP(failOffsetXStart); 275 APPLY_FLOAT_PROP(failOffsetXEnd); 276 APPLY_FLOAT_PROP(activeOffsetYStart); 277 APPLY_FLOAT_PROP(activeOffsetYEnd); 278 APPLY_FLOAT_PROP(failOffsetYStart); 279 APPLY_FLOAT_PROP(failOffsetYEnd); 280 281#if !TARGET_OS_TV && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400 282 if (@available(iOS 13.4, *)) { 283 bool enableTrackpadTwoFingerGesture = [RCTConvert BOOL:config[@"enableTrackpadTwoFingerGesture"]]; 284 if (enableTrackpadTwoFingerGesture) { 285 recognizer.allowedScrollTypesMask = UIScrollTypeMaskAll; 286 } 287 } 288 289 APPLY_NAMED_INT_PROP(minimumNumberOfTouches, @"minPointers"); 290 APPLY_NAMED_INT_PROP(maximumNumberOfTouches, @"maxPointers"); 291#endif 292 293 id prop = config[@"minDist"]; 294 if (prop != nil) { 295 CGFloat dist = [RCTConvert CGFloat:prop]; 296 recognizer.minDistSq = dist * dist; 297 } 298 299 prop = config[@"minVelocity"]; 300 if (prop != nil) { 301 CGFloat velocity = [RCTConvert CGFloat:prop]; 302 recognizer.minVelocitySq = velocity * velocity; 303 } 304 305 prop = config[@"activateAfterLongPress"]; 306 if (prop != nil) { 307 recognizer.activateAfterLongPress = [RCTConvert CGFloat:prop] / 1000.0; 308 recognizer.minDistSq = MAX(100, recognizer.minDistSq); 309 } 310 [recognizer updateHasCustomActivationCriteria]; 311} 312 313- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 314{ 315 RNGestureHandlerState savedState = _lastState; 316 BOOL shouldBegin = [super gestureRecognizerShouldBegin:gestureRecognizer]; 317 _lastState = savedState; 318 319 return shouldBegin; 320} 321 322- (RNGestureHandlerEventExtraData *)eventExtraData:(UIPanGestureRecognizer *)recognizer 323{ 324 return [RNGestureHandlerEventExtraData forPan:[recognizer locationInView:recognizer.view] 325 withAbsolutePosition:[recognizer locationInView:recognizer.view.window] 326 withTranslation:[recognizer translationInView:recognizer.view.window] 327 withVelocity:[recognizer velocityInView:recognizer.view.window] 328 withNumberOfTouches:recognizer.numberOfTouches]; 329} 330 331@end 332