React-native: React Native Android - PanResponder - gestures not working as expected

Created on 7 Sep 2016  路  11Comments  路  Source: facebook/react-native

PanResponder - gestures not working as expected

Hi,

if you run this code it will work great on IOS but not on Android.
its look like the pan gestures are not working as expected.
the move not working the ListView stack at the bottom and if you touch for some time
it will go up.

Also you can see this project:
https://github.com/paramaggarwal/rn-paper-interface

This is the code below:
import React, {Component} from 'react';
import {
Animated,
Image,
Platform,
Dimensions,
ScrollView,
StatusBar,
StyleSheet,
PanResponder,
ListView,
Text,
View,
Slider
} from 'react-native';

const deviceHeight = Dimensions.get('window').height;
const deviceWidth = Dimensions.get('window').width; //full width

const videoWidth = deviceWidth;
const videoHeight = Math.round((deviceWidth / 16) * 9);

let AnimatedListView = Animated.createAnimatedComponent(ListView);

let panDiff = 240;

let cards = ["http://wdtprs.com/blog/wp-content/uploads/2015/05/C__Data_Users_DefApps_AppData_INTERNETEXPLORER_Temp_Saved-Images_0tmX52a-Imgur_zpsu8dmz3za.jpg",
"http://cdn3-www.comingsoon.net/assets/uploads/gallery/marvels-daredevil-1413056220/marvels-daredevil-poster-2.jpg",
"http://nerdreactor.com/wp-content/uploads/2016/06/Narcos.jpg"];

export default class TVPaper extends Component {

constructor(props) {
    super(props);

    let pan = new Animated.ValueXY();
    let ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });

    this.state = {
        isDocked: true,
        pan: pan,
        dataSource: ds.cloneWithRows(cards),
        dockAnimation: pan.y.interpolate({
            inputRange: [-panDiff, 0],
            outputRange: [0, 1],
            extrapolate: 'clamp'
        })
    }

    this._panResponder = PanResponder.create({
        onStartShouldSetPanResponder: (evt, gestureState) => true,
        onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
        onMoveShouldSetPanResponder: (evt, gestureState) => true,
        onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
        onPanResponderGrant: this._handleOnPanResponderGrant.bind(this),
        onPanResponderMove: this._handleOnPanResponderMove.bind(this),
        onPanResponderRelease: this._handlePanResponderRelease.bind(this)
    })

}

_handleOnPanResponderGrant() {

}

_handleOnPanResponderMove(evt, gestureState) {
    console.log("moving...");
    console.log("x", this.state.pan.x);
    console.log("y", this.state.pan.y);
    console.log("evt",gestureState.moveY);
    Animated.event([null, { dx: this.state.pan.x, dy: this.state.pan.y }])
}

_handlePanResponderRelease(evt, gestureState) {
    console.log("release");
    let shouldToggle = this.state.isDocked ? (gestureState.dy < (-panDiff / 3)) : (gestureState.dy > (panDiff / 3))
    if (!shouldToggle) {
        // return to original position
        Animated.spring(this.state.pan.y, {
            toValue: this.state.isDocked ? 0 : 0
        }).start();
    } else {
        // toggle between docked and zoomed
        Animated.spring(this.state.pan.y, {
            toValue: this.state.isDocked ? (-panDiff) : panDiff
        }).start(() => {

            this.setState({
                isDocked: !this.state.isDocked,
                dockAnimation: !this.state.isDocked ? this.state.pan.y.interpolate({
                    inputRange: [-panDiff, 0],
                    outputRange: [0, 1],
                    extrapolate: 'clamp'
                }) : this.state.pan.y.interpolate({
                    inputRange: [0, panDiff],
                    outputRange: [0, 1],
                    extrapolate: 'clamp'
                })
            });
        })
    }
}


