|
|
|
|
@@ -14,10 +14,42 @@
|
|
|
|
|
// TODO: this class behaves a lot like a module, and could be implemented as a
|
|
|
|
|
// module if we were to assume that modules and RootViews had a 1:1 relationship
|
|
|
|
|
|
|
|
|
|
@interface RCTTouchEvent : NSObject
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, assign, readonly) NSUInteger id;
|
|
|
|
|
@property (nonatomic, copy, readonly) NSString *eventName;
|
|
|
|
|
@property (nonatomic, copy, readonly) NSArray *touches;
|
|
|
|
|
@property (nonatomic, copy, readonly) NSArray *changedIndexes;
|
|
|
|
|
@property (nonatomic, assign, readonly) CFTimeInterval originatingTime;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@implementation RCTTouchEvent
|
|
|
|
|
|
|
|
|
|
+ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime
|
|
|
|
|
{
|
|
|
|
|
RCTTouchEvent *touchEvent = [[self alloc] init];
|
|
|
|
|
touchEvent->_id = [self newID];
|
|
|
|
|
touchEvent->_eventName = [eventName copy];
|
|
|
|
|
touchEvent->_touches = [touches copy];
|
|
|
|
|
touchEvent->_changedIndexes = [changedIndexes copy];
|
|
|
|
|
touchEvent->_originatingTime = originatingTime;
|
|
|
|
|
return touchEvent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (NSUInteger)newID
|
|
|
|
|
{
|
|
|
|
|
static NSUInteger id = 0;
|
|
|
|
|
return ++id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
@implementation RCTTouchHandler
|
|
|
|
|
{
|
|
|
|
|
__weak RCTBridge *_bridge;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Arrays managed in parallel tracking native touch object along with the
|
|
|
|
|
* native view that was touched, and the react touch data dictionary.
|
|
|
|
|
@@ -27,6 +59,12 @@
|
|
|
|
|
NSMutableOrderedSet *_nativeTouches;
|
|
|
|
|
NSMutableArray *_reactTouches;
|
|
|
|
|
NSMutableArray *_touchViews;
|
|
|
|
|
|
|
|
|
|
BOOL _recordingInteractionTiming;
|
|
|
|
|
CFTimeInterval _mostRecentEnqueueJS;
|
|
|
|
|
CADisplayLink *_displayLink;
|
|
|
|
|
NSMutableArray *_pendingTouches;
|
|
|
|
|
NSMutableArray *_bridgeInteractionTiming;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithTarget:(id)target action:(SEL)action
|
|
|
|
|
@@ -37,15 +75,21 @@
|
|
|
|
|
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
|
|
|
|
{
|
|
|
|
|
if ((self = [super initWithTarget:nil action:NULL])) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RCTAssert(bridge != nil, @"Expect an event dispatcher");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_bridge = bridge;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_nativeTouches = [[NSMutableOrderedSet alloc] init];
|
|
|
|
|
_reactTouches = [[NSMutableArray alloc] init];
|
|
|
|
|
_touchViews = [[NSMutableArray alloc] init];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)];
|
|
|
|
|
_pendingTouches = [[NSMutableArray alloc] init];
|
|
|
|
|
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
|
|
|
|
|
|
|
|
|
|
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
|
|
|
|
|
|
|
|
// `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower
|
|
|
|
|
// level components not build using RCT, will fail to recognize gestures.
|
|
|
|
|
self.cancelsTouchesInView = NO;
|
|
|
|
|
@@ -53,6 +97,17 @@
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)isValid
|
|
|
|
|
{
|
|
|
|
|
return _displayLink != nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)invalidate
|
|
|
|
|
{
|
|
|
|
|
[_displayLink invalidate];
|
|
|
|
|
_displayLink = nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
RCTTouchEventTypeStart,
|
|
|
|
|
RCTTouchEventTypeMove,
|
|
|
|
|
@@ -65,10 +120,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
- (void)_recordNewTouches:(NSSet *)touches
|
|
|
|
|
{
|
|
|
|
|
for (UITouch *touch in touches) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RCTAssert(![_nativeTouches containsObject:touch],
|
|
|
|
|
@"Touch is already recorded. This is a critical bug.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Find closest React-managed touchable view
|
|
|
|
|
UIView *targetView = touch.view;
|
|
|
|
|
while (targetView) {
|
|
|
|
|
@@ -95,14 +150,14 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create touch
|
|
|
|
|
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
|
|
|
|
|
reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]];
|
|
|
|
|
reactTouch[@"identifier"] = @(touchID);
|
|
|
|
|
reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event
|
|
|
|
|
reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add to arrays
|
|
|
|
|
[_touchViews addObject:targetView];
|
|
|
|
|
[_nativeTouches addObject:touch];
|
|
|
|
|
@@ -129,10 +184,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
UITouch *nativeTouch = _nativeTouches[touchIndex];
|
|
|
|
|
CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
|
|
|
|
|
CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
UIView *touchView = _touchViews[touchIndex];
|
|
|
|
|
CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
|
|
|
|
|
reactTouch[@"pageX"] = @(rootViewLocation.x);
|
|
|
|
|
reactTouch[@"pageY"] = @(rootViewLocation.y);
|
|
|
|
|
@@ -157,9 +212,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
* (start/end/move/cancel) and the indices that represent "changed" `Touch`es
|
|
|
|
|
* from that array.
|
|
|
|
|
*/
|
|
|
|
|
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName
|
|
|
|
|
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime
|
|
|
|
|
{
|
|
|
|
|
// Update touches
|
|
|
|
|
CFTimeInterval enqueueTime = CACurrentMediaTime();
|
|
|
|
|
NSMutableArray *changedIndexes = [[NSMutableArray alloc] init];
|
|
|
|
|
for (UITouch *touch in touches) {
|
|
|
|
|
NSInteger index = [_nativeTouches indexOfObject:touch];
|
|
|
|
|
@@ -181,10 +237,72 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
for (NSDictionary *touch in _reactTouches) {
|
|
|
|
|
[reactTouches addObject:[touch copy]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName
|
|
|
|
|
touches:reactTouches
|
|
|
|
|
changedIndexes:changedIndexes
|
|
|
|
|
originatingTime:originatingTime];
|
|
|
|
|
[_pendingTouches addObject:touch];
|
|
|
|
|
|
|
|
|
|
if (_recordingInteractionTiming) {
|
|
|
|
|
[_bridgeInteractionTiming addObject:@{
|
|
|
|
|
@"timeSeconds": @(touch.originatingTime),
|
|
|
|
|
@"operation": @"taskOriginated",
|
|
|
|
|
@"taskID": @(touch.id),
|
|
|
|
|
}];
|
|
|
|
|
[_bridgeInteractionTiming addObject:@{
|
|
|
|
|
@"timeSeconds": @(enqueueTime),
|
|
|
|
|
@"operation": @"taskEnqueuedPending",
|
|
|
|
|
@"taskID": @(touch.id),
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)_update:(CADisplayLink *)sender
|
|
|
|
|
{
|
|
|
|
|
// Dispatch touch event
|
|
|
|
|
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
|
|
|
|
|
args:@[eventName, reactTouches, changedIndexes]];
|
|
|
|
|
NSUInteger pendingCount = _pendingTouches.count;
|
|
|
|
|
for (RCTTouchEvent *touch in _pendingTouches) {
|
|
|
|
|
_mostRecentEnqueueJS = CACurrentMediaTime();
|
|
|
|
|
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
|
|
|
|
|
args:@[touch.eventName, touch.touches, touch.changedIndexes]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_recordingInteractionTiming) {
|
|
|
|
|
for (RCTTouchEvent *touch in _pendingTouches) {
|
|
|
|
|
[_bridgeInteractionTiming addObject:@{
|
|
|
|
|
@"timeSeconds": @(sender.timestamp),
|
|
|
|
|
@"operation": @"frameAlignedDispatch",
|
|
|
|
|
@"taskID": @(touch.id),
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) {
|
|
|
|
|
[_bridgeInteractionTiming addObject:@{
|
|
|
|
|
@"timeSeconds": @(sender.timestamp),
|
|
|
|
|
@"operation": @"mainThreadDisplayLink",
|
|
|
|
|
@"taskID": @([RCTTouchEvent newID]),
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[_pendingTouches removeAllObjects];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)startOrResetInteractionTiming
|
|
|
|
|
{
|
|
|
|
|
RCTAssertMainThread();
|
|
|
|
|
[_bridgeInteractionTiming removeAllObjects];
|
|
|
|
|
_recordingInteractionTiming = YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSDictionary *)endAndResetInteractionTiming
|
|
|
|
|
{
|
|
|
|
|
RCTAssertMainThread();
|
|
|
|
|
_recordingInteractionTiming = NO;
|
|
|
|
|
NSArray *_prevInteractionTimingData = _bridgeInteractionTiming;
|
|
|
|
|
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
|
|
|
|
|
return @{ @"interactionTiming": _prevInteractionTimingData };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Gesture Recognizer Delegate Callbacks
|
|
|
|
|
@@ -193,11 +311,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
{
|
|
|
|
|
[super touchesBegan:touches withEvent:event];
|
|
|
|
|
self.state = UIGestureRecognizerStateBegan;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// "start" has to record new touches before extracting the event.
|
|
|
|
|
// "end"/"cancel" needs to remove the touch *after* extracting the event.
|
|
|
|
|
[self _recordNewTouches:touches];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchStart"];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
|
|
|
|
|
@@ -206,20 +324,20 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
|
|
|
|
if (self.state == UIGestureRecognizerStateFailed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchMove"];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
|
|
|
|
|
{
|
|
|
|
|
[super touchesEnded:touches withEvent:event];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchEnd"];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp];
|
|
|
|
|
[self _recordRemovedTouches:touches];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
|
|
|
|
|
{
|
|
|
|
|
[super touchesCancelled:touches withEvent:event];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchCancel"];
|
|
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp];
|
|
|
|
|
[self _recordRemovedTouches:touches];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|