Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gesture propagation in nested TapHandlers #1930

Closed
2 tasks done
enzomanuelmangano opened this issue Mar 14, 2022 · 5 comments
Closed
2 tasks done

Gesture propagation in nested TapHandlers #1930

enzomanuelmangano opened this issue Mar 14, 2022 · 5 comments
Labels
BugBash 31.03 Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided

Comments

@enzomanuelmangano
Copy link

enzomanuelmangano commented Mar 14, 2022

Description

Hi, first of all, thank you very much for the work you have done with this package. The issue I am experiencing concerns the propagation of tap events from a child TapHandler to the parent.
In the example presented below, two TapHandlers are visible (created with the GestureDetector component).
The goals of the example are:

  • When a TapHandler is clicked, the backgroundColor turns red;
  • By clicking on the child TapHandler, only its backgroundColor has to change. (Here's the issue)

Platforms

  • iOS
  • Android

Screenshots

Schermata 2022-03-14 alle 20 51 15

Steps To Reproduce

  1. Define two TapHandlers (Parent and Child)
  2. Tap on the Child

The child will propagate the tap gesture to the parent.

Expected behavior

Schermata 2022-03-14 alle 20 50 52

Actual behavior

Schermata 2022-03-14 alle 20 50 11

Snack or minimal code example

import { StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const INITIAL_BG_COLOR = 'gray';

const App: React.FC = () => {
  const backgroundColor = useSharedValue(INITIAL_BG_COLOR);
  const innerBackgroundColor = useSharedValue(INITIAL_BG_COLOR);

  const tapGesture = Gesture.Tap()
    .onTouchesDown(() => {
      backgroundColor.value = 'red';
    })
    .onFinalize(() => {
      backgroundColor.value = INITIAL_BG_COLOR;
    });

  const rButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: backgroundColor.value,
    };
  }, []);

  const innerTapGesture = Gesture.Tap()
    .onTouchesDown(() => {
      innerBackgroundColor.value = 'red';
    })
    .onFinalize(() => {
      innerBackgroundColor.value = INITIAL_BG_COLOR;
    });

  const rInnerButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: innerBackgroundColor.value,
    };
  }, []);

  return (
    <View style={styles.container}>
      <GestureDetector gesture={tapGesture}>
        <Animated.View style={[styles.button, rButtonStyle]}>
          <GestureDetector gesture={innerTapGesture}>
            <Animated.View style={[styles.innerButton, rInnerButtonStyle]} />
          </GestureDetector>
        </Animated.View>
      </GestureDetector>
    </View>
  );
};

export default function AppContainer() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <App />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    height: 200,
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'black',
  },
  innerButton: {
    height: 100,
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    borderWidth: 1,
    borderColor: 'black',
  },
});

Package versions

  • React: 17.0.1
  • React Native: 0.64.3
  • React Native Gesture Handler: 2.3.2
  • React Native Reanimated: 2.4.1
@github-actions github-actions bot added Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided labels Mar 14, 2022
@j-piasecki
Copy link
Member

