Skip to content

Commit

Permalink
Fabric: Introducing RCTSurfaceTouchHandler
Browse files Browse the repository at this point in the history
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
shergin authored and facebook-github-bot committed Jun 9, 2018
1 parent d01290d commit a32be38
Show file tree
Hide file tree
Showing 4 changed files with 389 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,9 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti
return YES;
}

- (SharedEventHandlers)touchEventHandlers
{
return _eventHandlers;
}

@end
19 changes: 19 additions & 0 deletions React/Fabric/RCTSurfaceTouchHandler.h
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
360 changes: 360 additions & 0 deletions React/Fabric/RCTSurfaceTouchHandler.mm
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
Loading

0 comments on commit a32be38

Please sign in to comment.