react-native-gesture-handler: Nested touchables don't work correctly on Android

Expected Behavior

Kapture 2019-10-05 at 16 34 43

Current Behavior

Kapture 2019-10-05 at 16 57 32

Description

When you have nested touchables from react-native-gesture-handler and press the inner touchable, the outer one also gets pressed.

Reproduction (Android): https://snack.expo.io/@brunolemos/rngh-nested-touchable-android-bug

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 19
  • Comments: 37 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Passing disallowInterruption={true} to the touchable should fix this.

<TouchableOpacity {...props} disallowInterruption={true} />,

Should this be enabled by default in GenericTouchable?

I tried TouchableOpacity from ‘react-native’ lib it works fine

I am still having this problem on 1.6.1

1.8.0 same problem

Any fix for this? I am experiencing it as well. Upgraded our react-native-gesture-handler version to 1.4.1 and still seeing the same problem. Everything works fine in iOS but Android is trigger both onPress events for the two separate touchable events.

I think this issue also affects if you try to use createNativeStackNavigator from react-native-screens, because when I set that stack navigator all my Touchables from react-native-gesture-handler stops working

I update patch to working with react-native-gesture-handler 1.6.1

diff --git a/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java b/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
index ca69016..fdfa662 100644
--- a/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
+++ b/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
@@ -4,6 +4,7 @@ import android.os.SystemClock;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager;

 public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHandler> {

@@ -13,7 +14,17 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
   public NativeViewGestureHandler() {
     setShouldCancelWhenOutside(true);
   }
-
+  private boolean isPreventedFromBeginning() {
+    // RNGestureHandlerButtonViewManager is connected with logic
+    // related to handling exclusive touches which should prevent
+    // another buttons from beginning gesture recognition.
+    View view = getView();
+    if (view instanceof RNGestureHandlerButtonViewManager.ButtonViewGroup) {
+      // setting flag for an exclusive touch for buttons
+      return !((RNGestureHandlerButtonViewManager.ButtonViewGroup) view).setResponder();
+    }
+    return false;
+  }
   public NativeViewGestureHandler setShouldActivateOnStart(boolean shouldActivateOnStart) {
     mShouldActivateOnStart = shouldActivateOnStart;
     return this;
@@ -85,7 +96,7 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
       } else if (tryIntercept(view, event)) {
         view.onTouchEvent(event);
         activate();
-      } else if (state != STATE_BEGAN) {
+      } else if (state != STATE_BEGAN && !isPreventedFromBeginning()) {
         begin();
       }
     } else if (state == STATE_ACTIVE) {
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
index fd625bd..250f1c3 100644
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
@@ -21,11 +21,11 @@ import com.facebook.react.uimanager.annotations.ReactProp;
 public class RNGestureHandlerButtonViewManager extends
         ViewGroupManager<RNGestureHandlerButtonViewManager.ButtonViewGroup> {

-  static class ButtonViewGroup extends ViewGroup {
+  public static class ButtonViewGroup extends ViewGroup {

     static TypedValue sResolveOutValue = new TypedValue();
     static ButtonViewGroup sResponder;
-
+    private boolean mExclusive = true;
     int mBackgroundColor = Color.TRANSPARENT;
     // Using object because of handling null representing no value set.
     Integer mRippleColor;
@@ -44,6 +44,10 @@ public class RNGestureHandlerButtonViewManager extends
       mNeedBackgroundUpdate = true;
     }

+    public void setExclusive(boolean exclusive) {
+      mExclusive = exclusive;
+    }
+
     @Override
     public void setBackgroundColor(int color) {
       mBackgroundColor = color;
@@ -55,6 +59,16 @@ public class RNGestureHandlerButtonViewManager extends
       mNeedBackgroundUpdate = true;
     }

+    public boolean setResponder() {
+      if (sResponder == null) {
+        if (mExclusive) {
+          sResponder = this;
+        }
+        return true;
+      }
+      return false;
+    }
+
     public void setBorderRadius(float borderRadius) {
       mBorderRadius = borderRadius * (float)getResources().getDisplayMetrics().density;
       mNeedBackgroundUpdate = true;
@@ -173,7 +187,7 @@ public class RNGestureHandlerButtonViewManager extends
         // first button to be pressed grabs button responder
         sResponder = this;
       }
-      if (!pressed || sResponder == this) {
+      if (!pressed || sResponder == this || (sResponder == null && !mExclusive)) {
         // we set pressed state only for current responder
         super.setPressed(pressed);
       }
@@ -225,6 +239,11 @@ public class RNGestureHandlerButtonViewManager extends
     view.setRippleColor(rippleColor);
   }

+  @ReactProp(name = "exclusive", defaultBoolean = true)
+  public void setExclusive(ButtonViewGroup view, boolean exclusive) {
+    view.setExclusive(exclusive);
+  }
+
   @Override
   protected void onAfterUpdateTransaction(ButtonViewGroup view) {
     view.updateBackground();

I switched to using the Button components which have basically all the feedback styles you’d want but more importantly they work when nested.

still an issue in ^1.9.0.

same issue 1.9.0 in android touch events bubble inside multiple overlapping touch areas

for Android, import the outer component from ‘react-native’ import { TouchableOpacity as NativeTouchableOpacity, Platform } from ‘react-native’; import { TouchableOpacity as GHTouchableOpacity } from ‘react-native-gesture-handler’ const TouchableOpacity = Platform.OS === ‘ios’ ? GHTouchableOpacity : NativeTouchableOpacity;

#486 fixes this bug 🎉

Here’s the patch that can be applied using patch-package:

patches/react-native-gesture-handler+1.5.2.patch
diff --git a/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/androidTest/debug/com/swmansion/gesturehandler/react/test/BuildConfig.java b/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/androidTest/debug/com/swmansion/gesturehandler/react/test/BuildConfig.java
new file mode 100644
index 0000000..dc64a01
--- /dev/null
+++ b/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/androidTest/debug/com/swmansion/gesturehandler/react/test/BuildConfig.java
@@ -0,0 +1,13 @@
+/**
+ * Automatically generated file. DO NOT MODIFY
+ */
+package com.swmansion.gesturehandler.react.test;
+
+public final class BuildConfig {
+  public static final boolean DEBUG = Boolean.parseBoolean("true");
+  public static final String APPLICATION_ID = "com.swmansion.gesturehandler.react.test";
+  public static final String BUILD_TYPE = "debug";
+  public static final String FLAVOR = "";
+  public static final int VERSION_CODE = 1;
+  public static final String VERSION_NAME = "1.0";
+}
diff --git a/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/debug/com/swmansion/gesturehandler/react/BuildConfig.java b/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/debug/com/swmansion/gesturehandler/react/BuildConfig.java
new file mode 100644
index 0000000..bd6dac7
--- /dev/null
+++ b/node_modules/react-native-gesture-handler/android/build/generated/source/buildConfig/debug/com/swmansion/gesturehandler/react/BuildConfig.java
@@ -0,0 +1,18 @@
+/**
+ * Automatically generated file. DO NOT MODIFY
+ */
+package com.swmansion.gesturehandler.react;
+
+public final class BuildConfig {
+  public static final boolean DEBUG = Boolean.parseBoolean("true");
+  public static final String LIBRARY_PACKAGE_NAME = "com.swmansion.gesturehandler.react";
+  /**
+   * @deprecated APPLICATION_ID is misleading in libraries. For the library package name use LIBRARY_PACKAGE_NAME
+   */
+  @Deprecated
+  public static final String APPLICATION_ID = "com.swmansion.gesturehandler.react";
+  public static final String BUILD_TYPE = "debug";
+  public static final String FLAVOR = "";
+  public static final int VERSION_CODE = 1;
+  public static final String VERSION_NAME = "1.0";
+}
diff --git a/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java b/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
index ca69016..0a38f5b 100644
--- a/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
+++ b/node_modules/react-native-gesture-handler/android/lib/src/main/java/com/swmansion/gesturehandler/NativeViewGestureHandler.java
@@ -5,6 +5,8 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager;
+
 public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHandler> {
 
   private boolean mShouldActivateOnStart;
@@ -34,6 +36,18 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
     return super.shouldRequireToWaitForFailure(handler);
   }
 
+  private boolean isPreventedFromBeginning() {
+    // RNGestureHandlerButtonViewManager is connected with logic
+    // related to handling exclusive touches which should prevent
+    // another buttons from beginning gesture recognition.
+    View view = getView();
+    if (view instanceof RNGestureHandlerButtonViewManager.ButtonViewGroup) {
+      // setting flag for an exclusive touch for buttons
+      return !((RNGestureHandlerButtonViewManager.ButtonViewGroup) view).setResponder();
+    }
+    return false;
+  }
+
   @Override
   public boolean shouldRecognizeSimultaneously(GestureHandler handler) {
     if (handler instanceof NativeViewGestureHandler) {
@@ -53,7 +67,7 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
     int otherState = handler.getState();
 
     if (state == STATE_ACTIVE && otherState == STATE_ACTIVE && canBeInterrupted) {
-      // if both handlers are active and the current handler can be interruped it we return `false`
+      // if both handlers are active and the current handler can be interrupted it we return `false`
       // as it means the other handler has turned active and returning `true` would prevent it from
       // interrupting the current handler
       return false;
@@ -85,8 +99,9 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
       } else if (tryIntercept(view, event)) {
         view.onTouchEvent(event);
         activate();
-      } else if (state != STATE_BEGAN) {
+      } else if (state != STATE_BEGAN && !isPreventedFromBeginning()) {
         begin();
+
       }
     } else if (state == STATE_ACTIVE) {
       view.onTouchEvent(event);
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
index fd625bd..c44aff2 100644
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.java
@@ -21,7 +21,7 @@ import com.facebook.react.uimanager.annotations.ReactProp;
 public class RNGestureHandlerButtonViewManager extends
         ViewGroupManager<RNGestureHandlerButtonViewManager.ButtonViewGroup> {
 
-  static class ButtonViewGroup extends ViewGroup {
+  public static class ButtonViewGroup extends ViewGroup {
 
     static TypedValue sResolveOutValue = new TypedValue();
     static ButtonViewGroup sResponder;
@@ -32,6 +32,7 @@ public class RNGestureHandlerButtonViewManager extends
     boolean mUseForeground = false;
     boolean mUseBorderless = false;
     float mBorderRadius = 0;
+    private boolean mExclusive = true;
     boolean mNeedBackgroundUpdate = false;
 
 
@@ -50,6 +51,10 @@ public class RNGestureHandlerButtonViewManager extends
       mNeedBackgroundUpdate = true;
     }
 
+    public void setExclusive(boolean exclusive) {
+      mExclusive = exclusive;
+    }
+
     public void setRippleColor(Integer color) {
       mRippleColor = color;
       mNeedBackgroundUpdate = true;
@@ -167,14 +172,20 @@ public class RNGestureHandlerButtonViewManager extends
       }
     }
 
+    public boolean setResponder() {
+      if (sResponder == null) {
+        if (mExclusive) {
+          sResponder = this;
+        }
+        return true;
+      }
+      return false;
+    }
+
     @Override
     public void setPressed(boolean pressed) {
-      if (pressed && sResponder == null) {
-        // first button to be pressed grabs button responder
-        sResponder = this;
-      }
-      if (!pressed || sResponder == this) {
-        // we set pressed state only for current responder
+      if (!pressed || sResponder == this || (sResponder == null && !mExclusive)) {
+        // we set pressed state only for current responder if exclusive
         super.setPressed(pressed);
       }
       if (!pressed && sResponder == this) {
@@ -225,6 +236,11 @@ public class RNGestureHandlerButtonViewManager extends
     view.setRippleColor(rippleColor);
   }
 
+  @ReactProp(name = "exclusive", defaultBoolean = true)
+  public void setExclusive(ButtonViewGroup view, boolean exclusive) {
+    view.setExclusive(exclusive);
+  }
+
   @Override
   protected void onAfterUpdateTransaction(ButtonViewGroup view) {
     view.updateBackground();

Seems like the issue happens on web too. I made a snack repro that shows the different behaviour with Touchable from react-native and react-native-gesture-handler. https://snack.expo.dev/JEnwHDo1h