RCTTouchHandler.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /*
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. #import "RCTTouchHandler.h"
  8. #import <UIKit/UIGestureRecognizerSubclass.h>
  9. #import "RCTAssert.h"
  10. #import "RCTBridge.h"
  11. #import "RCTEventDispatcher.h"
  12. #import "RCTLog.h"
  13. #import "RCTSurfaceView.h"
  14. #import "RCTTouchEvent.h"
  15. #import "RCTUIManager.h"
  16. #import "RCTUtils.h"
  17. #import "UIView+React.h"
  18. @interface RCTTouchHandler () <UIGestureRecognizerDelegate>
  19. @end
  20. // TODO: this class behaves a lot like a module, and could be implemented as a
  21. // module if we were to assume that modules and RootViews had a 1:1 relationship
  22. @implementation RCTTouchHandler {
  23. __weak RCTEventDispatcher *_eventDispatcher;
  24. /**
  25. * Arrays managed in parallel tracking native touch object along with the
  26. * native view that was touched, and the React touch data dictionary.
  27. * These must be kept track of because `UIKit` destroys the touch targets
  28. * if touches are canceled, and we have no other way to recover this info.
  29. */
  30. NSMutableOrderedSet<UITouch *> *_nativeTouches;
  31. NSMutableArray<NSMutableDictionary *> *_reactTouches;
  32. NSMutableArray<UIView *> *_touchViews;
  33. __weak UIView *_cachedRootView;
  34. uint16_t _coalescingKey;
  35. }
  36. - (instancetype)initWithBridge:(RCTBridge *)bridge
  37. {
  38. RCTAssertParam(bridge);
  39. if ((self = [super initWithTarget:nil action:NULL])) {
  40. _eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]];
  41. _nativeTouches = [NSMutableOrderedSet new];
  42. _reactTouches = [NSMutableArray new];
  43. _touchViews = [NSMutableArray new];
  44. // `cancelsTouchesInView` and `delaysTouches*` are needed in order to be used as a top level
  45. // event delegated recognizer. Otherwise, lower-level components not built
  46. // using RCT, will fail to recognize gestures.
  47. self.cancelsTouchesInView = NO;
  48. self.delaysTouchesBegan = NO; // This is default value.
  49. self.delaysTouchesEnded = NO;
  50. self.delegate = self;
  51. }
  52. return self;
  53. }
  54. RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action)
  55. - (void)attachToView:(UIView *)view
  56. {
  57. RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view.");
  58. [view addGestureRecognizer:self];
  59. }
  60. - (void)detachFromView:(UIView *)view
  61. {
  62. RCTAssertParam(view);
  63. RCTAssert(self.view == view, @"RCTTouchHandler attached to another view.");
  64. [view removeGestureRecognizer:self];
  65. }
  66. #pragma mark - Bookkeeping for touch indices
  67. - (void)_recordNewTouches:(NSSet<UITouch *> *)touches
  68. {
  69. for (UITouch *touch in touches) {
  70. RCTAssert(![_nativeTouches containsObject:touch], @"Touch is already recorded. This is a critical bug.");
  71. // Find closest React-managed touchable view
  72. UIView *targetView = touch.view;
  73. while (targetView) {
  74. if (targetView.reactTag && targetView.userInteractionEnabled) {
  75. break;
  76. }
  77. targetView = targetView.superview;
  78. }
  79. NSNumber *reactTag = [targetView reactTagAtPoint:[touch locationInView:targetView]];
  80. if (!reactTag || !targetView.userInteractionEnabled) {
  81. continue;
  82. }
  83. // Get new, unique touch identifier for the react touch
  84. const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
  85. NSInteger touchID = ([_reactTouches.lastObject[@"identifier"] integerValue] + 1) % RCTMaxTouches;
  86. for (NSDictionary *reactTouch in _reactTouches) {
  87. NSInteger usedID = [reactTouch[@"identifier"] integerValue];
  88. if (usedID == touchID) {
  89. // ID has already been used, try next value
  90. touchID++;
  91. } else if (usedID > touchID) {
  92. // If usedID > touchID, touchID must be unique, so we can stop looking
  93. break;
  94. }
  95. }
  96. // Create touch
  97. NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:RCTMaxTouches];
  98. reactTouch[@"target"] = reactTag;
  99. reactTouch[@"identifier"] = @(touchID);
  100. // Add to arrays
  101. [_touchViews addObject:targetView];
  102. [_nativeTouches addObject:touch];
  103. [_reactTouches addObject:reactTouch];
  104. }
  105. }
  106. - (void)_recordRemovedTouches:(NSSet<UITouch *> *)touches
  107. {
  108. for (UITouch *touch in touches) {
  109. NSUInteger index = [_nativeTouches indexOfObject:touch];
  110. if (index == NSNotFound) {
  111. continue;
  112. }
  113. [_touchViews removeObjectAtIndex:index];
  114. [_nativeTouches removeObjectAtIndex:index];
  115. [_reactTouches removeObjectAtIndex:index];
  116. }
  117. }
  118. - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
  119. {
  120. UITouch *nativeTouch = _nativeTouches[touchIndex];
  121. CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
  122. RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch");
  123. CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:_cachedRootView];
  124. UIView *touchView = _touchViews[touchIndex];
  125. CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];
  126. NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
  127. reactTouch[@"pageX"] = @(RCTSanitizeNaNValue(rootViewLocation.x, @"touchEvent.pageX"));
  128. reactTouch[@"pageY"] = @(RCTSanitizeNaNValue(rootViewLocation.y, @"touchEvent.pageY"));
  129. reactTouch[@"locationX"] = @(RCTSanitizeNaNValue(touchViewLocation.x, @"touchEvent.locationX"));
  130. reactTouch[@"locationY"] = @(RCTSanitizeNaNValue(touchViewLocation.y, @"touchEvent.locationY"));
  131. reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS
  132. // TODO: force for a 'normal' touch is usually 1.0;
  133. // should we expose a `normalTouchForce` constant somewhere (which would
  134. // have a value of `1.0 / nativeTouch.maximumPossibleForce`)?
  135. if (RCTForceTouchAvailable()) {
  136. reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force / nativeTouch.maximumPossibleForce));
  137. }
  138. }
  139. /**
  140. * Constructs information about touch events to send across the serialized
  141. * boundary. This data should be compliant with W3C `Touch` objects. This data
  142. * alone isn't sufficient to construct W3C `Event` objects. To construct that,
  143. * there must be a simple receiver on the other side of the bridge that
  144. * organizes the touch objects into `Event`s.
  145. *
  146. * We send the data as an array of `Touch`es, the type of action
  147. * (start/end/move/cancel) and the indices that represent "changed" `Touch`es
  148. * from that array.
  149. */
  150. - (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches eventName:(NSString *)eventName
  151. {
  152. // Update touches
  153. NSMutableArray<NSNumber *> *changedIndexes = [NSMutableArray new];
  154. for (UITouch *touch in touches) {
  155. NSInteger index = [_nativeTouches indexOfObject:touch];
  156. if (index == NSNotFound) {
  157. continue;
  158. }
  159. [self _updateReactTouchAtIndex:index];
  160. [changedIndexes addObject:@(index)];
  161. }
  162. if (changedIndexes.count == 0) {
  163. return;
  164. }
  165. // Deep copy the touches because they will be accessed from another thread
  166. // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller?
  167. NSMutableArray<NSDictionary *> *reactTouches = [[NSMutableArray alloc] initWithCapacity:_reactTouches.count];
  168. for (NSDictionary *touch in _reactTouches) {
  169. [reactTouches addObject:[touch copy]];
  170. }
  171. BOOL canBeCoalesced = [eventName isEqualToString:@"touchMove"];
  172. // We increment `_coalescingKey` twice here just for sure that
  173. // this `_coalescingKey` will not be reused by another (preceding or following) event
  174. // (yes, even if coalescing only happens (and makes sense) on events of the same type).
  175. if (!canBeCoalesced) {
  176. _coalescingKey++;
  177. }
  178. RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
  179. reactTag:self.view.reactTag
  180. reactTouches:reactTouches
  181. changedIndexes:changedIndexes
  182. coalescingKey:_coalescingKey];
  183. if (!canBeCoalesced) {
  184. _coalescingKey++;
  185. }
  186. [_eventDispatcher sendEvent:event];
  187. }
  188. /***
  189. * To ensure compatibility when using UIManager.measure and RCTTouchHandler, we have to adopt
  190. * UIManager.measure's behavior in finding a "root view".
  191. * Usually RCTTouchHandler is already attached to a root view but in some cases (e.g. Modal),
  192. * we are instead attached to some RCTView subtree. This is also the case when embedding some RN
  193. * views inside a separate ViewController not controlled by RN.
  194. * This logic will either find the nearest rootView, or go all the way to the UIWindow.
  195. * While this is not optimal, it is exactly what UIManager.measure does, and what Touchable.js
  196. * relies on.
  197. * We cache it here so that we don't have to repeat it for every touch in the gesture.
  198. */
  199. - (void)_cacheRootView
  200. {
  201. UIView *rootView = self.view;
  202. while (rootView.superview && ![rootView isReactRootView] && ![rootView isKindOfClass:[RCTSurfaceView class]]) {
  203. rootView = rootView.superview;
  204. }
  205. _cachedRootView = rootView;
  206. }
  207. #pragma mark - Gesture Recognizer Delegate Callbacks
  208. static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet<UITouch *> *touches)
  209. {
  210. for (UITouch *touch in touches) {
  211. if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) {
  212. return NO;
  213. }
  214. }
  215. return YES;
  216. }
  217. static BOOL RCTAnyTouchesChanged(NSSet<UITouch *> *touches)
  218. {
  219. for (UITouch *touch in touches) {
  220. if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
  221. return YES;
  222. }
  223. }
  224. return NO;
  225. }
  226. #pragma mark - `UIResponder`-ish touch-delivery methods
  227. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  228. {
  229. [super touchesBegan:touches withEvent:event];
  230. [self _cacheRootView];
  231. // "start" has to record new touches *before* extracting the event.
  232. // "end"/"cancel" needs to remove the touch *after* extracting the event.
  233. [self _recordNewTouches:touches];
  234. [self _updateAndDispatchTouches:touches eventName:@"touchStart"];
  235. if (self.state == UIGestureRecognizerStatePossible) {
  236. self.state = UIGestureRecognizerStateBegan;
  237. } else if (self.state == UIGestureRecognizerStateBegan) {
  238. self.state = UIGestureRecognizerStateChanged;
  239. }
  240. }
  241. - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  242. {
  243. [super touchesMoved:touches withEvent:event];
  244. [self _updateAndDispatchTouches:touches eventName:@"touchMove"];
  245. self.state = UIGestureRecognizerStateChanged;
  246. }
  247. - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  248. {
  249. [super touchesEnded:touches withEvent:event];
  250. [self _updateAndDispatchTouches:touches eventName:@"touchEnd"];
  251. if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
  252. self.state = UIGestureRecognizerStateEnded;
  253. } else if (RCTAnyTouchesChanged(event.allTouches)) {
  254. self.state = UIGestureRecognizerStateChanged;
  255. }
  256. [self _recordRemovedTouches:touches];
  257. }
  258. - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  259. {
  260. [super touchesCancelled:touches withEvent:event];
  261. [self _updateAndDispatchTouches:touches eventName:@"touchCancel"];
  262. if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
  263. self.state = UIGestureRecognizerStateCancelled;
  264. } else if (RCTAnyTouchesChanged(event.allTouches)) {
  265. self.state = UIGestureRecognizerStateChanged;
  266. }
  267. [self _recordRemovedTouches:touches];
  268. }
  269. - (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGestureRecognizer
  270. {
  271. return NO;
  272. }
  273. - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
  274. {
  275. // We fail in favour of other external gesture recognizers.
  276. // iOS will ask `delegate`'s opinion about this gesture recognizer little bit later.
  277. return ![preventingGestureRecognizer.view isDescendantOfView:self.view];
  278. }
  279. - (void)reset
  280. {
  281. if (_nativeTouches.count != 0) {
  282. [self _updateAndDispatchTouches:_nativeTouches.set eventName:@"touchCancel"];
  283. [_nativeTouches removeAllObjects];
  284. [_reactTouches removeAllObjects];
  285. [_touchViews removeAllObjects];
  286. _cachedRootView = nil;
  287. }
  288. }
  289. #pragma mark - Other
  290. - (void)cancel
  291. {
  292. self.enabled = NO;
  293. self.enabled = YES;
  294. }
  295. #pragma mark - UIGestureRecognizerDelegate
  296. - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer
  297. shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
  298. {
  299. // Same condition for `failure of` as for `be prevented by`.
  300. return [self canBePreventedByGestureRecognizer:otherGestureRecognizer];
  301. }
  302. @end