Files
react-native/React/Fabric/RCTSurfaceTouchHandler.mm
T
Vincent Riemer d5f40a9153 Fix MoveAcross pointer event test for iOS
Summary:
Changelog: [iOS][Internal] - Get the "move across" pointer event test passing on iOS

After implementing the "MoveAcross" pointer event test it indicated that the implementation, at least on iOS, was firing the events in an incorrect order. Extrapolating from the test's expectations all the events related to entering/leaving/ect. should fire before the down/move/up instead of placing those down/move/up/ect. between the leave and enter events.

Reviewed By: lunaleaps

Differential Revision: D39221372

fbshipit-source-id: 31b7292e524a9ec9d998e658b7d0ef3ebc825d68
2022-10-07 12:07:19 -07:00

991 lines
33 KiB
Plaintext

/*
* Copyright (c) Meta Platforms, Inc. and 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 <React/RCTReactTaggedView.h>
#import <React/RCTUtils.h>
#import <React/RCTViewComponentView.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#import "RCTConversions.h"
#import "RCTTouchableComponentViewProtocol.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;
};
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
RCTTouchEventTypeTouchStart,
RCTTouchEventTypeTouchMove,
RCTTouchEventTypeTouchEnd,
RCTTouchEventTypeTouchCancel,
};
struct ActiveTouch {
Touch touch;
SharedTouchEventEmitter eventEmitter;
/*
* The type of touch received.
*/
UITouchType touchType;
/*
* The radius (in points) of the touch.
*/
CGFloat majorRadius;
/*
* The altitude (in radians) of the stylus.
*/
CGFloat altitudeAngle;
/*
* The azimuth angle (in radians) of the stylus.
*/
CGFloat azimuthAngle;
/*
* The button mask of the touch
*/
UIEventButtonMask buttonMask;
/*
* The bit mask of modifier flags in the gesture represented by the receiver.
*/
UIKeyModifierFlags modifierFlags;
/*
* Indicates if the active touch represents the primary pointer of this pointer type.
*/
bool isPrimary;
/*
* The button number that was pressed (if applicable) when the event was fired.
*/
int button;
/*
* Informs the event system that when the touch is released it should be treated as the
* pointer leaving the screen entirely.
*/
bool shouldLeaveWhenReleased;
/*
* A component view on which the touch was begun.
*/
__strong UIView<RCTComponentViewProtocol> *componentView = nil;
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;
}
};
};
// Mouse and Pen pointers get reserved IDs so they stay consistent no matter the order
// at which events come in
static int const kMousePointerId = 0;
static int const kPencilPointerId = 1;
// If a new reserved ID is added above this should be incremented to ensure touch events
// do not conflict
static int const kTouchIdentifierPoolOffset = 2;
// Returns a CGPoint which represents the tiltX/Y values (in RADIANS)
// Adapted from https://gist.github.com/k3a/2903719bb42b48c9198d20c2d6f73ac1
static CGPoint SphericalToTilt(CGFloat altitudeAngleRad, CGFloat azimuthAngleRad)
{
if (altitudeAngleRad == M_PI / 2.0) {
return CGPointMake(0.0, 0.0);
} else if (altitudeAngleRad == 0.0) {
// when pen is laying on the pad it is impossible to precisely encode but at least approximate for 4 cases
if (azimuthAngleRad > 7.0 * M_PI / 4.0 || azimuthAngleRad <= M_PI / 4.0) {
// for azimuthRad == 0, the pen is on the positive Y axis
return CGPointMake(0.0, M_PI / 2.0);
} else if (azimuthAngleRad > M_PI / 4.0 && azimuthAngleRad <= 3 * M_PI / 4.0) {
// for azimuthRad == math.pi/2 the pen is on the positive X axis
return CGPointMake(M_PI / 2.0, 0.0);
} else if (azimuthAngleRad > 3.0 * M_PI / 4.0 && azimuthAngleRad <= 5.0 * M_PI / 4.0) {
// for azimuthRad == math.pi, the pen is on the negative Y axis
return CGPointMake(0.0, -M_PI / 2.0);
} else if (azimuthAngleRad > 5.0 * M_PI / 4.0 && azimuthAngleRad <= 7.0 * M_PI / 4.0) {
// for azimuthRad == math.pi + math.pi/2 pen on negative X axis
return CGPointMake(-M_PI / 2.0, 0.0);
}
}
CGFloat tanAlt = tan(altitudeAngleRad); // tan(x) = sin(x)/cos(x)
CGFloat tiltXrad = atan(sin(azimuthAngleRad) / tanAlt);
CGFloat tiltYrad = atan(cos(azimuthAngleRad) / tanAlt);
return CGPointMake(tiltXrad, tiltYrad);
}
static CGFloat RadsToDegrees(CGFloat rads)
{
return rads * 180 / M_PI;
}
static int ButtonMaskToButtons(UIEventButtonMask buttonMask)
{
int buttonsMaskResult = 0;
if (@available(iOS 13.4, *)) {
if ((buttonMask & UIEventButtonMaskPrimary) != 0) {
buttonsMaskResult |= 1;
}
if ((buttonMask & UIEventButtonMaskSecondary) != 0) {
buttonsMaskResult |= 2;
}
// undocumented mask value which represents the "auxiliary button" (i.e. middle mouse button)
if ((buttonMask & 0x4) != 0) {
buttonsMaskResult |= 4;
}
}
return buttonsMaskResult;
}
static int ButtonMaskDiffToButton(UIEventButtonMask prevButtonMask, UIEventButtonMask curButtonMask)
{
if (@available(iOS 13.4, *)) {
if ((prevButtonMask & UIEventButtonMaskPrimary) != (curButtonMask & UIEventButtonMaskPrimary)) {
return 0;
}
if ((prevButtonMask & 0x4) != (curButtonMask & 0x4)) {
return 1;
}
if ((prevButtonMask & UIEventButtonMaskSecondary) != (curButtonMask & UIEventButtonMaskSecondary)) {
return 2;
}
}
return -1;
}
static void UpdateActiveTouchWithUITouch(
ActiveTouch &activeTouch,
UITouch *uiTouch,
UIEvent *uiEvent,
UIView *rootComponentView,
CGPoint rootViewOriginOffset)
{
CGPoint offsetPoint = [uiTouch locationInView:activeTouch.componentView];
CGPoint pagePoint = [uiTouch locationInView:rootComponentView];
CGPoint screenPoint = [rootComponentView convertPoint:pagePoint
toCoordinateSpace:rootComponentView.window.screen.coordinateSpace];
pagePoint = CGPointMake(pagePoint.x + rootViewOriginOffset.x, pagePoint.y + rootViewOriginOffset.y);
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 = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce);
}
activeTouch.touchType = uiTouch.type;
activeTouch.majorRadius = uiTouch.majorRadius;
activeTouch.altitudeAngle = uiTouch.altitudeAngle;
activeTouch.azimuthAngle = [uiTouch azimuthAngleInView:nil];
if (@available(iOS 13.4, *)) {
UIEventButtonMask nextButtonMask = 0;
if (uiTouch.phase != UITouchPhaseEnded) {
nextButtonMask = uiTouch.type == UITouchTypeIndirectPointer ? uiEvent.buttonMask : 1;
}
activeTouch.button = ButtonMaskDiffToButton(activeTouch.buttonMask, nextButtonMask);
activeTouch.buttonMask = nextButtonMask;
activeTouch.modifierFlags = uiEvent.modifierFlags;
} else {
activeTouch.button = 0;
activeTouch.buttonMask = 0;
activeTouch.modifierFlags = 0;
}
}
static ActiveTouch
CreateTouchWithUITouch(UITouch *uiTouch, UIEvent *uiEvent, UIView *rootComponentView, CGPoint rootViewOriginOffset)
{
ActiveTouch activeTouch = {};
// Find closest Fabric-managed touchable view
UIView *componentView = uiTouch.view;
while (componentView) {
if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
activeTouch.eventEmitter = [(id<RCTTouchableComponentViewProtocol>)componentView
touchEventEmitterAtPoint:[uiTouch locationInView:componentView]];
activeTouch.touch.target = (Tag)componentView.tag;
activeTouch.componentView = componentView;
break;
}
componentView = componentView.superview;
}
UpdateActiveTouchWithUITouch(activeTouch, uiTouch, uiEvent, rootComponentView, rootViewOriginOffset);
return activeTouch;
}
static UIView *FindClosestFabricManagedTouchableView(UIView *componentView)
{
while (componentView) {
if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
return componentView;
}
componentView = componentView.superview;
}
return nil;
}
static NSOrderedSet<RCTReactTaggedView *> *GetTouchableViewsInPathToRoot(UIView *componentView)
{
NSMutableOrderedSet *results = [NSMutableOrderedSet orderedSet];
do {
if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
[results addObject:[RCTReactTaggedView wrap:componentView]];
}
componentView = componentView.superview;
} while (componentView);
return results;
}
static SharedTouchEventEmitter GetTouchEmitterFromView(UIView *componentView, CGPoint point)
{
return [(id<RCTTouchableComponentViewProtocol>)componentView touchEventEmitterAtPoint:point];
}
static const char *PointerTypeCStringFromUITouchType(UITouchType type)
{
switch (type) {
case UITouchTypeDirect:
return "touch";
case UITouchTypePencil:
return "pen";
case UITouchTypeIndirectPointer:
return "mouse";
case UITouchTypeIndirect:
default:
return "";
}
}
static void UpdatePointerEventModifierFlags(PointerEvent &event, UIKeyModifierFlags flags)
{
if (@available(iOS 13.4, *)) {
event.ctrlKey = (flags & UIKeyModifierControl) != 0;
event.shiftKey = (flags & UIKeyModifierShift) != 0;
event.altKey = (flags & UIKeyModifierAlternate) != 0;
event.metaKey = (flags & UIKeyModifierCommand) != 0;
} else {
event.ctrlKey = false;
event.shiftKey = false;
event.altKey = false;
event.metaKey = false;
}
}
static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch, RCTTouchEventType eventType)
{
Touch touch = activeTouch.touch;
PointerEvent event = {};
event.pointerId = touch.identifier;
event.pointerType = PointerTypeCStringFromUITouchType(activeTouch.touchType);
event.clientPoint = touch.pagePoint;
event.screenPoint = touch.screenPoint;
event.offsetPoint = touch.offsetPoint;
event.pressure = touch.force;
if (@available(iOS 13.4, *)) {
if (activeTouch.touchType == UITouchTypeIndirectPointer) {
// pointer events with a mouse button pressed should report a pressure of 0.5
// when the touch is down and 0.0 when it is lifted regardless of how it is reported by the OS
event.pressure = eventType != RCTTouchEventTypeTouchEnd ? 0.5 : 0.0;
}
}
CGFloat pointerSize = activeTouch.majorRadius * 2.0;
if (@available(iOS 13.4, *)) {
if (activeTouch.touchType == UITouchTypeIndirectPointer) {
// mouse type pointers should always report a size of 1
pointerSize = 1.0;
}
}
event.width = pointerSize;
event.height = pointerSize;
CGPoint tilt = SphericalToTilt(activeTouch.altitudeAngle, activeTouch.azimuthAngle);
event.tiltX = RadsToDegrees(tilt.x);
event.tiltY = RadsToDegrees(tilt.y);
event.detail = 0;
event.button = activeTouch.button;
event.buttons = ButtonMaskToButtons(activeTouch.buttonMask);
UpdatePointerEventModifierFlags(event, activeTouch.modifierFlags);
event.tangentialPressure = 0.0;
event.twist = 0;
event.isPrimary = activeTouch.isPrimary;
return event;
}
static PointerEvent CreatePointerEventFromIncompleteHoverData(
CGPoint clientLocation,
CGPoint screenLocation,
CGPoint offsetLocation,
UIKeyModifierFlags modifierFlags)
{
PointerEvent event = {};
// "touch" events produced from a mouse cursor on iOS always have the ID 0 so
// we can just assume that here since these sort of hover events only ever come
// from the mouse
event.pointerId = kMousePointerId;
event.pressure = 0.0;
event.pointerType = "mouse";
event.clientPoint = RCTPointFromCGPoint(clientLocation);
event.screenPoint = RCTPointFromCGPoint(screenLocation);
event.offsetPoint = RCTPointFromCGPoint(offsetLocation);
event.width = 1.0;
event.height = 1.0;
event.tiltX = 0;
event.tiltY = 0;
event.detail = 0;
event.button = -1;
event.buttons = 0;
UpdatePointerEventModifierFlags(event, modifierFlags);
event.tangentialPressure = 0.0;
event.twist = 0;
event.isPrimary = true;
return event;
}
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;
}
static BOOL IsViewListeningToEvent(RCTReactTaggedView *taggedView, ViewEvents::Offset eventType)
{
UIView *view = taggedView.view;
if (view && [view.class conformsToProtocol:@protocol(RCTComponentViewProtocol)]) {
auto props = ((id<RCTComponentViewProtocol>)view).props;
if (SharedViewProps viewProps = std::dynamic_pointer_cast<ViewProps const>(props)) {
return viewProps->events[eventType];
}
}
return NO;
}
static BOOL IsAnyViewInPathListeningToEvent(NSOrderedSet<RCTReactTaggedView *> *viewPath, ViewEvents::Offset eventType)
{
for (RCTReactTaggedView *taggedView in viewPath) {
if (IsViewListeningToEvent(taggedView, eventType)) {
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;
/*
* We hold the view weakly to prevent a retain cycle.
*/
__weak UIView *_rootComponentView;
IdentifierPool<11> _identifierPool;
UIHoverGestureRecognizer *_hoverRecognizer API_AVAILABLE(ios(13.0));
NSMutableDictionary<NSNumber *, NSOrderedSet<RCTReactTaggedView *> *> *_currentlyHoveredViewsPerPointer;
int _primaryTouchPointerId;
}
- (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;
_hoverRecognizer = nil;
_currentlyHoveredViewsPerPointer = [[NSMutableDictionary alloc] init];
_primaryTouchPointerId = -1;
}
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;
if (RCTGetDispatchW3CPointerEvents()) {
if (@available(iOS 13.0, *)) {
_hoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hovering:)];
[view addGestureRecognizer:_hoverRecognizer];
}
}
}
- (void)detachFromView:(UIView *)view
{
RCTAssertParam(view);
RCTAssert(self.view == view, @"RCTTouchHandler attached to another view.");
[view removeGestureRecognizer:self];
_rootComponentView = nil;
if (_hoverRecognizer != nil) {
[view removeGestureRecognizer:_hoverRecognizer];
_hoverRecognizer = nil;
}
}
- (void)_registerTouches:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
auto activeTouch = CreateTouchWithUITouch(touch, event, _rootComponentView, _viewOriginOffset);
if (@available(iOS 13.4, *)) {
switch (touch.type) {
case UITouchTypeIndirectPointer:
activeTouch.touch.identifier = kMousePointerId;
activeTouch.isPrimary = true;
break;
case UITouchTypePencil:
activeTouch.touch.identifier = kPencilPointerId;
activeTouch.isPrimary = true;
break;
default:
// use the identifier pool offset to ensure no conflicts between the reserved IDs and the
// touch IDs
activeTouch.touch.identifier = _identifierPool.dequeue() + kTouchIdentifierPoolOffset;
if (_primaryTouchPointerId == -1) {
_primaryTouchPointerId = activeTouch.touch.identifier;
activeTouch.isPrimary = true;
}
break;
}
} else {
activeTouch.touch.identifier = _identifierPool.dequeue();
if (_primaryTouchPointerId == -1) {
_primaryTouchPointerId = activeTouch.touch.identifier;
activeTouch.isPrimary = true;
}
}
// If the pointer has not been marked as hovering over views before the touch started, we register
// that the activeTouch should not maintain its hovered state once the pointer has been lifted.
auto currentlyHoveredViews = [_currentlyHoveredViewsPerPointer objectForKey:@(activeTouch.touch.identifier)];
if (currentlyHoveredViews == nil || [currentlyHoveredViews count] == 0) {
activeTouch.shouldLeaveWhenReleased = YES;
}
_activeTouches.emplace(touch, activeTouch);
}
}
- (void)_updateTouches:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
auto iterator = _activeTouches.find(touch);
assert(iterator != _activeTouches.end() && "Inconsistency between local and UIKit touch registries");
if (iterator == _activeTouches.end()) {
continue;
}
UpdateActiveTouchWithUITouch(iterator->second, touch, event, _rootComponentView, _viewOriginOffset);
}
}
- (void)_unregisterTouches:(NSSet<UITouch *> *)touches
{
for (UITouch *touch in touches) {
auto iterator = _activeTouches.find(touch);
assert(iterator != _activeTouches.end() && "Inconsistency between local and UIKit touch registries");
if (iterator == _activeTouches.end()) {
continue;
}
auto &activeTouch = iterator->second;
if (activeTouch.touch.identifier == _primaryTouchPointerId) {
_primaryTouchPointerId = -1;
}
if (@available(iOS 13.4, *)) {
// only need to enqueue if the touch type isn't one with a reserved identifier
switch (touch.type) {
case UITouchTypeIndirectPointer:
case UITouchTypePencil:
break;
default:
// since the touch's identifier has been offset we need to re-normalize it to 0-based
// which is what the identifier pool expects
_identifierPool.enqueue(activeTouch.touch.identifier - kTouchIdentifierPoolOffset);
}
} else {
_identifierPool.enqueue(activeTouch.touch.identifier);
}
_activeTouches.erase(touch);
}
}
- (std::vector<ActiveTouch>)_activeTouchesFromTouches:(NSSet<UITouch *> *)touches
{
std::vector<ActiveTouch> activeTouches;
activeTouches.reserve(touches.count);
for (UITouch *touch in touches) {
auto iterator = _activeTouches.find(touch);
assert(iterator != _activeTouches.end() && "Inconsistency between local and UIKit touch registries");
if (iterator == _activeTouches.end()) {
continue;
}
activeTouches.push_back(iterator->second);
}
return activeTouches;
}
- (void)_dispatchActiveTouches:(std::vector<ActiveTouch>)activeTouches eventType:(RCTTouchEventType)eventType
{
TouchEvent event = {};
std::unordered_set<ActiveTouch, ActiveTouch::Hasher, ActiveTouch::Comparator> changedActiveTouches = {};
std::unordered_set<SharedTouchEventEmitter> uniqueEventEmitters = {};
BOOL isEndishEventType = eventType == RCTTouchEventTypeTouchEnd || eventType == RCTTouchEventTypeTouchCancel;
for (const auto &activeTouch : activeTouches) {
if (!activeTouch.eventEmitter) {
continue;
}
changedActiveTouches.insert(activeTouch);
event.changedTouches.insert(activeTouch.touch);
uniqueEventEmitters.insert(activeTouch.eventEmitter);
// emit w3c pointer events
if (RCTGetDispatchW3CPointerEvents()) {
PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(activeTouch, eventType);
if ((eventType == RCTTouchEventTypeTouchEnd && activeTouch.shouldLeaveWhenReleased)) {
activeTouch.eventEmitter->onPointerUp(pointerEvent);
[self handleIncomingPointerEvent:pointerEvent onView:nil];
} else {
CGPoint clientLocation = CGPointMake(pointerEvent.clientPoint.x, pointerEvent.clientPoint.y);
UIView *targetView = FindClosestFabricManagedTouchableView([_rootComponentView hitTest:clientLocation
withEvent:nil]);
NSOrderedSet<RCTReactTaggedView *> *eventPathViews = [self handleIncomingPointerEvent:pointerEvent
onView:targetView];
switch (eventType) {
case RCTTouchEventTypeTouchStart:
activeTouch.eventEmitter->onPointerDown(pointerEvent);
break;
case RCTTouchEventTypeTouchMove: {
bool hasMoveEventListeners =
IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) ||
IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture);
if (hasMoveEventListeners) {
activeTouch.eventEmitter->onPointerMove(pointerEvent);
}
break;
}
case RCTTouchEventTypeTouchEnd:
activeTouch.eventEmitter->onPointerUp(pointerEvent);
break;
case RCTTouchEventTypeTouchCancel:
activeTouch.eventEmitter->onPointerCancel(pointerEvent);
break;
}
}
}
}
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 : uniqueEventEmitters) {
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<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self _registerTouches:touches withEvent:event];
[self _dispatchActiveTouches:[self _activeTouchesFromTouches: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 withEvent:event];
[self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchMove];
self.state = UIGestureRecognizerStateChanged;
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self _updateTouches:touches withEvent:event];
[self _dispatchActiveTouches:[self _activeTouchesFromTouches: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 withEvent:event];
[self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchCancel];
[self _unregisterTouches:touches];
if (AllTouchesAreCancelledOrEnded(event.allTouches)) {
self.state = UIGestureRecognizerStateCancelled;
} else if (AnyTouchesChanged(event.allTouches)) {
self.state = UIGestureRecognizerStateChanged;
}
}
- (void)reset
{
[super reset];
if (!_activeTouches.empty()) {
std::vector<ActiveTouch> activeTouches;
activeTouches.reserve(_activeTouches.size());
for (auto const &pair : _activeTouches) {
activeTouches.push_back(pair.second);
}
[self _dispatchActiveTouches:activeTouches eventType:RCTTouchEventTypeTouchCancel];
// Force-unregistering all the touches.
_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];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
BOOL canBePrevented = [self canBePreventedByGestureRecognizer:otherGestureRecognizer];
if (canBePrevented) {
[self _cancelTouches];
}
return NO;
}
#pragma mark -
- (void)_cancelTouches
{
[self setEnabled:NO];
[self setEnabled:YES];
}
- (void)hovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0))
{
UIView *listenerView = recognizer.view;
CGPoint clientLocation = [recognizer locationInView:listenerView];
CGPoint screenLocation = [listenerView convertPoint:clientLocation
toCoordinateSpace:listenerView.window.screen.coordinateSpace];
UIView *targetView = [listenerView hitTest:clientLocation withEvent:nil];
targetView = FindClosestFabricManagedTouchableView(targetView);
CGPoint offsetLocation = [recognizer locationInView:targetView];
UIKeyModifierFlags modifierFlags;
if (@available(iOS 13.4, *)) {
modifierFlags = recognizer.modifierFlags;
} else {
modifierFlags = 0;
}
PointerEvent event =
CreatePointerEventFromIncompleteHoverData(clientLocation, screenLocation, offsetLocation, modifierFlags);
NSOrderedSet<RCTReactTaggedView *> *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView];
SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation);
bool hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) ||
IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture);
if (eventEmitter != nil && hasMoveEventListeners) {
eventEmitter->onPointerMove(event);
}
}
/**
* Private method which is used for tracking the location of pointer events to manage the entering/leaving events.
* The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down,
* move, and up — and they should all be treated the same when it comes to tracking the entering & leaving of pointers
* to views. This method accomplishes that by recieving the pointer event, the target view (can be null in cases when
* the event indicates that the pointer has left the screen entirely), and a block/callback where the underlying event
* should be fired.
*/
- (NSOrderedSet<RCTReactTaggedView *> *)handleIncomingPointerEvent:(PointerEvent)event
onView:(nullable UIView *)targetView
{
int pointerId = event.pointerId;
CGPoint clientLocation = CGPointMake(event.clientPoint.x, event.clientPoint.y);
NSOrderedSet<RCTReactTaggedView *> *currentlyHoveredViews =
[_currentlyHoveredViewsPerPointer objectForKey:@(pointerId)];
if (currentlyHoveredViews == nil) {
currentlyHoveredViews = [NSOrderedSet orderedSet];
}
RCTReactTaggedView *targetTaggedView = [RCTReactTaggedView wrap:targetView];
RCTReactTaggedView *prevTargetTaggedView = [currentlyHoveredViews firstObject];
UIView *prevTargetView = prevTargetTaggedView.view;
NSOrderedSet<RCTReactTaggedView *> *eventPathViews = GetTouchableViewsInPathToRoot(targetView);
// Out
if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut);
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]);
if (shouldEmitOutEvent && eventEmitter != nil) {
eventEmitter->onPointerOut(event);
}
}
// Leaving
// pointerleave events need to be emited from the deepest target to the root but
// we also need to efficiently keep track of if a view has a parent which is listening to the leave events,
// so we first iterate from the root to the target, collecting the views which need events fired for, of which
// we reverse iterate (now from target to root), actually emitting the events.
NSMutableOrderedSet<UIView *> *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet];
BOOL hasParentLeaveListener = NO;
for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) {
UIView *componentView = taggedView.view;
BOOL shouldEmitEvent = componentView != nil &&
(hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave));
if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) {
[viewsToEmitLeaveEventsTo addObject:componentView];
}
if (shouldEmitEvent && !hasParentLeaveListener) {
hasParentLeaveListener = YES;
}
}
for (UIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
if (eventEmitter != nil) {
eventEmitter->onPointerLeave(event);
}
}
// Over
if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) {
BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver);
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]);
if (shouldEmitOverEvent && eventEmitter != nil) {
eventEmitter->onPointerOver(event);
}
}
// Entering
// We only want to emit events to JS if there is a view that is currently listening to said event
// so we only send those event to the JS side if the element which has been entered is itself listening,
// or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability
// for native to distingusih between capturing listeners and not could be an optimization to futher reduce the number
// of events we send to JS
BOOL hasParentEnterListener = NO;
for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) {
UIView *componentView = taggedView.view;
BOOL shouldEmitEvent = componentView != nil &&
(hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter));
if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) {
SharedTouchEventEmitter eventEmitter =
GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]);
if (eventEmitter != nil) {
eventEmitter->onPointerEnter(event);
}
}
if (shouldEmitEvent && !hasParentEnterListener) {
hasParentEnterListener = YES;
}
}
[_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)];
return eventPathViews;
}
@end