From c5cb707ba8b8a2a2290e794754324870f331ef1e Mon Sep 17 00:00:00 2001 From: Vincent Riemer Date: Mon, 11 Apr 2022 15:49:30 -0700 Subject: [PATCH] Add basic onPointerEnter/Leave event emitting to iOS Summary: Changelog: [Internal] Add basic onPointerEnter/Leave event emitting to iOS Reviewed By: lunaleaps Differential Revision: D35414116 fbshipit-source-id: dd62cf7736c466e328b9ebbf51bf010610f4bd92 --- .../NativeComponent/PlatformBaseViewConfig.js | 14 +++ React/Fabric/RCTSurfaceTouchHandler.mm | 103 ++++++++++++++++++ React/Views/RCTViewManager.m | 2 + .../components/view/TouchEventEmitter.cpp | 16 +++ .../components/view/TouchEventEmitter.h | 2 + 5 files changed, 137 insertions(+) diff --git a/Libraries/NativeComponent/PlatformBaseViewConfig.js b/Libraries/NativeComponent/PlatformBaseViewConfig.js index af14525f07d..4f439ac6811 100644 --- a/Libraries/NativeComponent/PlatformBaseViewConfig.js +++ b/Libraries/NativeComponent/PlatformBaseViewConfig.js @@ -389,6 +389,20 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName = bubbled: 'onPointerUp', }, }, + topPointerEnter2: { + phasedRegistrationNames: { + captured: 'onPointerEnter2Capture', + bubbled: 'onPointerEnter2', + skipBubbling: true, + }, + }, + topPointerLeave2: { + phasedRegistrationNames: { + captured: 'onPointerLeave2Capture', + bubbled: 'onPointerLeave2', + skipBubbling: true, + }, + }, }, directEventTypes: { topAccessibilityAction: { diff --git a/React/Fabric/RCTSurfaceTouchHandler.mm b/React/Fabric/RCTSurfaceTouchHandler.mm index 95e2d5e5de5..45af639bc6b 100644 --- a/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/React/Fabric/RCTSurfaceTouchHandler.mm @@ -128,6 +128,34 @@ static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponen return activeTouch; } +static UIView *FindClosestFabricManagedTouchableView(UIView *componentView) +{ + while (componentView) { + if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { + return componentView; + } + componentView = componentView.superview; + } + return nil; +} + +static NSOrderedSet *GetTouchableViewsInPathToRoot(UIView *componentView) +{ + NSMutableOrderedSet *results = [NSMutableOrderedSet orderedSet]; + do { + if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { + [results addObject:componentView]; + } + componentView = componentView.superview; + } while (componentView); + return results; +} + +static SharedTouchEventEmitter GetTouchEmitterFromView(UIView *componentView, CGPoint point) +{ + return [(id)componentView touchEventEmitterAtPoint:point]; +} + static const char *PointerTypeCStringFromUITouchType(UITouchType type) { switch (type) { @@ -157,6 +185,22 @@ static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch) return event; } +static PointerEvent +CreatePointerEventFromIncompleteHoverData(UIView *view, CGPoint clientLocation, NSTimeInterval timestamp) +{ + 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 = 0; + event.pressure = 0.0; + event.pointerType = "mouse"; + event.clientPoint = RCTPointFromCGPoint(clientLocation); + event.target = (Tag)view.tag; + event.timestamp = timestamp; + return event; +} + static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) { for (UITouch *touch in touches) { @@ -203,6 +247,9 @@ struct PointerHasher { */ __weak UIView *_rootComponentView; IdentifierPool<11> _identifierPool; + + UIHoverGestureRecognizer *_hoverRecognizer API_AVAILABLE(ios(13.0)); + NSOrderedSet *_currentlyHoveredViews; } - (instancetype)init @@ -217,6 +264,9 @@ struct PointerHasher { self.delaysTouchesEnded = NO; self.delegate = self; + + _hoverRecognizer = nil; + _currentlyHoveredViews = [NSOrderedSet orderedSet]; } return self; @@ -230,6 +280,13 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act [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 @@ -239,6 +296,11 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act [view removeGestureRecognizer:self]; _rootComponentView = nil; + + if (_hoverRecognizer != nil) { + [view removeGestureRecognizer:_hoverRecognizer]; + _hoverRecognizer = nil; + } } - (void)_registerTouches:(NSSet *)touches @@ -483,4 +545,45 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act [self setEnabled:YES]; } +- (void)hovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) +{ + UIView *listenerView = recognizer.view; + CGPoint clientLocation = [recognizer locationInView:listenerView]; + + UIView *targetView = [listenerView hitTest:clientLocation withEvent:nil]; + targetView = FindClosestFabricManagedTouchableView(targetView); + + NSOrderedSet *eventPathViews = GetTouchableViewsInPathToRoot(targetView); + + NSTimeInterval timestamp = CACurrentMediaTime(); + + // Entering + // root -> child + for (UIView *componentView in [eventPathViews reverseObjectEnumerator]) { + if (![_currentlyHoveredViews containsObject:componentView]) { + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [recognizer locationInView:componentView]); + if (eventEmitter != nil) { + PointerEvent event = CreatePointerEventFromIncompleteHoverData(componentView, clientLocation, timestamp); + eventEmitter->onPointerEnter2(event); + } + } + } + + // Leaving + // child -> root + for (UIView *componentView in _currentlyHoveredViews) { + if (![eventPathViews containsObject:componentView]) { + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [recognizer locationInView:componentView]); + if (eventEmitter != nil) { + PointerEvent event = CreatePointerEventFromIncompleteHoverData(componentView, clientLocation, timestamp); + eventEmitter->onPointerLeave2(event); + } + } + } + + _currentlyHoveredViews = eventPathViews; +} + @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index f41027c24ac..0af349726a4 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -450,5 +450,7 @@ RCT_EXPORT_VIEW_PROPERTY(onPointerCancel, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPointerDown, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPointerMove2, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPointerUp, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPointerEnter2, RCTCapturingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPointerLeave2, RCTCapturingEventBlock) @end diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index d2436fd0969..f57d15a24b5 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -161,5 +161,21 @@ void TouchEventEmitter::onPointerUp(const PointerEvent &event) const { RawEvent::Category::ContinuousEnd); } +void TouchEventEmitter::onPointerEnter2(const PointerEvent &event) const { + dispatchPointerEvent( + "pointerEnter2", + event, + EventPriority::AsynchronousBatched, + RawEvent::Category::ContinuousStart); +} + +void TouchEventEmitter::onPointerLeave2(const PointerEvent &event) const { + dispatchPointerEvent( + "pointerLeave2", + event, + EventPriority::AsynchronousBatched, + RawEvent::Category::ContinuousEnd); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.h b/ReactCommon/react/renderer/components/view/TouchEventEmitter.h index 25e884fbeeb..c1e9ed29bfb 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.h +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.h @@ -34,6 +34,8 @@ class TouchEventEmitter : public EventEmitter { void onPointerDown(PointerEvent const &event) const; void onPointerMove2(PointerEvent const &event) const; void onPointerUp(PointerEvent const &event) const; + void onPointerEnter2(PointerEvent const &event) const; + void onPointerLeave2(PointerEvent const &event) const; private: void dispatchTouchEvent(