getListViewStyle() {
    return [
        styles.container,
        {
            borderWidth: 1,
            borderColor: 'red',
            width: this.state.dockAnimation.interpolate({
                inputRange: [0, 1],
                outputRange: [Dimensions.get('window').width, Dimensions.get('window').width * 2]
            }),
        },
        {
            transform: [
                {
                    scale: this.state.dockAnimation.interpolate({
                        inputRange: [0, 1],
                        outputRange: [1, 0.5]
                    }),
                },
                {
                    translateX: this.state.dockAnimation.interpolate({
                        inputRange: [0, 1],
                        outputRange: [0, -(Dimensions.get('window').width)]
                    }),
                },
                {
                    translateY: this.state.dockAnimation.interpolate({
                        inputRange: [0, 1],
                        outputRange: [0, (Dimensions.get('window').height / 2)]
                    }),
                }
            ],
        }
    ];
}

render() {
    return (
        <View style={styles.box}>

            <AnimatedListView
                horizontal={true}
                pagingEnabled={!this.state.isDocked}

                style={this.getListViewStyle() }
                {...this._panResponder.panHandlers}

                dataSource={this.state.dataSource}
                renderRow={  (rowData, i) => {
                    return (
                        <View style={{ flex: 1 }} key={i}>
                            <Image style={{ width: deviceWidth, height: deviceHeight, resizeMode: 'cover' }} source={{ uri: rowData }} />
                        </View>
                    )
                } }
                />
        </View>
    );
}

}

const styles = StyleSheet.create({
box: {
flex: 1,
backgroundColor: 'black',
},
container: {
flex: 1,
},
});

Locked

Most helpful comment

@yanirmanor I was faced with the same issue. Resolved by passing return true in each handler, like:
onStartShouldSetPanResponder: (e, gestureState) => {
console.log(gestureState);
return true;
},
onPanResponderEnd: (e, gestureState) => {
console.log(gestureState);
return true;
}

All 11 comments

+1

@yanirmanor I was faced with the same issue. Resolved by passing return true in each handler, like:
onStartShouldSetPanResponder: (e, gestureState) => {
console.log(gestureState);
return true;
},
onPanResponderEnd: (e, gestureState) => {
console.log(gestureState);
return true;
}

@Arkanine that not working.
can you pls send example that work.

@yanirmanor it works for me, the full example you can use is:
componentWillMount: function() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (e, gestureState) => {
console.log(gestureState);
return true;
},
onPanResponderEnd: (e, gestureState) => {
console.log(gestureState);
return true;
}
})
}

Its not working!
Please attached or write the code that works on Android.

import React, { Component,PropTypes} from 'react';
import {
StyleSheet,
View,
Dimensions,
Animated,
AppRegistry,
Alert,
Modal,
Text,
TouchableHighlight
} from 'react-native';
var alertMessage = 'Hello'
import MapView from 'react-native-maps';
import PanController from './PanController';
import PriceMarker from './AnimatedPriceMarker';

const screen = Dimensions.get('window');

const ASPECT_RATIO = screen.width / screen.height;
const LATITUDE = 37.78825;
const LONGITUDE = -122.4324;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

const ITEM_SPACING = 10;
const ITEM_PREVIEW = 10;
const ITEM_WIDTH = screen.width - (2 * ITEM_SPACING) - (2 * ITEM_PREVIEW);
const SNAP_WIDTH = ITEM_WIDTH + ITEM_SPACING;
const ITEM_PREVIEW_HEIGHT = 40;
const SCALE_END = screen.width / ITEM_WIDTH;
const BREAKPOINT1 = 246;
const BREAKPOINT2 = 350;
const ONE = new Animated.Value(1);

