Skip to content

Commit 36e6153

Browse files
authored
Allow GestureHandlerRootView to be manually made active (#2401)
## Description 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. ## Test plan Tested on the reproduction from #2383
1 parent 648ba29 commit 36e6153

10 files changed

Lines changed: 114 additions & 50 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/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerButtonManagerDelegate.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.facebook.react.uimanager.BaseViewManagerDelegate;
1717
import com.facebook.react.uimanager.LayoutShadowNode;
1818

19+
@SuppressWarnings("deprecation")
1920
public class RNGestureHandlerButtonManagerDelegate<T extends View, U extends BaseViewManager<T, ? extends LayoutShadowNode> & RNGestureHandlerButtonManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
2021
public RNGestureHandlerButtonManagerDelegate(U viewManager) {
2122
super(viewManager);

packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@
1515
import com.facebook.react.uimanager.BaseViewManagerDelegate;
1616
import com.facebook.react.uimanager.LayoutShadowNode;
1717

18+
@SuppressWarnings("deprecation")
1819
public class RNGestureHandlerRootViewManagerDelegate<T extends View, U extends BaseViewManager<T, ? extends LayoutShadowNode> & RNGestureHandlerRootViewManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
1920
public RNGestureHandlerRootViewManagerDelegate(U viewManager) {
2021
super(viewManager);
2122
}
2223
@Override
2324
public void setProperty(T view, String propName, @Nullable Object value) {
24-
super.setProperty(view, propName, value);
25+
switch (propName) {
26+
case "unstable_forceActive":
27+
mViewManager.setUnstable_forceActive(view, value == null ? false : (boolean) value);
28+
break;
29+
default:
30+
super.setProperty(view, propName, value);
31+
}
2532
}
2633
}

packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
import com.facebook.react.uimanager.ViewManagerWithGeneratedInterface;
1414

1515
public interface RNGestureHandlerRootViewManagerInterface<T extends View> extends ViewManagerWithGeneratedInterface {
16-
// No props
16+
void setUnstable_forceActive(T view, boolean value);
1717
}

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
@@ -13,10 +13,12 @@ import com.facebook.react.views.view.ReactViewGroup
1313

1414
class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
1515
private var rootViewEnabled = false
16+
private var unstableForceActive = false
1617
private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit
18+
1719
override fun onAttachedToWindow() {
1820
super.onAttachedToWindow()
19-
rootViewEnabled = !hasGestureHandlerEnabledRootView(this)
21+
rootViewEnabled = unstableForceActive || !hasGestureHandlerEnabledRootView(this)
2022
if (!rootViewEnabled) {
2123
Log.i(
2224
ReactConstants.TAG,
@@ -56,6 +58,12 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) {
5658
rootHelper?.activateNativeHandlers(view)
5759
}
5860

61+
fun isRootViewEnabled() = rootViewEnabled
62+
63+
fun setUnstableForceActive(active: Boolean) {
64+
this.unstableForceActive = active
65+
}
66+
5967
companion object {
6068
private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean {
6169
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

@@ -32,6 +33,11 @@ class RNGestureHandlerRootViewManager :
3233
view.tearDown()
3334
}
3435

36+
@ReactProp(name = "unstable_forceActive")
37+
override fun setUnstable_forceActive(view: RNGestureHandlerRootView, active: Boolean) {
38+
view.setUnstableForceActive(active)
39+
}
40+
3541
/**
3642
* The following event configuration is necessary even if you are not using
3743
* 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,12 +1,13 @@
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 { maybeInitializeFabric } from '../init';
55
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
6+
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
67
import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent';
78

89
export interface GestureHandlerRootViewProps
9-
extends PropsWithChildren<ViewProps> {}
10+
extends PropsWithChildren<RootViewNativeProps> {}
1011

1112
export default function GestureHandlerRootView({
1213
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,11 +1,12 @@
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 { maybeInitializeFabric } from '../init';
55
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
6+
import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
67

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

1011
export default function GestureHandlerRootView({
1112
style,
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
22
import type { ViewProps } from 'react-native';
33

4-
interface NativeProps extends ViewProps {}
4+
export interface RootViewNativeProps extends ViewProps {
5+
unstable_forceActive?: boolean;
6+
}
57

6-
export default codegenNativeComponent<NativeProps>('RNGestureHandlerRootView');
8+
export default codegenNativeComponent<RootViewNativeProps>(
9+
'RNGestureHandlerRootView'
10+
);

0 commit comments

Comments
 (0)