123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /*
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
- #import "RCTTouchHandler.h"
- #import <UIKit/UIGestureRecognizerSubclass.h>
- #import "RCTAssert.h"
- #import "RCTBridge.h"
- #import "RCTEventDispatcher.h"
- #import "RCTLog.h"
- #import "RCTSurfaceView.h"
- #import "RCTTouchEvent.h"
- #import "RCTUIManager.h"
- #import "RCTUtils.h"
- #import "UIView+React.h"
- @interface RCTTouchHandler () <UIGestureRecognizerDelegate>
- @end
- // TODO: this class behaves a lot like a module, and could be implemented as a
- // module if we were to assume that modules and RootViews had a 1:1 relationship
- @implementation RCTTouchHandler {
- __weak RCTEventDispatcher *_eventDispatcher;
- /**
- * Arrays managed in parallel tracking native touch object along with the
- * native view that was touched, and the React touch data dictionary.
- * These must be kept track of because `UIKit` destroys the touch targets
- * if touches are canceled, and we have no other way to recover this info.
- */
- NSMutableOrderedSet<UITouch *> *_nativeTouches;
- NSMutableArray<NSMutableDictionary *> *_reactTouches;
- NSMutableArray<UIView *> *_touchViews;
- __weak UIView *_cachedRootView;
- uint16_t _coalescingKey;
- }
- - (instancetype)initWithBridge:(RCTBridge *)bridge
- {
- RCTAssertParam(bridge);
- if ((self = [super initWithTarget:nil action:NULL])) {
- _eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]];
- _nativeTouches = [NSMutableOrderedSet new];
- _reactTouches = [NSMutableArray new];
- _touchViews = [NSMutableArray new];
- // `cancelsTouchesInView` and `delaysTouches*` are needed in order to be used as a top level
- // event delegated recognizer. Otherwise, lower-level components not built
- // using RCT, will fail to recognize gestures.
- self.cancelsTouchesInView = NO;
- self.delaysTouchesBegan = NO; // This is default value.
- self.delaysTouchesEnded = NO;
- self.delegate = self;
- }
- return self;
- }
- RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action)
- - (void)attachToView:(UIView *)view
- {
- RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view.");
- [view addGestureRecognizer:self];
- }
- - (void)detachFromView:(UIView *)view
- {
- RCTAssertParam(view);
- RCTAssert(self.view == view, @"RCTTouchHandler attached to another view.");
- [view removeGestureRecognizer:self];
- }
- #pragma mark - Bookkeeping for touch indices
- - (void)_recordNewTouches:(NSSet<UITouch *> *)touches
- {
- for (UITouch *touch in touches) {
- RCTAssert(![_nativeTouches containsObject:touch], @"Touch is already recorded. This is a critical bug.");
- // Find closest React-managed touchable view
- UIView *targetView = touch.view;
- while (targetView) {
- if (targetView.reactTag && targetView.userInteractionEnabled) {
- break;
- }
- targetView = targetView.superview;
- }
- NSNumber *reactTag = [targetView reactTagAtPoint:[touch locationInView:targetView]];
- if (!reactTag || !targetView.userInteractionEnabled) {
- continue;
- }
- // Get new, unique touch identifier for the react touch
- const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
- NSInteger touchID = ([_reactTouches.lastObject[@"identifier"] integerValue] + 1) % RCTMaxTouches;
- for (NSDictionary *reactTouch in _reactTouches) {
- NSInteger usedID = [reactTouch[@"identifier"] integerValue];
- if (usedID == touchID) {
- // ID has already been used, try next value
- touchID++;
- } else if (usedID > touchID) {
- // If usedID > touchID, touchID must be unique, so we can stop looking
- break;
- }
- }
- // Create touch
- NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:RCTMaxTouches];
- reactTouch[@"target"] = reactTag;
- reactTouch[@"identifier"] = @(touchID);
- // Add to arrays
- [_touchViews addObject:targetView];
- [_nativeTouches addObject:touch];
- [_reactTouches addObject:reactTouch];
- }
- }
- - (void)_recordRemovedTouches:(NSSet<UITouch *> *)touches
- {
- for (UITouch *touch in touches) {
- NSUInteger index = [_nativeTouches indexOfObject:touch];
- if (index == NSNotFound) {
- continue;
- }
- [_touchViews removeObjectAtIndex:index];
- [_nativeTouches removeObjectAtIndex:index];
- [_reactTouches removeObjectAtIndex:index];
- }
- }
- - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
- {
- UITouch *nativeTouch = _nativeTouches[touchIndex];
- CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
- RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch");
- CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:_cachedRootView];
- UIView *touchView = _touchViews[touchIndex];
- CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];
- NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
- reactTouch[@"pageX"] = @(RCTSanitizeNaNValue(rootViewLocation.x, @"touchEvent.pageX"));
- reactTouch[@"pageY"] = @(RCTSanitizeNaNValue(rootViewLocation.y, @"touchEvent.pageY"));
- reactTouch[@"locationX"] = @(RCTSanitizeNaNValue(touchViewLocation.x, @"touchEvent.locationX"));
- reactTouch[@"locationY"] = @(RCTSanitizeNaNValue(touchViewLocation.y, @"touchEvent.locationY"));
- reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS
- // TODO: force for a 'normal' touch is usually 1.0;
- // should we expose a `normalTouchForce` constant somewhere (which would
- // have a value of `1.0 / nativeTouch.maximumPossibleForce`)?
- if (RCTForceTouchAvailable()) {
- reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force / nativeTouch.maximumPossibleForce));
- }
- }
- /**
- * Constructs information about touch events to send across the serialized
- * boundary. This data should be compliant with W3C `Touch` objects. This data
- * alone isn't sufficient to construct W3C `Event` objects. To construct that,
- * there must be a simple receiver on the other side of the bridge that
- * organizes the touch objects into `Event`s.
- *
- * We send the data as an array of `Touch`es, the type of action
- * (start/end/move/cancel) and the indices that represent "changed" `Touch`es
- * from that array.
- */
- - (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches eventName:(NSString *)eventName
- {
- // Update touches
- NSMutableArray<NSNumber *> *changedIndexes = [NSMutableArray new];
- for (UITouch *touch in touches) {
- NSInteger index = [_nativeTouches indexOfObject:touch];
- if (index == NSNotFound) {
- continue;
- }
- [self _updateReactTouchAtIndex:index];
- [changedIndexes addObject:@(index)];
- }
- if (changedIndexes.count == 0) {
- return;
- }
- // Deep copy the touches because they will be accessed from another thread
- // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller?
- NSMutableArray<NSDictionary *> *reactTouches = [[NSMutableArray alloc] initWithCapacity:_reactTouches.count];
- for (NSDictionary *touch in _reactTouches) {
- [reactTouches addObject:[touch copy]];
- }
- BOOL canBeCoalesced = [eventName isEqualToString:@"touchMove"];
- // We increment `_coalescingKey` twice here just for sure that
- // this `_coalescingKey` will not be reused by another (preceding or following) event
- // (yes, even if coalescing only happens (and makes sense) on events of the same type).
- if (!canBeCoalesced) {
- _coalescingKey++;
- }
- RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
- reactTag:self.view.reactTag
- reactTouches:reactTouches
- changedIndexes:changedIndexes
- coalescingKey:_coalescingKey];
- if (!canBeCoalesced) {
- _coalescingKey++;
- }
- [_eventDispatcher sendEvent:event];
- }
- /***
- * To ensure compatibility when using UIManager.measure and RCTTouchHandler, we have to adopt
- * UIManager.measure's behavior in finding a "root view".
- * Usually RCTTouchHandler is already attached to a root view but in some cases (e.g. Modal),
- * we are instead attached to some RCTView subtree. This is also the case when embedding some RN
- * views inside a separate ViewController not controlled by RN.
- * This logic will either find the nearest rootView, or go all the way to the UIWindow.
- * While this is not optimal, it is exactly what UIManager.measure does, and what Touchable.js
- * relies on.
- * We cache it here so that we don't have to repeat it for every touch in the gesture.
- */
- - (void)_cacheRootView
- {
- UIView *rootView = self.view;
- while (rootView.superview && ![rootView isReactRootView] && ![rootView isKindOfClass:[RCTSurfaceView class]]) {
- rootView = rootView.superview;
- }
- _cachedRootView = rootView;
- }
- #pragma mark - Gesture Recognizer Delegate Callbacks
- static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet<UITouch *> *touches)
- {
- for (UITouch *touch in touches) {
- if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) {
- return NO;
- }
- }
- return YES;
- }
- static BOOL RCTAnyTouchesChanged(NSSet<UITouch *> *touches)
- {
- for (UITouch *touch in touches) {
- if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
- return YES;
- }
- }
- return NO;
- }
- #pragma mark - `UIResponder`-ish touch-delivery methods
- - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesBegan:touches withEvent:event];
- [self _cacheRootView];
- // "start" has to record new touches *before* extracting the event.
- // "end"/"cancel" needs to remove the touch *after* extracting the event.
- [self _recordNewTouches:touches];
- [self _updateAndDispatchTouches:touches eventName:@"touchStart"];
- if (self.state == UIGestureRecognizerStatePossible) {
- self.state = UIGestureRecognizerStateBegan;
- } else if (self.state == UIGestureRecognizerStateBegan) {
- self.state = UIGestureRecognizerStateChanged;
- }
- }
- - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesMoved:touches withEvent:event];
- [self _updateAndDispatchTouches:touches eventName:@"touchMove"];
- self.state = UIGestureRecognizerStateChanged;
- }
- - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesEnded:touches withEvent:event];
- [self _updateAndDispatchTouches:touches eventName:@"touchEnd"];
- if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
- self.state = UIGestureRecognizerStateEnded;
- } else if (RCTAnyTouchesChanged(event.allTouches)) {
- self.state = UIGestureRecognizerStateChanged;
- }
- [self _recordRemovedTouches:touches];
- }
- - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesCancelled:touches withEvent:event];
- [self _updateAndDispatchTouches:touches eventName:@"touchCancel"];
- if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
- self.state = UIGestureRecognizerStateCancelled;
- } else if (RCTAnyTouchesChanged(event.allTouches)) {
- self.state = UIGestureRecognizerStateChanged;
- }
- [self _recordRemovedTouches:touches];
- }
- - (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGestureRecognizer
- {
- return NO;
- }
- - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
- {
- // We fail in favour of other external gesture recognizers.
- // iOS will ask `delegate`'s opinion about this gesture recognizer little bit later.
- return ![preventingGestureRecognizer.view isDescendantOfView:self.view];
- }
- - (void)reset
- {
- if (_nativeTouches.count != 0) {
- [self _updateAndDispatchTouches:_nativeTouches.set eventName:@"touchCancel"];
- [_nativeTouches removeAllObjects];
- [_reactTouches removeAllObjects];
- [_touchViews removeAllObjects];
- _cachedRootView = nil;
- }
- }
- #pragma mark - Other
- - (void)cancel
- {
- self.enabled = NO;
- self.enabled = YES;
- }
- #pragma mark - UIGestureRecognizerDelegate
- - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer
- shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
- {
- // Same condition for `failure of` as for `be prevented by`.
- return [self canBePreventedByGestureRecognizer:otherGestureRecognizer];
- }
- @end
|