function getMarkerState(panX, panY, scrollY, i) {
const xLeft = (-SNAP_WIDTH * i) + (SNAP_WIDTH / 2);
const xRight = (-SNAP_WIDTH * i) - (SNAP_WIDTH / 2);
const xPos = -SNAP_WIDTH * i;

const isIndex = panX.interpolate({
inputRange: [xRight - 1, xRight, xLeft, xLeft + 1],
outputRange: [0, 1, 1, 0],
extrapolate: 'clamp',
});

const isNotIndex = panX.interpolate({
inputRange: [xRight - 1, xRight, xLeft, xLeft + 1],
outputRange: [1, 0, 0, 1],
extrapolate: 'clamp',
});

const center = panX.interpolate({
inputRange: [xPos - 10, xPos, xPos + 10],
outputRange: [0, 1, 0],
extrapolate: 'clamp',
});

const selected = panX.interpolate({
inputRange: [xRight, xPos, xLeft],
outputRange: [0, 1, 0],
extrapolate: 'clamp',
});

const translateY = Animated.multiply(isIndex, panY);

const translateX = panX;

const anim = Animated.multiply(isIndex, scrollY.interpolate({
inputRange: [0, BREAKPOINT1],
outputRange: [0, 1],
extrapolate: 'clamp',
}));

const scale = Animated.add(ONE, Animated.multiply(isIndex, scrollY.interpolate({
inputRange: [BREAKPOINT1, BREAKPOINT2],
outputRange: [0, SCALE_END - 1],
extrapolate: 'clamp',
})));

// [0 => 1]
let opacity = scrollY.interpolate({
inputRange: [BREAKPOINT1, BREAKPOINT2],
outputRange: [0, 1],
extrapolate: 'clamp',
});

// if i === index: [0 => 0]
// if i !== index: [0 => 1]
opacity = Animated.multiply(isNotIndex, opacity);

// if i === index: [1 => 1]
// if i !== index: [1 => 0]
opacity = opacity.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
});

let markerOpacity = scrollY.interpolate({
inputRange: [0, BREAKPOINT1],
outputRange: [0, 1],
extrapolate: 'clamp',
});

markerOpacity = Animated.multiply(isNotIndex, markerOpacity).interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
});

const markerScale = selected.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.2],
});

return {
translateY,
translateX,
scale,
opacity,
anim,
center,
selected,
markerOpacity,
markerScale,
};
}

