24

I have a parent PanResponder with a child TouchableOpacity. What happens is that the TouchableOpacity doesn't respond to clicks because the PanResponder takes the click. I have tried to follow this guide but no success: http://browniefed.com/blog/react-native-maintain-touchable-items-with-a-parent-panresponder/

this is my code:

this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
            onMoveShouldSetResponderCapture: () => true,
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
                return gestureState.dx != 0 && gestureState.dy != 0;
            },
            onPanResponderGrant: (evt, gestureState) => {
                let isFirst = gestureState.y0 > 164 ? false : true;
                this.setState({animObj: isFirst, isUsingCurtain: true});
            },
            onPanResponderMove: (evt, gestureState) => {

                //let Y = this.state.animObj ? gestureState.moveY - this.state.currentHeaderHeight  : gestureState.moveY - this.state.currentHeaderHeight ;// - this.state.currentHeaderHeight;
                let Y = gestureState.moveY - this.state.currentHeaderHeight + 20
                if (Y < 0) {
                    return false
                }
                this.state.animCurtain.setValue(Y);
                gestureState.moveY > height / 2 ? this.setState({curtainOnMiddle: true}) : this.setState({curtainOnMiddle: false})
            },
            onPanResponderTerminationRequest: (evt, gestureState) => true,
            onPanResponderRelease: (evt, gestureState) => {
                if (((height / 2) > gestureState.moveY)) {//slide back up1
                    this._CurtainAnimation(0, false);
                } else {//slide to bottom
                    let val = height - calcSize(180);
                    this._CurtainAnimation(val, true);
                }
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
            onPanResponderStart: (e, gestureState) => {
            },
        });

and this is my View :

<Animated.View
            style={[styles.bottomHPHeader, TopArroOpacity ? {opacity: TopArroOpacity} : ""]} {...this._panResponder.panHandlers}>
            <TouchableOpacity onPress={() => this._animateAutoCurtain()}>
                {this.state.curtainOnMiddle ?
                    <AIIcon image={require('../../../../assets/images/homepage/close_drawer_arrow.png')}
                            boxSize={30}/>
                    : <AIIcon image={require('../../../../assets/images/homepage/open_drawer_arrow.png')}
                              boxSize={30}/>}
            </TouchableOpacity></Animated.View>

Thank you

8 Answers 8

41

The solution for my case was to modify onMoveShouldSetPanResponder

onMoveShouldSetPanResponder: (evt, gestureState) => {
    //return true if user is swiping, return false if it's a single click
                return !(gestureState.dx === 0 && gestureState.dy === 0)                  
}
0
16

Perfect! I just needed to adjust because of the sensitivity.

onMoveShouldSetPanResponder: (evt, gestureState) => {
      const { dx, dy } = gestureState
      return dx > 2 || dx < -2 || dy > 2 || dy < -2
}
2
  • 3
    If I could give you more votes, I would. Thank you! I have fat fingers and couldn't figure out how to adjust for sensitivity. This is perfect May 16, 2019 at 16:22
  • 5
    This one works fine. But instead of checking for negative, I just instead Math.abs: return Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2; Jul 8, 2019 at 12:52
10

I had the same exact issue. The solution was a combination of the above solutions. Had to add a condition for both the onMoveShouldSetPanResponder and onMoveShouldSetPanResponderCapture. Here is what I did.

 onMoveShouldSetPanResponder: (_, gestureState) => {
    const { dx, dy } = gestureState
    return (dx > 2 || dx < -2 || dy > 2 || dy < -2)
  },
  
onMoveShouldSetPanResponderCapture: (_, gestureState) => {
    const { dx, dy } = gestureState
    return (dx > 2 || dx < -2 || dy > 2 || dy < -2)
  },

2
  • 2
    You freaking life saver ! I struggled for hours trying to understand why not a single touchable was working in an AnimatedView that use a PanResponder. It should be written somewhere that nothing inside a panResponder can emmit it's touchable event, unless the PanResponder is "inactive"
    – Topsy
    Oct 6, 2020 at 9:58
  • Happy to help Topsy! You could also try importing TouchableOpacity from 'react-native-gesture-handler'. This may also behave better / fix instances like this. Oct 6, 2020 at 11:15
6
this.panResponder = PanResponder.create({
    onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
});
2

It seems a slightly different situation works for everyone... here is the combination of PanResponder.create properties which worked for me (I was trying to allow taps through to child elements, but capture moves with the PanResponder:

onStartShouldSetPanResponder:        ( e, state ) => false,
onStartShouldSetPanResponderCapture: ( e, state ) => false,
onMoveShouldSetPanResponder:         ( e, state ) => true,
onMoveShouldSetPanResponderCapture:  ( e, state ) => true,

I hope this helps someone else and it doesn't somehow come back to haunt me!

1

I came up to the similar problem but for me having a container view containing the pan view inside while having the button on absolute position & keeping it above pan view did help. It could not fit for everyone but for the cases (like mine) where button can be absolutely positioned, it works.

Here's a sample code of a component that has the pan view and button.

import React, { Component } from "react";
import {
  Animated,
  View,
  Text,
  StyleSheet,
  PanResponder,
  TouchableOpacity
} from "react-native";


export class ToolRuler extends Component {

  constructor(props) {
    super(props);
    this.state = {
      moveX: 0,
      moveY: 0
    };
  }

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
      onMoveShouldSetResponderCapture: () => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        // anything
      },
      onPanResponderMove: (evt, gestureState) => {
        this.setState({
          moveX: gestureState.moveX,
          moveY: gestureState.moveY
        });
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        this.setState({
          moveX: gestureState.moveX,
          moveY: gestureState.moveY
        });
      },
      onPanResponderTerminate: (evt, gestureState) => {},
      onPanResponderStart: (e, gestureState) => {}
    });
  }

  render() {
    return (
      <View
        style={{
          flex: 1,
          position: "relative",
          alignItems: "center",
          justifyContent: "center"
        }}
      >
        <Animated.View
          {...this._panResponder.panHandlers}
          style={{ flex: 1, width: "100%", backgroundColor: "#ccc" }}
        >
          <Text style={{ marginTop: 200, textAlign: "center" }}>
            x: {this.state.moveX}, y: {this.state.moveY}
          </Text>
        </Animated.View>

        <TouchableOpacity
          style={{
            position: "absolute"
          }}
          onPress={() => alert("I am tapped!")}
        >
          <Text>Click Me</Text>
        </TouchableOpacity>
      </View>
    );
  }
}
1

My solution for Android. All others return false

onMoveShouldSetPanResponder: (evt, { dx, dy }) => dx !== 0 || dy !== 0,
0

The above fixes did not work for me but the following did -

set onStartShouldSetPanResponderCapture to false in PanResponder.create

Below is my panResponder code

 const panResponder = React.useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      // We need this code to listen for onPress vs pan events
      onStartShouldSetPanResponderCapture: () => false,
      onMoveShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponderCapture: () => true,
      onPanResponderTerminationRequest: () => true,
      onPanResponderMove: (e, gestureState) => {
        if (gestureState.dy > 0) {
          Animated.event([null, { dy: pan.y }], { useNativeDriver: false })(
            e,
            gestureState,
          );
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        // If pulled down far enough, dismiss modal
        if (gestureState.dy > SWIPE_DISTANCE) onCancel();
        else {
          // If not dismissed slide back to standard height
          Animated.spring(pan, {
            toValue: { x: 0, y: 0 },
            useNativeDriver: false,
          }).start();
        }
      },
      onShouldBlockNativeResponder: () => true,
    }),
  ).current;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.