Hi! This is by design - when you touch the screen, all gestures attached to the views underneath the touch point start tracking the pointer, and they continue to track it (and sent touch events) until the gesture finishes. Similarly, onBegin callback` is called when the gesture starts tracking the first pointer and it's possible that the gesture will be recognized.

I believe that, in your case, using a LongPress gesture simultaneous with Tap will solve the problem. You can configure LongPress to activate immediately, thus cancelling other gestures that are not simultaneous with it, leaving only tap gesture. It may look somewhat like this:

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const INITIAL_BG_COLOR = 'gray';

const App: React.FC = () => {
  const backgroundColor = useSharedValue(INITIAL_BG_COLOR);
  const innerBackgroundColor = useSharedValue(INITIAL_BG_COLOR);

  const tapGesture = Gesture.Tap()
    .onBegin(() => {
      backgroundColor.value = 'red';
    })
    .onFinalize(() => {
      backgroundColor.value = INITIAL_BG_COLOR;
    });

  const rButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: backgroundColor.value,
    };
  }, []);

  const innerTapGesture = Gesture.Tap()
    .onStart(() => {
      console.log('inner tap');
    })
    .onFinalize(() => {
      innerBackgroundColor.value = INITIAL_BG_COLOR;
    });

  const innerLongPress = Gesture.LongPress()
    .minDuration(0)
    .onStart(() => {
      innerBackgroundColor.value = 'red';
    });

  const rInnerButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: innerBackgroundColor.value,
    };
  }, []);

  return (
    <View style={styles.container}>
      <GestureDetector gesture={tapGesture}>
        <Animated.View style={[styles.button, rButtonStyle]}>
          <GestureDetector
            gesture={Gesture.Simultaneous(innerLongPress, innerTapGesture)}>
            <Animated.View style={[styles.innerButton, rInnerButtonStyle]} />
          </GestureDetector>
        </Animated.View>
      </GestureDetector>
    </View>
  );
};

export default function AppContainer() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <App />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    height: 200,
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'black',
  },
  innerButton: {
    height: 100,
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    borderWidth: 1,
    borderColor: 'black',
  },
});

@j-piasecki j-piasecki added Close when stale The issue will be closed automatically if it remains inactive BugBash 31.03 labels Mar 31, 2022
@enzomanuelmangano
Copy link
Author

I apologize for the late reply. Thank you very much for the solution, it works as I expected.

@demedos
Copy link

demedos commented Apr 20, 2023

I am facing a similar issue where nested GestureDetectors are used as children, making it impossible to implement the simultaneous LongPress solution. Are you aware of any alternative methods to accomplish this feature? Here's the updated reproduction code and an expo snack for reference: https://snack.expo.dev/Zl9l9D9jb

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';

const INITIAL_BG_COLOR = 'gray';

const Square: React.FC<{ size: number }> = ({ size, children }) => {
  const backgroundColor = useSharedValue(INITIAL_BG_COLOR);

  const tapGesture = Gesture.Tap()
    .onTouchesDown(() => {
      backgroundColor.value = 'red';
    })
    .onFinalize(() => {
      backgroundColor.value = INITIAL_BG_COLOR;
    });

  const rButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: backgroundColor.value,
    };
  }, []);

  return (
    <GestureDetector gesture={tapGesture}>
      <Animated.View style={[styles.button, rButtonStyle, { height: size }]}>{children}</Animated.View>
    </GestureDetector>
  );
};

const App: React.FC = () => {
  return (
    <View style={styles.container}>
      <Square size={200}>
        <Square size={100} />
      </Square>
    </View>
  );
};

export default function AppContainer() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <App />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'black',
  },
});

@demedos
Copy link

demedos commented Apr 20, 2023

The requirement is the same as #2331, but using the new API.

@j-piasecki
Copy link
Member

@demedos

You could try something like this
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const INITIAL_BG_COLOR = 'gray';

const Square: React.FC<{ size: number; children?: React.ReactNode }> = ({
  size,
  children,
}) => {
  const backgroundColor = useSharedValue(INITIAL_BG_COLOR);

  const tapGesture = Gesture.LongPress()
    .minDuration(0)
    .onStart(() => {
      backgroundColor.value = 'red';
    })
    .onEnd((_, finishedGracefully) => {
      if (finishedGracefully) {
        console.log('tap', size);
      }
    })
    .onFinalize(() => {
      backgroundColor.value = INITIAL_BG_COLOR;
    });

  const rButtonStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: backgroundColor.value,
    };
  }, []);

  return (
    <GestureDetector gesture={tapGesture}>
      <Animated.View style={[styles.button, rButtonStyle, { height: size }]}>
        {children}
      </Animated.View>
    </GestureDetector>
  );
};

const App: React.FC = () => {
  return (
    <View style={styles.container}>
      <Square size={200}>
        <Square size={100} />
      </Square>
    </View>
  );
};

export default function AppContainer() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <App />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    borderRadius: 10,
    aspectRatio: 1,
    backgroundColor: 'gray',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'black',
  },
});

By using LongPress instead of Tap and setting minDuration to 0, both handlers will try to activate at the same time, however, the inner one will have priority and will cancel the outer one. The second argument in the onEnd callback tells whether the gesture has finished on its own, or whether it has been canceled so you can use it to distinguish whether to actually invoke the callback.

The thing you lose in this approach is the cancelation of the gesture after the user holds it for a longer period of time that the Tap gesture provides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugBash 31.03 Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided
Projects
None yet
Development

No branches or pull requests

3 participants