Skip to content

Commit 01dae90

Browse files
committed
Fix text getting selected on gestures
1 parent cd952bf commit 01dae90

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.swmansion.gesturehandler.core
22

33
import android.graphics.Matrix
44
import android.graphics.PointF
5+
import android.util.SparseArray
56
import android.view.MotionEvent
67
import android.view.View
78
import android.view.ViewGroup
@@ -31,6 +32,10 @@ class GestureHandlerOrchestrator(
3132
private val awaitingHandlers = arrayListOf<GestureHandler>()
3233
private val preparedHandlers = arrayListOf<GestureHandler>()
3334

35+
// Used by `cancelTouchesInInterceptedViews`.
36+
private val viewsToCancel = arrayListOf<View>()
37+
private val pointerDownPoints = SparseArray<PointF>()
38+
3439
// In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list.
3540
// To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was
3641
// removed from `awaitingHandlers`, it was still present in copy of original list. This hashset helps us identify which handlers
@@ -49,6 +54,7 @@ class GestureHandlerOrchestrator(
4954
fun onTouchEvent(event: MotionEvent): Boolean {
5055
isHandlingTouch = true
5156
val action = event.actionMasked
57+
trackPointerDownPoints(event)
5258
if (action == MotionEvent.ACTION_DOWN ||
5359
action == MotionEvent.ACTION_POINTER_DOWN ||
5460
action == MotionEvent.ACTION_HOVER_MOVE
@@ -681,6 +687,108 @@ class GestureHandlerOrchestrator(
681687
return false
682688
}
683689

690+
private fun trackPointerDownPoints(event: MotionEvent) {
691+
val index = event.actionIndex
692+
when (event.actionMasked) {
693+
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
694+
pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index)))
695+
MotionEvent.ACTION_POINTER_UP ->
696+
pointerDownPoints.remove(event.getPointerId(index))
697+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
698+
pointerDownPoints.clear()
699+
}
700+
}
701+
702+
fun cancelTouchesInInterceptedViews(event: MotionEvent) {
703+
viewsToCancel.clear()
704+
for (i in 0 until pointerDownPoints.size()) {
705+
val point = pointerDownPoints.valueAt(i)
706+
tempCoords[0] = point.x
707+
tempCoords[1] = point.y
708+
collectViewsAtPoint(wrapperView, tempCoords, viewsToCancel)
709+
}
710+
711+
if (viewsToCancel.isEmpty()) {
712+
return
713+
}
714+
715+
val activeHandlers = gestureHandlers.filter { it.isActive }
716+
val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
717+
718+
for (view in viewsToCancel) {
719+
if (view === wrapperView || isViewDrivenByActiveNativeGesture(view, activeHandlers)) {
720+
continue
721+
}
722+
view.onTouchEvent(cancelEvent)
723+
}
724+
725+
cancelEvent.recycle()
726+
viewsToCancel.clear()
727+
}
728+
729+
// Whether the view's touch is still owned by a NativeViewGestureHandler that survived arbitration
730+
// (active, or non-conflicting with an active handler). Only those are fed through `onTouchEvent`,
731+
// so only those break if cancelled. Other handlers are orchestrator-driven and unaffected.
732+
private fun isViewDrivenByActiveNativeGesture(view: View, activeHandlers: List<GestureHandler>): Boolean {
733+
val handlers = handlerRegistry.getHandlersForView(view) ?: return false
734+
return handlers.any { handler ->
735+
handler is NativeViewGestureHandler &&
736+
(handler.isActive || activeHandlers.none { active -> shouldHandlerBeCancelledBy(handler, active) })
737+
}
738+
}
739+
740+
// Collects the view path under the point (topmost child first, like touch dispatch), leaf to root.
741+
private fun collectViewsAtPoint(view: View, coords: FloatArray, out: MutableList<View>): Boolean {
742+
if (shouldIgnoreSubtreeIfGestureHandlerRootView(view)) {
743+
// A nested active root view manages its own subtree (and its own interception cancellation).
744+
return false
745+
}
746+
747+
val pointerEvents = viewConfigHelper.getPointerEventsConfigForView(view)
748+
if (pointerEvents == PointerEventsConfig.NONE) {
749+
return false
750+
}
751+
752+
var found = false
753+
if (view is ViewGroup && pointerEvents != PointerEventsConfig.BOX_ONLY) {
754+
for (i in view.childCount - 1 downTo 0) {
755+
val child = view.getChildAt(i)
756+
if (!canReceiveEvents(child)) {
757+
continue
758+
}
759+
val childPoint = tempPoint
760+
transformPointToChildViewCoords(coords[0], coords[1], view, child, childPoint)
761+
if (isClipping(child) && !isTransformedTouchPointInView(childPoint.x, childPoint.y, child)) {
762+
continue
763+
}
764+
val restoreX = coords[0]
765+
val restoreY = coords[1]
766+
coords[0] = childPoint.x
767+
coords[1] = childPoint.y
768+
found = collectViewsAtPoint(child, coords, out)
769+
coords[0] = restoreX
770+
coords[1] = restoreY
771+
772+
if (found) {
773+
break
774+
}
775+
}
776+
}
777+
778+
// BOX_NONE views can't be the target themselves, only their children can
779+
val selfIsTarget = pointerEvents != PointerEventsConfig.BOX_NONE &&
780+
isTransformedTouchPointInView(coords[0], coords[1], view)
781+
782+
if (found || selfIsTarget) {
783+
// `out` may already contain this view when several pointers share part of their path.
784+
if (!out.contains(view)) {
785+
out.add(view)
786+
}
787+
return true
788+
}
789+
return false
790+
}
791+
684792
private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int, event: MotionEvent): Boolean =
685793
if (shouldIgnoreSubtreeIfGestureHandlerRootView(view)) {
686794
// When we encounter another active root view while traversing the view hierarchy, we want

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
2020
private val jsGestureHandler: GestureHandler?
2121
val rootView: ViewGroup
2222
private var shouldIntercept = false
23+
private var wasIntercepting = false
2324
private var passingTouch = false
2425

2526
init {
@@ -129,6 +130,14 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
129130
passingTouch = true
130131
orchestrator!!.onTouchEvent(event)
131132
passingTouch = false
133+
134+
// On the transition into interception, cancel the native views the pointers landed on - the
135+
// framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`
136+
if (shouldIntercept && !wasIntercepting) {
137+
orchestrator!!.cancelTouchesInInterceptedViews(event)
138+
}
139+
wasIntercepting = shouldIntercept
140+
132141
return shouldIntercept
133142
}
134143

0 commit comments

Comments
 (0)