class AnimatedViews extends React.Component {
constructor(props) {
super(props);

const panX = new Animated.Value(0);
const panY = new Animated.Value(0);

const scrollY = panY.interpolate({
  inputRange: [-1, 1],
  outputRange: [1, -1],
});

const scrollX = panX.interpolate({
  inputRange: [-1, 1],
  outputRange: [1, -1],
});

const scale = scrollY.interpolate({
  inputRange: [0, BREAKPOINT1],
  outputRange: [1, 1.6],
  extrapolate: 'clamp',
});

const translateY = scrollY.interpolate({
  inputRange: [0, BREAKPOINT1],
  outputRange: [0, -100],
  extrapolate: 'clamp',
});

const markers = [
  {
    id: 0,
    amount: 99,
    coordinate: {
      latitude: LATITUDE,
      longitude: LONGITUDE,
    },
  },
  {
    id: 1,
    amount: 199,
    coordinate: {
      latitude: LATITUDE + 0.004,
      longitude: LONGITUDE - 0.004,
    },
  },
  {
    id: 2,
    amount: 285,
    coordinate: {
      latitude: LATITUDE - 0.004,
      longitude: LONGITUDE - 0.004,
    },
  },
];


const animations = markers.map((m, i) =>
  getMarkerState(panX, panY, scrollY, i));

this.state = {
  modalVisible: false,
  panX,
  panY,
  animations,
  index: 0,
  canMoveHorizontal: true,
  scrollY,
  scrollX,
  scale,
  translateY,
  markers,
  region: new MapView.AnimatedRegion({
    latitude: LATITUDE,
    longitude: LONGITUDE,
    latitudeDelta: LATITUDE_DELTA,
    longitudeDelta: LONGITUDE_DELTA,
  }),
};
setModalVisible=(visible)=> {
   this.setState({modalVisible: visible});
 }

}

componentDidMount() {
const { region, panX, panY, scrollX, markers } = this.state;

panX.addListener(this.onPanXChange);
panY.addListener(this.onPanYChange);

region.stopAnimation();
region.timing({
  latitude: scrollX.interpolate({
    inputRange: markers.map((m, i) => i * SNAP_WIDTH),
    outputRange: markers.map(m => m.coordinate.latitude),
  }),
  longitude: scrollX.interpolate({
    inputRange: markers.map((m, i) => i * SNAP_WIDTH),
    outputRange: markers.map(m => m.coordinate.longitude),
  }),
  duration: 0,
}).start();

}

onStartShouldSetPanResponder=(e) => {
console.log("value of geasture e",e);
// we only want to move the view if they are starting the gesture on top
// of the view, so this calculates that and returns true if so. If we return
// false, the gesture should get passed to the map view appropriately.
const { panY } = this.state;
const { pageY } = e.nativeEvent;
const topOfMainWindow = ITEM_PREVIEW_HEIGHT + panY.__getValue();
const topOfTap = screen.height - pageY;
return topOfTap < topOfMainWindow;
console.log("onStartShouldSetPanResponder",topOfTap,topOfMainWindow);
//setModalVisible(true);
/Alert.alert(
'Alert Title',
alertMessage,
[{text: 'Closed', onPress: () => setModalVisible(true)},]
)
/
}

onMoveShouldSetPanResponder =(e)=> {
console.log("value of geasture e",e);
const { panY } = this.state;
const { pageY } = e.nativeEvent;
const topOfMainWindow = ITEM_PREVIEW_HEIGHT + panY.__getValue();
const topOfTap = screen.height - pageY;
console.log("onMoveShouldSetPanResponder",topOfTap,topOfMainWindow);
return topOfTap < topOfMainWindow;
}

onPanXChange({ value }) {
const { index } = this.state;
const newIndex = Math.floor(((-1 * value) + (SNAP_WIDTH / 2)) / SNAP_WIDTH);
if (index !== newIndex) {
this.setState({ index: newIndex });
}
}

onPanYChange({ value }) {
const { canMoveHorizontal, region, scrollY, scrollX, markers, index } = this.state;
const shouldBeMovable = Math.abs(value) < 2;
if (shouldBeMovable !== canMoveHorizontal) {
this.setState({ canMoveHorizontal: shouldBeMovable });
if (!shouldBeMovable) {
const { coordinate } = markers[index];
region.stopAnimation();
region.timing({
latitude: scrollY.interpolate({
inputRange: [0, BREAKPOINT1],
outputRange: [
coordinate.latitude,
coordinate.latitude - (LATITUDE_DELTA * 0.5 * 0.375),
],
extrapolate: 'clamp',
}),
latitudeDelta: scrollY.interpolate({
inputRange: [0, BREAKPOINT1],
outputRange: [LATITUDE_DELTA, LATITUDE_DELTA * 0.5],
extrapolate: 'clamp',
}),
longitudeDelta: scrollY.interpolate({
inputRange: [0, BREAKPOINT1],
outputRange: [LONGITUDE_DELTA, LONGITUDE_DELTA * 0.5],
extrapolate: 'clamp',
}),
duration: 0,
}).start();
} else {
region.stopAnimation();
region.timing({
latitude: scrollX.interpolate({
inputRange: markers.map((m, i) => i * SNAP_WIDTH),
outputRange: markers.map(m => m.coordinate.latitude),
}),
longitude: scrollX.interpolate({
inputRange: markers.map((m, i) => i * SNAP_WIDTH),
outputRange: markers.map(m => m.coordinate.longitude),
}),
duration: 0,
}).start();
}
}
}

onRegionChange(/* region */) {
// this.state.region.setValue(region);
}

render() {
const {
panX,
panY,
animations,
canMoveHorizontal,
markers,
region,
} = this.state;

return (
  <View style={styles.container}>
  <Modal
     animationType={"slide"}
     transparent={false}
     visible={this.state.modalVisible}
     onRequestClose={() => {alert("Modal has been closed.")}}
     >
    <View style={{marginTop: 22}}>
     <View>
       <Text>Hello World!</Text>

       <TouchableHighlight onPress={() =>setModalVisible(false)
       }>
         <Text>Hide Modal</Text>
       </TouchableHighlight>

     </View>
    </View>
   </Modal>

    <PanController
      style={styles.container}
      vertical
      horizontal
      xMode="snap"
      snapSpacingX={SNAP_WIDTH}
      yBounds={[-1 * screen.height, 0]}
      xBounds={[-screen.width * (markers.length - 1), 0]}
      panY={panY}
      panX={panX}
      onStartShouldSetPanResponder={this.onStartShouldSetPanResponder}
      onMoveShouldSetPanResponder={this.onMoveShouldSetPanResponder}
    >
      <MapView.Animated
        provider={this.props.provider}
        style={styles.map}
        region={region}
        onRegionChange={this.onRegionChange}
      >
        {markers.map((marker, i) => {
          const {
            selected,
            markerOpacity,
            markerScale,
          } = animations[i];

          return (
            <MapView.Marker
              key={marker.id}
              coordinate={marker.coordinate}
            >
              <PriceMarker
                style={{
                  opacity: markerOpacity,
                  transform: [
                    { scale: markerScale },
                  ],
                }}
                amount={marker.amount}
                selected={selected}
              />
            </MapView.Marker>
          );
        })}
      </MapView.Animated>
      <View style={styles.itemContainer}>
        {markers.map((marker, i) => {
          const {
            translateY,
            translateX,
            scale,
            opacity,
          } = animations[i];

          return (
            <Animated.View
              key={marker.id}
              style={[styles.item, {
                opacity,
                transform: [
                  { translateY },
                  { translateX },
                  { scale },
                ],
              }]}
            />
          );
        })}
      </View>
    </PanController>
  </View>
);

}
}

