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