Skip to content

Commit ad3f731

Browse files
committed
Allow GestureHandlerRootView to be manually made active
1 parent ec656ba commit ad3f731

10 files changed

Lines changed: 108 additions & 49 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: 70 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 {
@@ -568,6 +575,17 @@ class GestureHandlerOrchestrator(
568575
pointerId: Int,
569576
event: MotionEvent,
570577
): Boolean {
578+
if (viewGroup is RNGestureHandlerRootView && viewGroup != wrapperView && viewGroup.isRootViewEnabled()) {
579+
// When we encounter another active root view while traversing the view hierarchy, we want
580+
// to stop there so that it can handle the gesture attached under it itself.
581+
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
582+
// cancel all gestures handled by its parent root view) but there may be some gestures attached
583+
// to views under it which should work. Adding another root view under that particular view
584+
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
585+
// We want to stop here so the gesture receives event only once.
586+
return false
587+
}
588+
571589
val childrenCount = viewGroup.childCount
572590
for (i in childrenCount - 1 downTo 0) {
573591
val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i)
@@ -595,52 +613,63 @@ class GestureHandlerOrchestrator(
595613
}
596614

597615
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)
616+
if (view is RNGestureHandlerRootView && view != wrapperView && view.isRootViewEnabled()) {
617+
// When we encounter another active root view while traversing the view hierarchy, we want
618+
// to stop there so that it can handle the gesture attached under it itself.
619+
// This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would
620+
// cancel all gestures handled by its parent root view) but there may be some gestures attached
621+
// to views under it which should work. Adding another root view under that particular view
622+
// would allow the gesture to be recognized even though the parent root view cancelled its gestures.
623+
// We want to stop here so the gesture receives event only once.
624+
false
625+
} else {
626+
when (viewConfigHelper.getPointerEventsConfigForView(view)) {
627+
PointerEventsConfig.NONE -> {
628+
// This view and its children can't be the target
629+
false
630+
}
631+
PointerEventsConfig.BOX_ONLY -> {
632+
// This view is the target, its children don't matter
633+
(
634+
recordViewHandlersForPointer(view, coords, pointerId, event) ||
635+
shouldHandlerlessViewBecomeTouchTarget(view, coords)
636+
)
637+
}
638+
PointerEventsConfig.BOX_NONE -> {
639+
// This view can't be the target, but its children might
640+
when (view) {
641+
is ViewGroup -> {
642+
extractGestureHandlers(view, coords, pointerId, event).also { found ->
643+
// A child view is handling touch, also extract handlers attached to this view
644+
if (found) {
645+
recordViewHandlersForPointer(view, coords, pointerId, event)
646+
}
618647
}
619648
}
649+
// When <TextInput> has editable set to `false` getPointerEventsConfigForView returns
650+
// `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract
651+
// handlers attached to the text input, as it makes sense that gestures would work on a
652+
// non-editable TextInput.
653+
is EditText -> {
654+
recordViewHandlersForPointer(view, coords, pointerId, event)
655+
}
656+
else -> false
620657
}
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
637658
}
659+
PointerEventsConfig.AUTO -> {
660+
// Either this view or one of its children is the target
661+
val found = if (view is ViewGroup) {
662+
extractGestureHandlers(view, coords, pointerId, event)
663+
} else {
664+
false
665+
}
638666

639-
(
640-
recordViewHandlersForPointer(view, coords, pointerId, event) ||
641-
found ||
642-
shouldHandlerlessViewBecomeTouchTarget(view, coords)
643-
)
667+
(
668+
recordViewHandlersForPointer(view, coords, pointerId, event) ||
669+
found ||
670+
shouldHandlerlessViewBecomeTouchTarget(view, coords)
671+
)
672+
}
644673
}
645674
}
646675

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 { NativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
67
import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent';
78

89
export interface GestureHandlerRootViewProps
9-
extends PropsWithChildren<ViewProps> {}
10+
extends PropsWithChildren<NativeProps> {}
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 { NativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent';
67

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

1011
export default function GestureHandlerRootView({
1112
style,
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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 NativeProps extends ViewProps {
5+
unstable_forceActive?: boolean;
6+
}
57

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

0 commit comments

Comments
 (0)