/** * 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 "RCTSurfaceTouchHandler.h" #import #import #import #import "RCTConversions.h" #import "RCTTouchableComponentViewProtocol.h" using namespace facebook::react; template class IdentifierPool { public: void enqueue(int index) { usage[index] = false; } int dequeue() { while (true) { if (!usage[lastIndex]) { usage[lastIndex] = true; return lastIndex; } lastIndex = (lastIndex + 1) % size; } } void reset() { for (int i = 0; i < size; i++) { usage[i] = false; } } private: bool usage[size]; int lastIndex; }; typedef NS_ENUM(NSInteger, RCTTouchEventType) { RCTTouchEventTypeTouchStart, RCTTouchEventTypeTouchMove, RCTTouchEventTypeTouchEnd, RCTTouchEventTypeTouchCancel, }; struct ActiveTouch { Touch touch; SharedTouchEventEmitter eventEmitter; struct Hasher { size_t operator()(const ActiveTouch &activeTouch) const { return std::hash()(activeTouch.touch.identifier); } }; struct Comparator { bool operator()(const ActiveTouch &lhs, const ActiveTouch &rhs) const { return lhs.touch.identifier == rhs.touch.identifier; } }; }; static void UpdateActiveTouchWithUITouch(ActiveTouch &activeTouch, UITouch *uiTouch, UIView *rootComponentView) { CGPoint offsetPoint = [uiTouch locationInView:uiTouch.view]; CGPoint screenPoint = [uiTouch locationInView:uiTouch.window]; CGPoint pagePoint = [uiTouch locationInView:rootComponentView]; activeTouch.touch.offsetPoint = RCTPointFromCGPoint(offsetPoint); activeTouch.touch.screenPoint = RCTPointFromCGPoint(screenPoint); activeTouch.touch.pagePoint = RCTPointFromCGPoint(pagePoint); activeTouch.touch.timestamp = uiTouch.timestamp; if (RCTForceTouchAvailable()) { activeTouch.touch.force = uiTouch.force / uiTouch.maximumPossibleForce; } } static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponentView) { UIView *componentView = uiTouch.view; ActiveTouch activeTouch = {}; if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { activeTouch.eventEmitter = [(id)componentView touchEventEmitterAtPoint:[uiTouch locationInView:componentView]]; activeTouch.touch.target = (Tag)componentView.tag; } UpdateActiveTouchWithUITouch(activeTouch, uiTouch, rootComponentView); return activeTouch; } static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) { return NO; } } return YES; } static BOOL AnyTouchesChanged(NSSet *touches) { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) { return YES; } } return NO; } /** * Surprisingly, `__unsafe_unretained id` pointers are not regular pointers * and `std::hash<>` cannot hash them. * This is quite trivial but decent implementation of hasher function * inspired by this research: https://stackoverflow.com/a/21062520/496389. */ template struct PointerHasher { constexpr std::size_t operator()(const PointerT &value) const { return reinterpret_cast(&value); } }; @interface RCTSurfaceTouchHandler () @end @implementation RCTSurfaceTouchHandler { std::unordered_map< __unsafe_unretained UITouch *, ActiveTouch, PointerHasher<__unsafe_unretained UITouch *> > _activeTouches; UIView *_rootComponentView; IdentifierPool<11> _identifierPool; } - (instancetype)init { if (self = [super initWithTarget:nil action:nil]) { // `cancelsTouchesInView` and `delaysTouches*` are needed in order // to be used as a top level event delegated recognizer. // Otherwise, lower-level components not built using React Native, // 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]; _rootComponentView = view; } - (void)detachFromView:(UIView *)view { RCTAssertParam(view); RCTAssert(self.view == view, @"RCTTouchHandler attached to another view."); [view removeGestureRecognizer:self]; _rootComponentView = nil; } - (void)_registerTouches:(NSSet *)touches { for (UITouch *touch in touches) { auto activeTouch = CreateTouchWithUITouch(touch, _rootComponentView); activeTouch.touch.identifier = _identifierPool.dequeue(); _activeTouches.emplace(touch, activeTouch); } } - (void)_updateTouches:(NSSet *)touches { for (UITouch *touch in touches) { UpdateActiveTouchWithUITouch(_activeTouches[touch], touch, _rootComponentView); } } - (void)_unregisterTouches:(NSSet *)touches { for (UITouch *touch in touches) { const auto &activeTouch = _activeTouches[touch]; _identifierPool.enqueue(activeTouch.touch.identifier); _activeTouches.erase(touch); } } - (void)_dispatchTouches:(NSSet *)touches eventType:(RCTTouchEventType)eventType { TouchEvent event = {}; std::unordered_set changedActiveTouches = {}; std::unordered_set uniqueEventEmitter = {}; BOOL isEndishEventType = eventType == RCTTouchEventTypeTouchEnd || eventType == RCTTouchEventTypeTouchCancel; for (UITouch *touch in touches) { const auto &activeTouch = _activeTouches[touch]; if (!activeTouch.eventEmitter) { continue; } changedActiveTouches.insert(activeTouch); event.changedTouches.insert(activeTouch.touch); uniqueEventEmitter.insert(activeTouch.eventEmitter); } for (const auto &pair : _activeTouches) { if (!pair.second.eventEmitter) { continue; } if ( isEndishEventType && event.changedTouches.find(pair.second.touch) != event.changedTouches.end() ) { continue; } event.touches.insert(pair.second.touch); } for (const auto &eventEmitter : uniqueEventEmitter) { event.targetTouches.clear(); for (const auto &pair : _activeTouches) { if (pair.second.eventEmitter == eventEmitter) { event.targetTouches.insert(pair.second.touch); } } switch (eventType) { case RCTTouchEventTypeTouchStart: eventEmitter->onTouchStart(event); break; case RCTTouchEventTypeTouchMove: eventEmitter->onTouchMove(event); break; case RCTTouchEventTypeTouchEnd: eventEmitter->onTouchEnd(event); break; case RCTTouchEventTypeTouchCancel: eventEmitter->onTouchCancel(event); break; } } } #pragma mark - `UIResponder`-ish touch-delivery methods - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; [self _registerTouches:touches]; [self _dispatchTouches:touches eventType:RCTTouchEventTypeTouchStart]; if (self.state == UIGestureRecognizerStatePossible) { self.state = UIGestureRecognizerStateBegan; } else if (self.state == UIGestureRecognizerStateBegan) { self.state = UIGestureRecognizerStateChanged; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; [self _updateTouches:touches]; [self _dispatchTouches:touches eventType:RCTTouchEventTypeTouchMove]; self.state = UIGestureRecognizerStateChanged; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self _updateTouches:touches]; [self _dispatchTouches:touches eventType:RCTTouchEventTypeTouchEnd]; [self _unregisterTouches:touches]; if (AllTouchesAreCancelledOrEnded(event.allTouches)) { self.state = UIGestureRecognizerStateEnded; } else if (AnyTouchesChanged(event.allTouches)) { self.state = UIGestureRecognizerStateChanged; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; [self _updateTouches:touches]; [self _dispatchTouches:touches eventType:RCTTouchEventTypeTouchCancel]; [self _unregisterTouches:touches]; if (AllTouchesAreCancelledOrEnded(event.allTouches)) { self.state = UIGestureRecognizerStateCancelled; } else if (AnyTouchesChanged(event.allTouches)) { self.state = UIGestureRecognizerStateChanged; } } - (void)reset { // Technically, `_activeTouches` must be already empty at this point, // but just to be sure, we clear it explicitly. _activeTouches.clear(); _identifierPool.reset(); } - (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]; } #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