From a32be38017fa635152b40ede35d9a5c06f69aa72 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Fri, 8 Jun 2018 20:16:19 -0700 Subject: [PATCH] Fabric: Introducing RCTSurfaceTouchHandler Summary: RCTSurfaceTouchHandler is a complete rewrite of RCTTouchHandler which uses direct Fabric-specific event dispatching pipeline and several new approaches to managing active events (such as high-performant C++ collections, better management of identifier pool, and so on). Besides that, the new implementation is much more W3C compliant that it used to be (see old TODOs near `receiveTouches()` implementation in Javascript). So, touch events work now! Reviewed By: fkgozali Differential Revision: D8246713 fbshipit-source-id: 218dc15cd8f982237de7e2497ff36a7bfe6d37cc --- .../View/RCTViewComponentView.mm | 5 + React/Fabric/RCTSurfaceTouchHandler.h | 19 + React/Fabric/RCTSurfaceTouchHandler.mm | 360 ++++++++++++++++++ React/Fabric/Surface/RCTFabricSurface.mm | 7 +- 4 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 React/Fabric/RCTSurfaceTouchHandler.h create mode 100644 React/Fabric/RCTSurfaceTouchHandler.mm diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 189850df9..fb688d031 100644 --- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -67,4 +67,9 @@ using namespace facebook::react; return YES; } +- (SharedEventHandlers)touchEventHandlers +{ + return _eventHandlers; +} + @end diff --git a/React/Fabric/RCTSurfaceTouchHandler.h b/React/Fabric/RCTSurfaceTouchHandler.h new file mode 100644 index 000000000..670ef2848 --- /dev/null +++ b/React/Fabric/RCTSurfaceTouchHandler.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTSurfaceTouchHandler : UIGestureRecognizer + +- (void)attachToView:(UIView *)view; +- (void)detachFromView:(UIView *)view; + +@end + +NS_ASSUME_NONNULL_END diff --git a/React/Fabric/RCTSurfaceTouchHandler.mm b/React/Fabric/RCTSurfaceTouchHandler.mm new file mode 100644 index 000000000..09f586646 --- /dev/null +++ b/React/Fabric/RCTSurfaceTouchHandler.mm @@ -0,0 +1,360 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * 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 + +#import "RCTConversions.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; +}; + +@protocol RCTTouchableComponentViewProtocol + - (SharedViewEventHandlers)touchEventHandlers; +@end + +typedef NS_ENUM(NSInteger, RCTTouchEventType) { + RCTTouchEventTypeTouchStart, + RCTTouchEventTypeTouchMove, + RCTTouchEventTypeTouchEnd, + RCTTouchEventTypeTouchCancel, +}; + +struct ActiveTouch { + Touch touch; + SharedViewEventHandlers eventHandlers; + + 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(touchEventHandlers)]) { + activeTouch.eventHandlers = [(id)componentView touchEventHandlers]; + 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) { + 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 uniqueEventHandlers = {}; + BOOL isEndishEventType = eventType == RCTTouchEventTypeTouchEnd || eventType == RCTTouchEventTypeTouchCancel; + + for (UITouch *touch in touches) { + auto &&activeTouch = _activeTouches[touch]; + + if (!activeTouch.eventHandlers) { + continue; + } + + changedActiveTouches.insert(activeTouch); + event.changedTouches.insert(activeTouch.touch); + uniqueEventHandlers.insert(activeTouch.eventHandlers); + } + + for (auto &&pair : _activeTouches) { + if (!pair.second.eventHandlers) { + continue; + } + + if ( + isEndishEventType && + event.changedTouches.find(pair.second.touch) != event.changedTouches.end() + ) { + continue; + } + + event.touches.insert(pair.second.touch); + } + + for (auto &&eventHandlers : uniqueEventHandlers) { + event.targetTouches.clear(); + + for (auto &&pair : _activeTouches) { + if (pair.second.eventHandlers == eventHandlers) { + event.targetTouches.insert(pair.second.touch); + } + } + + switch (eventType) { + case RCTTouchEventTypeTouchStart: + eventHandlers->onTouchStart(event); + break; + case RCTTouchEventTypeTouchMove: + eventHandlers->onTouchMove(event); + break; + case RCTTouchEventTypeTouchEnd: + eventHandlers->onTouchEnd(event); + break; + case RCTTouchEventTypeTouchCancel: + eventHandlers->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 diff --git a/React/Fabric/Surface/RCTFabricSurface.mm b/React/Fabric/Surface/RCTFabricSurface.mm index 3028a204e..2e06d361d 100644 --- a/React/Fabric/Surface/RCTFabricSurface.mm +++ b/React/Fabric/Surface/RCTFabricSurface.mm @@ -17,7 +17,7 @@ #import #import #import -#import +#import #import #import @@ -39,7 +39,7 @@ // The Main thread only RCTSurfaceView *_Nullable _view; - RCTTouchHandler *_Nullable _touchHandler; + RCTSurfaceTouchHandler *_Nullable _touchHandler; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -71,6 +71,8 @@ _stage = RCTSurfaceStageSurfaceDidInitialize; + _touchHandler = [RCTSurfaceTouchHandler new]; + [self _run]; } @@ -102,6 +104,7 @@ if (!_view) { _view = [[RCTSurfaceView alloc] initWithSurface:(RCTSurface *)self]; + [_touchHandler attachToView:_view]; } return _view;