AnimatedViews.propTypes = {
provider: MapView.ProviderPropType,
};

const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
},
itemContainer: {
backgroundColor: 'transparent',
flexDirection: 'row',
paddingHorizontal: (ITEM_SPACING / 2) + ITEM_PREVIEW,
position: 'absolute',
// top: screen.height - ITEM_PREVIEW_HEIGHT - 64,
paddingTop: screen.height - ITEM_PREVIEW_HEIGHT - 64,
//paddingTop: !ANDROID ? 0 : screen.height - ITEM_PREVIEW_HEIGHT - 64,
},
map: {
backgroundColor: 'transparent',
...StyleSheet.absoluteFillObject,
},
item: {
width: ITEM_WIDTH,
height: screen.height + (2 * ITEM_PREVIEW_HEIGHT),
backgroundColor: 'red',
marginHorizontal: ITEM_SPACING / 2,
overflow: 'hidden',
borderRadius: 3,
borderColor: '#000',
},
});

AppRegistry.registerComponent('AnimatedViews', ()=> AnimatedViews);
module.exports = AnimatedViews;

panresponser not working ios and android both

Panresonser move start method geasture property not working properly

onStartShouldSetPanResponder not working on MAPVIEW react native

I have the exact same behavior nativeEvent.locationX and nativeEvent.locationY do not update on Android. pageX and pageY do update on Android. @Arkanine 's suggestion to return true does not fix this.

This issue is pretty unclear. Lets continue the discussion in #12591 because it seems better articulated there

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lazywei picture lazywei  路  3Comments

josev55 picture josev55  路  3Comments

despairblue picture despairblue  路  3Comments

oney picture oney  路  3Comments

grabbou picture grabbou  路  3Comments