Skip to content

Commit f0c98c7

Browse files
j-piaseckim-bert
andauthored
[Android] Properly handle requestDisallowInterceptTouchEvent for v3 (#4367)
## Description On Android, when a native view calls `requestDisallowInterceptTouchEvent` (e.g. a pager starting a swipe), `RNGestureHandlerRootView` reacted by cancelling **all** handlers registered on the root. `react-native-pager-view` calls it eagerly on touch down, so any v3 gesture rendered inside a pager (e.g. inside material top tabs) was cancelled before it could activate — a long press nested in top tabs never activated at all. This PR makes the cancellation targeted: - The root view now cancels only legacy handlers (v1/v2 action types) via the new `GestureHandlerOrchestrator.cancelAllLegacyHandlers`, instead of the old trick of activating the internal `RootViewGestureHandler`. The root handler is now attached with `ACTION_TYPE_NONE` so it's excluded from that sweep (and no longer sends dead events to JS). - v3 handlers are cancelled by new `requestDisallowInterceptTouchEvent` overrides on `RNGestureHandlerDetectorView` and `ButtonViewGroup`. Since the request only bubbles upward from the requesting view, only handlers attached to its ancestors are cancelled — handlers below the requester (like the long press under the pager) keep working. - Cancellation is skipped while the orchestrator is delivering events (`isHandlingTouch`), mirroring the existing `passingTouch` guard, so disallow requests caused by RNGH's own event delivery don't cancel gestures. - `findGestureHandlerRootView` now returns the nearest *enabled* root view so the checks above consult the orchestrator that actually manages the subtree. ## Test plan Tested on reproducer from #2383 https://github.com/user-attachments/assets/4d01dff7-5268-42ff-befa-5c971d39bb4a --------- Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
1 parent edcd60c commit f0c98c7

5 files changed

Lines changed: 72 additions & 16 deletions

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class GestureHandlerOrchestrator(
5252
// `contains` method on HashSet has O(1) complexity, so calling it inside for loop won't result in O(n^2) (contrary to ArrayList)
5353
private val awaitingHandlersTags = HashSet<Int>()
5454

55-
private var isHandlingTouch = false
55+
var isHandlingTouch = false
56+
private set
5657
private var handlingChangeSemaphore = 0
5758
private var finishedHandlersCleanupScheduled = false
5859
private var activationIndex = 0
@@ -358,6 +359,30 @@ class GestureHandlerOrchestrator(
358359
event.recycle()
359360
}
360361

362+
/**
363+
* Cancels all handlers created using API v1 and v2
364+
*/
365+
fun cancelAllLegacyHandlers() {
366+
val handlersToProcess = obtainHandlerList()
367+
handlersToProcess.addAll(gestureHandlers)
368+
369+
try {
370+
handlersToProcess.forEach {
371+
if (it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API ||
372+
it.actionType == GestureHandler.ACTION_TYPE_JS_FUNCTION_NEW_API ||
373+
it.actionType == GestureHandler.ACTION_TYPE_REANIMATED_WORKLET ||
374+
it.actionType == GestureHandler.ACTION_TYPE_NATIVE_ANIMATED_EVENT
375+
) {
376+
it.cancel()
377+
}
378+
}
379+
380+
scheduleFinishedHandlersCleanup()
381+
} finally {
382+
recycleHandlerList(handlersToProcess)
383+
}
384+
}
385+
361386
/**
362387
* isViewAttachedUnderWrapper checks whether all of parents for view related to handler
363388
* view are attached. Since there might be an issue rarely observed when view

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,24 @@ class RNGestureHandlerButtonViewManager :
783783
}
784784
}
785785

786+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
787+
super.requestDisallowInterceptTouchEvent(disallowIntercept)
788+
if (!disallowIntercept) {
789+
return
790+
}
791+
792+
val moduleId = moduleId ?: return
793+
val managedHandlerTag = managedHandlerTag ?: return
794+
795+
val orchestrator =
796+
RNGestureHandlerRootView.findGestureHandlerRootView(this)?.orchestrator ?: return
797+
val handler = RNGestureHandlerModule.registries[moduleId]?.getHandler(managedHandlerTag) ?: return
798+
799+
if (!orchestrator.isHandlingTouch && handler.view != null) {
800+
handler.cancel()
801+
}
802+
}
803+
786804
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
787805
if (super.onInterceptTouchEvent(event)) {
788806
return true

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {
4646
super.onDetachedFromWindow()
4747
}
4848

49+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
50+
super.requestDisallowInterceptTouchEvent(disallowIntercept)
51+
if (!disallowIntercept) {
52+
return
53+
}
54+
55+
val orchestrator = RNGestureHandlerRootView.findGestureHandlerRootView(this)?.orchestrator ?: return
56+
57+
if (!orchestrator.isHandlingTouch) {
58+
val registry = RNGestureHandlerModule.registries[moduleId] ?: return
59+
for (tag in attachedHandlers) {
60+
registry.getHandler(tag)?.takeIf { it.view != null }?.cancel()
61+
}
62+
}
63+
}
64+
4965
fun setModuleId(id: Int) {
5066
if (this.moduleId == id) {
5167
return

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.swmansion.gesturehandler.core.GestureHandlerOrchestrator
1616
import com.swmansion.gesturehandler.core.OnJSResponderCancelListener
1717

1818
class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView: ViewGroup, private val moduleId: Int) {
19-
private val orchestrator: GestureHandlerOrchestrator?
19+
val orchestrator: GestureHandlerOrchestrator?
2020
private val jsGestureHandler: GestureHandler?
2121
val rootView: ViewGroup
2222
private var shouldIntercept = false
@@ -57,7 +57,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
5757
}
5858
jsGestureHandler = RootViewGestureHandler(handlerTag = -wrappedViewTag)
5959
registry.registerHandler(jsGestureHandler)
60-
registry.attachHandlerToView(jsGestureHandler.tag, wrappedViewTag, GestureHandler.ACTION_TYPE_JS_FUNCTION_OLD_API)
60+
registry.attachHandlerToView(jsGestureHandler.tag, wrappedViewTag, GestureHandler.ACTION_TYPE_NONE)
6161
module.registerRootHelper(this)
6262
}
6363

@@ -120,7 +120,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
120120
if (orchestrator != null && !passingTouch) {
121121
// if we are in the process of delivering touch events via GH orchestrator, we don't want to
122122
// treat it as a native gesture capturing the lock
123-
tryCancelAllHandlers()
123+
orchestrator.cancelAllLegacyHandlers()
124124
}
125125
}
126126

@@ -142,17 +142,6 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
142142
return shouldIntercept
143143
}
144144

145-
private fun tryCancelAllHandlers() {
146-
// In order to cancel handlers we activate handler that is hooked to the root view
147-
jsGestureHandler?.apply {
148-
if (state == GestureHandler.STATE_BEGAN) {
149-
// Try activate main JS handler
150-
activate()
151-
end()
152-
}
153-
}
154-
}
155-
156145
fun activateNativeHandlers(view: View) {
157146
orchestrator?.activateNativeHandlersForView(view)
158147
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ import com.facebook.react.common.ReactConstants
1212
import com.facebook.react.uimanager.RootView
1313
import com.facebook.react.views.view.ReactViewGroup
1414
import com.swmansion.gesturehandler.core.GestureHandler
15+
import com.swmansion.gesturehandler.core.GestureHandlerOrchestrator
1516

1617
class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
1718
private var moduleId: Int = -1
1819
private var rootViewEnabled = false
1920
private var unstableForceActive = false
2021
private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit
2122

23+
val orchestrator: GestureHandlerOrchestrator?
24+
get() = rootHelper?.orchestrator
25+
2226
override fun onAttachedToWindow() {
2327
super.onAttachedToWindow()
2428
rootViewEnabled = unstableForceActive || !hasGestureHandlerEnabledRootView(this)
@@ -72,7 +76,7 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
7276
}
7377

7478
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
75-
if (rootViewEnabled) {
79+
if (rootViewEnabled && disallowIntercept) {
7680
rootHelper!!.requestDisallowInterceptTouchEvent()
7781
}
7882
super.requestDisallowInterceptTouchEvent(disallowIntercept)
@@ -114,6 +118,10 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
114118
while (parent != null) {
115119
if (parent is RNGestureHandlerRootView) {
116120
gestureHandlerRootView = parent
121+
122+
if (parent.rootViewEnabled) {
123+
return parent
124+
}
117125
}
118126
parent = parent.parent
119127
}

0 commit comments

Comments
 (0)