Skip to content

Commit d72966a

Browse files
committed
Allow GestureHandlerRootView to be manually made active (#2401)
This PR allows users to override whether a particular `GestureHandlerRootView` is enabled or not. By default, only the topmost root view would be active, allowing all gestures to interact with each other. The problem with that approach is that if any of the native views underneath called`requestDisallowInterceptTouchEvent`, all gestures would be canceled. It comes from the fact that `requestDisallowInterceptTouchEvent` calls are propagated up the view hierarchy and we expect `GestureHandlerRootView` to be relatively close to the top. The simple solution would be to add another root view underneath the view that may call `requestDisallowInterceptTouchEvent` to prevent gestures from being canceled, this, however, resulted in two problems: - the inner root view would not be enabled - after adding the option to enable it manually, both root views would start handling the event, resulting in duplicated event streams I've fixed the second problem by adding checks at the beginning of `extractGestureHandlers` and `traverseWithPointerEvents` to ensure that we're not extracting gestures attached under another enabled root view. This allows for solving the problem described in #2383. Tested on the reproduction from #2383
1 parent c08f479 commit d72966a

7 files changed

Lines changed: 104 additions & 46 deletions

File tree

packages/docs-gesture-handler/docs/fundamentals/installation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ If you're unsure if one of your dependencies already renders `GestureHandlerRoot
7070
If you're using gesture handler in your component library, you may want to wrap your library's code in the `GestureHandlerRootView` component. This will avoid extra configuration for the user.
7171
:::
7272

73+
:::tip
74+
If you're having trouble with gestures not working when inside a component provided by a third-party library, even though you've wrapped the entry point with `<GestureHandlerRootView>`, you can try adding another `<GestureHandlerRootView unstable_forceActive>` closer to the place the gestures are defined. This way, you can prevent Android from canceling relevant gestures when one of the native views tries to grab lock for delivering touch events.
75+
:::
76+
7377
### 3. Platform specific setup
7478

7579
#### [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/)

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

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.view.View
77
import android.view.ViewGroup
88
import android.widget.EditText
99
import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper
10+
import com.swmansion.gesturehandler.react.RNGestureHandlerRootView
1011
import java.util.*
1112

1213
class GestureHandlerOrchestrator(
@@ -475,6 +476,12 @@ class GestureHandlerOrchestrator(
475476

476477
while (parent != null) {
477478
if (parent is ViewGroup) {
479+
// Stop traversing the hierarchy when encountering another active root view to prevent
480+
// gestures from being extracted multiple times by different orchestrators.
481+
if (parent is RNGestureHandlerRootView && parent.isRootViewEnabled()) {
482+
break
483+
}
484+
478485
val parentViewGroup: ViewGroup = parent
479486

480487
handlerRegistry.getHandlersForView(parent)?.let {
@@ -562,12 +569,26 @@ class GestureHandlerOrchestrator(
562569
extractGestureHandlers(wrapperView, tempCoords, pointerId, event)
563570
}
564571

572+
private fun shouldIgnoreSubtreeIfGestureHandlerRootView(view: View) =
573+
view is RNGestureHandlerRootView && view != wrapperView && view.isRootViewEnabled()
574+
565575
private fun extractGestureHandlers(
566576
viewGroup: ViewGroup,
567577
coords: FloatArray,
568578
pointerId: Int,
569579
event: MotionEvent,
570580
): Boolean {
581+
if (shouldIgnoreSubtreeIfGestureHandlerRootView(viewGroup)) {
582+
// When we encounter another active root view while traversing the view hierarchy, we want
583+
// to stop there so that it can handle the gesture attached under it itself.
584+
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
585+
// cancel all gestures handled by its parent root view) but there may be some gestures attached
586+
// to views under it which should work. Adding another root view under that particular view
587+
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
588+
// We want to stop here so the gesture receives event only once.
589+
return false
590+
}
591+
571592
val childrenCount = viewGroup.childCount
572593
for (i in childrenCount - 1 downTo 0) {
573594
val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i)
@@ -595,52 +616,63 @@ class GestureHandlerOrchestrator(
595616
}
596617

597618
private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int, event: MotionEvent): Boolean =
598-
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
599-
PointerEventsConfig.NONE -> {
600-
// This view and its children can't be the target
601-
false
602-
}
603-
PointerEventsConfig.BOX_ONLY -> {
604-
// This view is the target, its children don't matter
605-
(
606-
recordViewHandlersForPointer(view, coords, pointerId, event) ||
607-
shouldHandlerlessViewBecomeTouchTarget(view, coords)
608-
)
609-
}
610-
PointerEventsConfig.BOX_NONE -> {
611-
// This view can't be the target, but its children might
612-
when (view) {
613-
is ViewGroup -> {
614-
extractGestureHandlers(view, coords, pointerId, event).also { found ->
615-
// A child view is handling touch, also extract handlers attached to this view
616-
if (found) {
617-
recordViewHandlersForPointer(view, coords, pointerId, event)
619+
if (shouldIgnoreSubtreeIfGestureHandlerRootView(view)) {
620+
// When we encounter another active root view while traversing the view hierarchy, we want
621+
// to stop there so that it can handle the gesture attached under it itself.
622+
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
623+
// cancel all gestures handled by its parent root view) but there may be some gestures attached
624+
// to views under it which should work. Adding another root view under that particular view
625+
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
626+
// We want to stop here so the gesture receives event only once.
627+
false
628+
} else {
629+
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
630+
PointerEventsConfig.NONE -> {
631+
// This view and its children can't be the target
632+
false
633+
}
634+
PointerEventsConfig.BOX_ONLY -> {
635+
// This view is the target, its children don't matter
636+
(
637+
recordViewHandlersForPointer(view, coords, pointerId, event) ||
638+
shouldHandlerlessViewBecomeTouchTarget(view, coords)
639+
)
640+
}
641+
PointerEventsConfig.BOX_NONE -> {
642+
// This view can't be the target, but its children might
643+
when (view) {
644+
is ViewGroup -> {
645+
extractGestureHandlers(view, coords, pointerId, event).also { found ->
646+
// A child view is handling touch, also extract handlers attached to this view
647+
if (found) {
648+
recordViewHandlersForPointer(view, coords, pointerId, event)
649+
}
618650
}
619651
}
652+
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
653+
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
654+
// handlers attached to the text input, as it makes sense that gestures would work on a
655+
// non-editable TextInput.
656+
is EditText -> {
657+
recordViewHandlersForPointer(view, coords, pointerId, event)
658+
}
659+
else -> false
620660
}
621-
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
622-
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
623-
// handlers attached to the text input, as it makes sense that gestures would work on a
624-
// non-editable TextInput.
625-
is EditText -> {
626-
recordViewHandlersForPointer(view, coords, pointerId, event)
627-
}
628-
else -> false
629-
}
630-
}
631-
PointerEventsConfig.AUTO -> {
632-
// Either this view or one of its children is the target
633-
val found = if (view is ViewGroup) {
634-
extractGestureHandlers(view, coords, pointerId, event)
635-
} else {
636-
false
637661
}
662+
PointerEventsConfig.AUTO -> {
663+
// Either this view or one of its children is the target
664+
val found = if (view is ViewGroup) {
665+
extractGestureHandlers(view, coords, pointerId, event)
666+
} else {
667+
false
668+
}
638669

639-
(
640-
recordViewHandlersForPointer(view, coords, pointerId, event) ||
641-
found ||
642-
shouldHandlerlessViewBecomeTouchTarget(view, coords)
643-
)
670+
(
671+
recordViewHandlersForPointer(view, coords, pointerId, event) ||
672+
found ||
673+
shouldHandlerlessViewBecomeTouchTarget(view, coords)
674+
)
675+
}
644676
}
645677
}
646678

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
@@ -14,10 +14,12 @@ import com.facebook.react.views.view.ReactViewGroup
1414
class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
1515
private var moduleId: Int = -1
1616
private var rootViewEnabled = false
17+
private var unstableForceActive = false
1718
private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit
19+
1820
override fun onAttachedToWindow() {
1921
super.onAttachedToWindow()
20-
rootViewEnabled = !hasGestureHandlerEnabledRootView(this)
22+
rootViewEnabled = unstableForceActive || !hasGestureHandlerEnabledRootView(this)
2123
if (!rootViewEnabled) {
2224
Log.i(
2325
ReactConstants.TAG,
@@ -61,6 +63,12 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
6163
rootHelper?.activateNativeHandlers(view)
6264
}
6365

66+
fun isRootViewEnabled() = rootViewEnabled
67+
68+
fun setUnstableForceActive(active: Boolean) {
69+
this.unstableForceActive = active
70+
}
71+
6472
companion object {
6573
private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean {
6674
UiThreadUtil.assertOnUiThread()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.facebook.react.module.annotations.ReactModule
44
import com.facebook.react.uimanager.ThemedReactContext
55
import com.facebook.react.uimanager.ViewGroupManager
66
import com.facebook.react.uimanager.ViewManagerDelegate
7+
import com.facebook.react.uimanager.annotations.ReactProp
78
import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerDelegate
89
import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerInterface
910
import com.swmansion.gesturehandler.react.events.RNGestureHandlerEvent
@@ -38,6 +39,11 @@ class RNGestureHandlerRootViewManager :
3839
view.setModuleId(value)
3940
}
4041

42+
@ReactProp(name = "unstable_forceActive")
43+
override fun setUnstable_forceActive(view: RNGestureHandlerRootView, active: Boolean) {
44+
view.setUnstableForceActive(active)
45+
}
46+
4147
/**
4248
* The following event configuration is necessary even if you are not using
4349
* GestureHandlerRootView component directly.

packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
22
import { PropsWithChildren } from 'react';
3-
import { ViewProps, StyleSheet } from 'react-native';
3+
import { StyleSheet } from 'react-native';
44
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
5+
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
56
import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent';
67

78
export interface GestureHandlerRootViewProps
8-
extends PropsWithChildren<ViewProps> {}
9+
extends PropsWithChildren<RootViewNativeProps> {}
910

1011
export default function GestureHandlerRootView({
1112
style,

packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as React from 'react';
22
import { PropsWithChildren } from 'react';
3-
import { View, ViewProps, StyleSheet } from 'react-native';
3+
import { View, StyleSheet } from 'react-native';
44
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
5+
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
56

67
export interface GestureHandlerRootViewProps
7-
extends PropsWithChildren<ViewProps> {}
8+
extends PropsWithChildren<RootViewNativeProps> {}
89

910
export default function GestureHandlerRootView({
1011
style,

packages/react-native-gesture-handler/src/specs/RNGestureHandlerRootViewNativeComponent.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNati
22
import type { Int32 } from 'react-native/Libraries/Types/CodegenTypes';
33
import type { ViewProps } from 'react-native';
44

5+
// Publicly accessible type, moduleId is set internally
6+
export interface RootViewNativeProps extends ViewProps {
7+
unstable_forceActive?: boolean;
8+
}
9+
510
interface NativeProps extends ViewProps {
611
moduleId: Int32;
12+
unstable_forceActive?: boolean;
713
}
814

915
export default codegenNativeComponent<NativeProps>('RNGestureHandlerRootView');

0 commit comments

Comments
 (0)