diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 5794462956..0457c7fa8f 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -6,6 +6,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.EditText +import com.swmansion.gesturehandler.react.RNGestureHandlerRootView import java.util.* class GestureHandlerOrchestrator( @@ -497,6 +498,17 @@ class GestureHandlerOrchestrator( } private fun extractGestureHandlers(viewGroup: ViewGroup, coords: FloatArray, pointerId: Int): Boolean { + if (viewGroup is RNGestureHandlerRootView && viewGroup != wrapperView && viewGroup.isActive()) { + // 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) @@ -523,8 +535,19 @@ class GestureHandlerOrchestrator( return false } - private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int): Boolean = - when (viewConfigHelper.getPointerEventsConfigForView(view)) { + private fun traverseWithPointerEvents(view: View, coords: FloatArray, pointerId: Int): Boolean { + if (view is RNGestureHandlerRootView && view != wrapperView && view.isActive()) { + // 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 + } + + return when (viewConfigHelper.getPointerEventsConfigForView(view)) { PointerEventsConfig.NONE -> { // This view and its children can't be the target false @@ -569,6 +592,7 @@ class GestureHandlerOrchestrator( ) } } + } private fun canReceiveEvents(view: View) = view.visibility == View.VISIBLE && view.alpha >= minimumAlphaForTraversal diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt index 953d3f3d74..e2c86d8d73 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt @@ -12,10 +12,12 @@ import com.facebook.react.views.view.ReactViewGroup class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { private var _enabled = false + private var active = false private var rootHelper: RNGestureHandlerRootHelper? = null // TODO: resettable lateinit + override fun onAttachedToWindow() { super.onAttachedToWindow() - _enabled = !hasGestureHandlerEnabledRootView(this) + _enabled = active || !hasGestureHandlerEnabledRootView(this) if (!_enabled) { Log.i( ReactConstants.TAG, @@ -43,6 +45,17 @@ class RNGestureHandlerRootView(context: Context?) : ReactViewGroup(context) { super.requestDisallowInterceptTouchEvent(disallowIntercept) } + // Those methods refer to different variables because we need to enforce at least + // one of the Root Views to be enabled to intecept events. By default only the + // top-most view is enabled, but users may want to enable more views to be able + // to work around some native views calling `requestDisallowInterceptTouchEvent`, + // which prevents gestures from working. + fun isActive() = _enabled + + fun setActive(active: Boolean) { + this.active = active + } + companion object { private fun hasGestureHandlerEnabledRootView(viewGroup: ViewGroup): Boolean { UiThreadUtil.assertOnUiThread() diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt index 6adf728974..f8c04c3fa6 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt +++ b/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 @@ -34,6 +35,11 @@ class RNGestureHandlerRootViewManager : view.tearDown() } + @ReactProp(name = "active") + override fun setActive(view: RNGestureHandlerRootView, active: Boolean) { + view.setActive(active) + } + /** * The following event configuration is necessary even if you are not using * GestureHandlerRootView component directly. diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java index d7d7222571..1b5e6c1f4a 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java @@ -20,6 +20,12 @@ public RNGestureHandlerRootViewManagerDelegate(U viewManager) { } @Override public void setProperty(T view, String propName, @Nullable Object value) { - super.setProperty(view, propName, value); + switch (propName) { + case "active": + mViewManager.setActive(view, value == null ? false : (boolean) value); + break; + default: + super.setProperty(view, propName, value); + } } } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java index d82d5a0450..698b340413 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java @@ -12,5 +12,5 @@ import android.view.View; public interface RNGestureHandlerRootViewManagerInterface { - // No props + void setActive(T view, boolean value); } diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 7e1743dccb..5a46c2a9ab 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -66,6 +66,10 @@ Note that `GestureHandlerRootView` acts like a normal `View`. So if you want it 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. +::: + ### Linking > **Important**: You only need to do this step if you're using React Native 0.59 or lower. Since v0.60, linking happens automatically. diff --git a/src/fabric/RNGestureHandlerRootViewNativeComponent.ts b/src/fabric/RNGestureHandlerRootViewNativeComponent.ts index e92061f1f3..72c2627b41 100644 --- a/src/fabric/RNGestureHandlerRootViewNativeComponent.ts +++ b/src/fabric/RNGestureHandlerRootViewNativeComponent.ts @@ -1,6 +1,8 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { ViewProps } from 'react-native'; -interface NativeProps extends ViewProps {} +interface NativeProps extends ViewProps { + active?: boolean; +} export default codegenNativeComponent('RNGestureHandlerRootView');