-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
d01290d
commit a32be38
Showing
4 changed files
with
389 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <UIKit/UIKit.h> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@interface RCTSurfaceTouchHandler : UIGestureRecognizer | ||
|
||
- (void)attachToView:(UIView *)view; | ||
- (void)detachFromView:(UIView *)view; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <UIKit/UIGestureRecognizerSubclass.h> | ||
#import <fabric/view/ViewEventHandlers.h> | ||
#import <React/RCTUtils.h> | ||
#import <React/RCTViewComponentView.h> | ||
|
||
#import "RCTConversions.h" | ||
|
||
using namespace facebook::react; | ||
|
||
template <size_t size> | ||
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 <NSObject> | ||
- (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<decltype(activeTouch.touch.identifier)>()(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<RCTTouchableComponentViewProtocol>)componentView touchEventHandlers]; | ||
activeTouch.touch.target = (Tag)componentView.tag; | ||
} | ||
|
||
UpdateActiveTouchWithUITouch(activeTouch, uiTouch, rootComponentView); | ||
return activeTouch; | ||
} | ||
|
||
static BOOL AllTouchesAreCancelledOrEnded(NSSet<UITouch *> *touches) { | ||
for (UITouch *touch in touches) { | ||
if (touch.phase == UITouchPhaseBegan || | ||
touch.phase == UITouchPhaseMoved || | ||
touch.phase == UITouchPhaseStationary) { | ||
return NO; | ||
} | ||
} | ||
return YES; | ||
} | ||
|
||
static BOOL AnyTouchesChanged(NSSet<UITouch *> *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<typename PointerT> | ||
struct PointerHasher { | ||
constexpr std::size_t operator()(const PointerT &value) const { | ||
return reinterpret_cast<size_t>(&value); | ||
} | ||
}; | ||
|
||
@interface RCTSurfaceTouchHandler () <UIGestureRecognizerDelegate> | ||
@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<UITouch *> *)touches | ||
{ | ||
for (UITouch *touch in touches) { | ||
auto &&activeTouch = CreateTouchWithUITouch(touch, _rootComponentView); | ||
activeTouch.touch.identifier = _identifierPool.dequeue(); | ||
_activeTouches.emplace(touch, activeTouch); | ||
} | ||
} | ||
|
||
- (void)_updateTouches:(NSSet<UITouch *> *)touches | ||
{ | ||
for (UITouch *touch in touches) { | ||
UpdateActiveTouchWithUITouch(_activeTouches[touch], touch, _rootComponentView); | ||
} | ||
} | ||
|
||
- (void)_unregisterTouches:(NSSet<UITouch *> *)touches | ||
{ | ||
for (UITouch *touch in touches) { | ||
auto &&activeTouch = _activeTouches[touch]; | ||
_identifierPool.enqueue(activeTouch.touch.identifier); | ||
_activeTouches.erase(touch); | ||
} | ||
} | ||
|
||
- (void)_dispatchTouches:(NSSet<UITouch *> *)touches eventType:(RCTTouchEventType)eventType | ||
{ | ||
TouchEvent event = {}; | ||
std::unordered_set<ActiveTouch, ActiveTouch::Hasher, ActiveTouch::Comparator> changedActiveTouches = {}; | ||
std::unordered_set<SharedViewEventHandlers> 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<UITouch *> *)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<UITouch *> *)touches withEvent:(UIEvent *)event | ||
{ | ||
[super touchesMoved:touches withEvent:event]; | ||
|
||
[self _updateTouches:touches]; | ||
[self _dispatchTouches:touches eventType:RCTTouchEventTypeTouchMove]; | ||
|
||
self.state = UIGestureRecognizerStateChanged; | ||
} | ||
|
||
- (void)touchesEnded:(NSSet<UITouch *> *)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<UITouch *> *)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 |
Oops, something went wrong.