diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/pseudoSelectors/PseudoSelectorManager.kt b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/pseudoSelectors/PseudoSelectorManager.kt index 07e1c146ee6..f461747966e 100644 --- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/pseudoSelectors/PseudoSelectorManager.kt +++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/pseudoSelectors/PseudoSelectorManager.kt @@ -2,6 +2,7 @@ package com.swmansion.reanimated.pseudoSelectors import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.ViewParent import com.facebook.react.bridge.UIManager import com.facebook.react.bridge.UIManagerListener @@ -25,6 +26,8 @@ class PseudoSelectorManager( private val gestureDownLeaf = HashMap() + private val gestureDownPoint = HashMap() + private val hover = TouchHoverCoordinator() private val pendingAttaches = LinkedHashMap() @@ -235,6 +238,7 @@ class PseudoSelectorManager( } touchHostRefs.remove(host) gestureDownLeaf.remove(host) + gestureDownPoint.remove(host) host.setOnTouchListener(null) } @@ -247,6 +251,10 @@ class PseudoSelectorManager( if (!hover.isGestureSettled(event)) { onHostDown(host, event) } + MotionEvent.ACTION_MOVE -> + if (event.findPointerIndex(0) >= 0) { + onHostMove(host, event) + } MotionEvent.ACTION_POINTER_UP -> if (event.getPointerId(event.actionIndex) == 0) { onHostRelease(host) @@ -272,13 +280,28 @@ class PseudoSelectorManager( ) { findTouchedLeaf(host, event)?.let { gestureDownLeaf[host] = it + gestureDownPoint[host] = floatArrayOf(event.rawX, event.rawY) fireActiveCallbacksUpTree(it, true) fireDeepestIfHit(it, event.rawX, event.rawY) } hover.onViewTouchDown(host, event) } + private fun onHostMove( + host: View, + event: MotionEvent, + ) { + val down = gestureDownPoint[host] ?: return + val dx = event.rawX - down[0] + val dy = event.rawY - down[1] + val slop = ViewConfiguration.get(host.context).scaledTouchSlop.toFloat() + if (dx * dx + dy * dy > slop * slop) { + onHostRelease(host) + } + } + private fun onHostRelease(host: View) { + gestureDownPoint.remove(host) val leaf = gestureDownLeaf.remove(host) ?: return fireActiveCallbacksUpTree(leaf, false) deepestCallbacks[leaf]?.onSelectorStateChanged(false) diff --git a/packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.mm b/packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.mm index 64ac2311932..e6b4f419836 100644 --- a/packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.mm +++ b/packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REAPseudoSelectorObserver.mm @@ -23,6 +23,10 @@ - (void)attachMacFocusObservers; #endif @end +#if !TARGET_OS_OSX +static const CGFloat kActivePressMovementThreshold = 10.0; +#endif + @implementation REAPseudoSelectorObserver { __weak REAUIView *_view; reanimated::PseudoSelector _selector; @@ -37,6 +41,10 @@ @implementation REAPseudoSelectorObserver { NSArray *_notificationObservers; std::function _callback; BOOL _activeTouchCounted; +#if !TARGET_OS_OSX + CGPoint _pressDownLocation; + BOOL _pressDismissed; +#endif } #if !TARGET_OS_OSX @@ -87,7 +95,7 @@ - (void)attachActiveGestureRecognizerToView:(REAUIView *)view { #if !TARGET_OS_OSX UILongPressGestureRecognizer *recognizer = - [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouchGesture:)]; + [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleActiveGesture:)]; recognizer.cancelsTouchesInView = NO; #else NSPressGestureRecognizer *recognizer = @@ -104,7 +112,7 @@ - (void)attachHoverToView:(REAUIView *)view #if !TARGET_OS_OSX #if !TARGET_OS_TV UIHoverGestureRecognizer *recognizer = - [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouchGesture:)]; + [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(handleHoverGesture:)]; recognizer.delegate = self; [view addGestureRecognizer:recognizer]; _gestureRecognizer = recognizer; @@ -261,17 +269,34 @@ - (void)attachFocusObserversWithPredicate:(BOOL (^)(NSNotification *))isOurs #if !TARGET_OS_OSX -- (void)handleTouchGesture:(UIGestureRecognizer *)recognizer +- (void)handleActiveGesture:(UILongPressGestureRecognizer *)recognizer { switch (recognizer.state) { case UIGestureRecognizerStateBegan: + _pressDownLocation = [recognizer locationInView:nil]; + _pressDismissed = NO; _callback(true); [self retainActiveTouch]; break; + case UIGestureRecognizerStateChanged: { + if (_pressDismissed) { + break; + } + CGPoint point = [recognizer locationInView:nil]; + CGFloat dx = point.x - _pressDownLocation.x; + CGFloat dy = point.y - _pressDownLocation.y; + if (dx * dx + dy * dy > kActivePressMovementThreshold * kActivePressMovementThreshold) { + _pressDismissed = YES; + _callback(false); + } + break; + } case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: - _callback(false); + if (!_pressDismissed) { + _callback(false); + } [self releaseActiveTouch]; break; default: @@ -279,6 +304,22 @@ - (void)handleTouchGesture:(UIGestureRecognizer *)recognizer } } +- (void)handleHoverGesture:(UIHoverGestureRecognizer *)recognizer +{ + switch (recognizer.state) { + case UIGestureRecognizerStateBegan: + _callback(true); + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: + _callback(false); + break; + default: + break; + } +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if (![self isPressSelector]) {