diff --git a/packages/docs-gesture-handler/docs/fundamentals/installation.md b/packages/docs-gesture-handler/docs/fundamentals/installation.md index ef8d3e1ef1..4ca8b0e532 100644 --- a/packages/docs-gesture-handler/docs/fundamentals/installation.md +++ b/packages/docs-gesture-handler/docs/fundamentals/installation.md @@ -70,6 +70,10 @@ If you're unsure if one of your dependencies already renders `GestureHandlerRoot 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. ::: +:::tip +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 ``, you can try adding another `` 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. +::: + ### 3. Platform specific setup #### [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) diff --git a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerButtonManagerDelegate.java b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerButtonManagerDelegate.java index 755f026e14..3e0b2e26c4 100644 --- a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerButtonManagerDelegate.java +++ b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerButtonManagerDelegate.java @@ -16,6 +16,7 @@ import com.facebook.react.uimanager.BaseViewManagerDelegate; import com.facebook.react.uimanager.LayoutShadowNode; +@SuppressWarnings("deprecation") public class RNGestureHandlerButtonManagerDelegate & RNGestureHandlerButtonManagerInterface> extends BaseViewManagerDelegate { public RNGestureHandlerButtonManagerDelegate(U viewManager) { super(viewManager); diff --git a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java index 99dfe019d0..69624e0ea5 100644 --- a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java +++ b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java @@ -15,12 +15,19 @@ import com.facebook.react.uimanager.BaseViewManagerDelegate; import com.facebook.react.uimanager.LayoutShadowNode; +@SuppressWarnings("deprecation") public class RNGestureHandlerRootViewManagerDelegate & RNGestureHandlerRootViewManagerInterface> extends BaseViewManagerDelegate { public RNGestureHandlerRootViewManagerDelegate(U viewManager) { super(viewManager); } @Override public void setProperty(T view, String propName, @Nullable Object value) { - super.setProperty(view, propName, value); + switch (propName) { + case "unstable_forceActive": + mViewManager.setUnstable_forceActive(view, value == null ? false : (boolean) value); + break; + default: + super.setProperty(view, propName, value); + } } } diff --git a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java index c94080ea6c..9864e0fc48 100644 --- a/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java +++ b/packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java @@ -13,5 +13,5 @@ import com.facebook.react.uimanager.ViewManagerWithGeneratedInterface; public interface RNGestureHandlerRootViewManagerInterface extends ViewManagerWithGeneratedInterface { - // No props + void setUnstable_forceActive(T view, boolean value); } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index a532cf6ec4..13d588790b 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper +import com.swmansion.gesturehandler.react.RNGestureHandlerRootView import java.util.* class GestureHandlerOrchestrator( @@ -475,6 +476,12 @@ class GestureHandlerOrchestrator( while (parent != null) { if (parent is ViewGroup) { + // Stop traversing the hierarchy when encountering another active root view to prevent + // gestures from being extracted multiple times by different orchestrators. + if (parent is RNGestureHandlerRootView && parent.isRootViewEnabled()) { + break + } + val parentViewGroup: ViewGroup = parent handlerRegistry.getHandlersForView(parent)?.let { @@ -562,12 +569,26 @@ class GestureHandlerOrchestrator( extractGestureHandlers(wrapperView, tempCoords, pointerId, event) } + private fun shouldIgnoreSubtreeIfGestureHandlerRootView(view: View) = + view is RNGestureHandlerRootView && view != wrapperView && view.isRootViewEnabled() + private fun extractGestureHandlers( viewGroup: ViewGroup, coords: FloatArray, pointerId: Int, event: MotionEvent, ): Boolean { + if (shouldIgnoreSubtreeIfGestureHandlerRootView(viewGroup)) { + // When we encounter another active root view while traversing the view hierarchy, we want + // to stop there so that it can handle the gesture attached under it itself. + // This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would + // cancel all gestures handled by its parent root view) but there may be some gestures attached + // to views under it which should work. Adding another root view under that particular view + // would allow the gesture to be recognized even though the parent root view cancelled its gestures. + // We want to stop here so the gesture receives event only once. + return false + } + val childrenCount = viewGroup.childCount for (i in childrenCount - 1 downTo 0) { val child = viewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i) @@ -595,52 +616,63 @@ class GestureHandlerOrchestrator( } private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int, event: MotionEvent): Boolean = - when (viewConfigHelper.getPointerEventsConfigForView(view)) { - PointerEventsConfig.NONE -> { - // This view and its children can't be the target - false - } - PointerEventsConfig.BOX_ONLY -> { - // This view is the target, its children don't matter - ( - recordViewHandlersForPointer(view, coords, pointerId, event) || - shouldHandlerlessViewBecomeTouchTarget(view, coords) - ) - } - PointerEventsConfig.BOX_NONE -> { - // This view can't be the target, but its children might - when (view) { - is ViewGroup -> { - extractGestureHandlers(view, coords, pointerId, event).also { found -> - // A child view is handling touch, also extract handlers attached to this view - if (found) { - recordViewHandlersForPointer(view, coords, pointerId, event) + if (shouldIgnoreSubtreeIfGestureHandlerRootView(view)) { + // When we encounter another active root view while traversing the view hierarchy, we want + // to stop there so that it can handle the gesture attached under it itself. + // This helps in cases where a view may call `requestDisallowInterceptTouchEvent` (which would + // cancel all gestures handled by its parent root view) but there may be some gestures attached + // to views under it which should work. Adding another root view under that particular view + // would allow the gesture to be recognized even though the parent root view cancelled its gestures. + // We want to stop here so the gesture receives event only once. + false + } else { + when (viewConfigHelper.getPointerEventsConfigForView(view)) { + PointerEventsConfig.NONE -> { + // This view and its children can't be the target + false + } + PointerEventsConfig.BOX_ONLY -> { + // This view is the target, its children don't matter + ( + recordViewHandlersForPointer(view, coords, pointerId, event) || + shouldHandlerlessViewBecomeTouchTarget(view, coords) + ) + } + PointerEventsConfig.BOX_NONE -> { + // This view can't be the target, but its children might + when (view) { + is ViewGroup -> { + extractGestureHandlers(view, coords, pointerId, event).also { found -> + // A child view is handling touch, also extract handlers attached to this view + if (found) { + recordViewHandlersForPointer(view, coords, pointerId, event) + } } } + // When has editable set to `false` getPointerEventsConfigForView returns + // `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract + // handlers attached to the text input, as it makes sense that gestures would work on a + // non-editable TextInput. + is EditText -> { + recordViewHandlersForPointer(view, coords, pointerId, event) + } + else -> false } - // When has editable set to `false` getPointerEventsConfigForView returns - // `BOX_NONE` as it's `isEnabled` property is false. In this case we still want to extract - // handlers attached to the text input, as it makes sense that gestures would work on a - // non-editable TextInput. - is EditText -> { - recordViewHandlersForPointer(view, coords, pointerId, event) - } - else -> false - } - } - PointerEventsConfig.AUTO -> { - // Either this view or one of its children is the target - val found = if (view is ViewGroup) { - extractGestureHandlers(view, coords, pointerId, event) - } else { - false } + PointerEventsConfig.AUTO -> { + // Either this view or one of its children is the target + val found = if (view is ViewGroup) { + extractGestureHandlers(view, coords, pointerId, event) + } else { + false + } - ( - recordViewHandlersForPointer(view, coords, pointerId, event) || - found || - shouldHandlerlessViewBecomeTouchTarget(view, coords) - ) + ( + recordViewHandlersForPointer(view, coords, pointerId, event) || + found || + shouldHandlerlessViewBecomeTouchTarget(view, coords) + ) + } } } diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 54cc1a2d7d..d2a78f06e9 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -13,10 +13,12 @@ import com.facebook.react.views.view.ReactViewGroup class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { private var rootViewEnabled = false + private var unstableForceActive = false private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit + override fun onAttachedToWindow() { super.onAttachedToWindow() - rootViewEnabled = !hasGestureHandlerEnabledRootView(this) + rootViewEnabled = unstableForceActive || !hasGestureHandlerEnabledRootView(this) if (!rootViewEnabled) { Log.i( ReactConstants.TAG, @@ -56,6 +58,12 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { rootHelper?.activateNativeHandlers(view) } + fun isRootViewEnabled() = rootViewEnabled + + fun setUnstableForceActive(active: Boolean) { + this.unstableForceActive = active + } + companion object { private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean { UiThreadUtil.assertOnUiThread() diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt index 381d31d3ab..f209a7c407 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt @@ -4,6 +4,7 @@ import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerDelegate import com.facebook.react.viewmanagers.RNGestureHandlerRootViewManagerInterface @@ -32,6 +33,11 @@ class RNGestureHandlerRootViewManager : view.tearDown() } + @ReactProp(name = "unstable_forceActive") + override fun setUnstable_forceActive(view: RNGestureHandlerRootView, active: Boolean) { + view.setUnstableForceActive(active) + } + /** * The following event configuration is necessary even if you are not using * GestureHandlerRootView component directly. diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx index 6b5eb466cf..a30fabd75b 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.android.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; import { PropsWithChildren } from 'react'; -import { ViewProps, StyleSheet } from 'react-native'; +import { StyleSheet } from 'react-native'; import { maybeInitializeFabric } from '../init'; import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext'; +import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent'; import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent'; export interface GestureHandlerRootViewProps - extends PropsWithChildren {} + extends PropsWithChildren {} export default function GestureHandlerRootView({ style, diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx index 57d3f77a83..66a3091cf2 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { PropsWithChildren } from 'react'; -import { View, ViewProps, StyleSheet } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import { maybeInitializeFabric } from '../init'; import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext'; +import type { RootViewNativeProps } from '../specs/RNGestureHandlerRootViewNativeComponent'; export interface GestureHandlerRootViewProps - extends PropsWithChildren {} + extends PropsWithChildren {} export default function GestureHandlerRootView({ style, diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerRootViewNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerRootViewNativeComponent.ts index e92061f1f3..95f7d20f8e 100644 --- a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerRootViewNativeComponent.ts +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerRootViewNativeComponent.ts @@ -1,6 +1,10 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { ViewProps } from 'react-native'; -interface NativeProps extends ViewProps {} +export interface RootViewNativeProps extends ViewProps { + unstable_forceActive?: boolean; +} -export default codegenNativeComponent('RNGestureHandlerRootView'); +export default codegenNativeComponent( + 'RNGestureHandlerRootView' +);