React-native: [PanResponder] onPanResponderMove locationX/locationY values incorrect

Created on 31 Jul 2017  路  20Comments  路  Source: facebook/react-native

Is this a bug report?

yes

Have you read the Bugs section of the How to Contribute guide?

yes

Environment

  1. react-native -v: 0.43.4
  2. node -v: v6.9.1
  3. npm -v: 3.10.8
  4. yarn --version: 0.18.1

Then, specify:

  • Target Platform: iOS
  • Development Operating System: macOS

  • Build tools: Xcode

Steps to Reproduce

(Write your steps here:)

  1. use panResponder
  2. console.log locationX and locationY of view when I move it in onPanResponderMove

Expected Behavior

the locationX change with the view move

Actual Behavior

locationX do change when view move, but the value is not currect, just like:

locationX : 38.5 locationY : 53.5
locationX : 152.5 locationY : 278.5
locationX : 29.5 locationY : 45
locationX : 138 locationY : 264.5

Reproducible Demo

import React, {PureComponent, Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    PanResponder,
} from 'react-native';

export default class TouchStartAndRelease extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: 'red',
            marginTop: 100,
            marginLeft: 100,
        }
    }

    componentWillMount(){
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => {
                return true;
            },
            onMoveShouldSetPanResponder:  (evt, gestureState) => {
                return true;
            },
            onPanResponderGrant: (evt, gestureState) => {
                this._highlight();
            },
            onPanResponderMove: (evt, gestureState) => {
                console.log(`locationX : ${evt.nativeEvent.locationX}   locationY : ${evt.nativeEvent.locationY}`);
                this.setState({
                        marginLeft: evt.nativeEvent.locationX,
                        marginTop: evt.nativeEvent.locationY,
                });
            },
            onPanResponderRelease: (evt, gestureState) => {
                this._unhighlight();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
        });
    }

    _unhighlight(){
        this.setState({
            backgroundColor: 'red',
        });
    }

    _highlight(){
        this.setState({
            backgroundColor: 'blue',
        });
    }

    render() {
        return (
            <View style={styles.container}>
                <View style={[styles.redView,
                    {
                        backgroundColor: this.state.backgroundColor,
                        marginTop: this.state.marginTop,
                        marginLeft: this.state.marginLeft,
                    }
                ]}
                    {...this._panResponder.panHandlers}
                ></View>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    redView: {
        width: 100,
        height: 100,
    },

});

AppRegistry.registerComponent('TouchStartAndRelease', () => TouchStartAndRelease);
PanResponder Bug iOS Stale

Most helpful comment

Regarding the issue where two clicks within a second will yield two onPanResponderGrant events, the second of which has wildly incorrect locationX/locationY values: this issue is still extant in 0.51. I should note that this seems to be distinct from the onPanResponderMove issue with locationX/locationY, but I assume they are related. I haven't checked that one on 0.51, but I assume it's still going on as well.

Overall, the issue is that the relative positions reported by PanResponder are impossibly to rely on. This forces developers to rely on absolute positions reported by layout calls, but since onLayout does not report absolute positions, messy measure() calls are necessary.

I do not think we should close this issue, as this problem prevents the use of PanResponder for correct relative positioning information. In my opinion, this is the kind of "wtf?" experience that turns off a lot of newcomers from React Native.

All 20 comments

@shergin, your PR is for Android, while this bug report is for iOS. I am seeing this behavior on both iOS and Android. PanResponder sometimes gets into a state where the values of locationX and locationY it's passing on are rather incorrect. pageX and pageY are unaffected.

I am seeing the issue that your PR addressed as well, which is that on Android, onPanResponderMove doesn't update the values of locationX/locationY.

However, there is another distinct issue, that I can repro on both iOS and Android. If you press a location twice in short succession, the second press will register incorrect (and in my case, far smaller) values of locationX and locationY.

@Ashoat Oh, sorry. Unfortunately I don't have a solution for PanResponder, probably it is problem in JS, probably on native side, or on both sides.

I would love to review and land PR fixing this problem, btw. 馃槃

I think this is also related:

see the gif first: https://giphy.com/gifs/3ov9k5OWQQictSMZfG/fullscreen

panResponder is attached to the red box, onPanResponderMove I'm logging the locationX which should be relative to the red box but appears it is not (take a look at the logs in gif)

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.

Regarding the issue where two clicks within a second will yield two onPanResponderGrant events, the second of which has wildly incorrect locationX/locationY values: this issue is still extant in 0.51. I should note that this seems to be distinct from the onPanResponderMove issue with locationX/locationY, but I assume they are related. I haven't checked that one on 0.51, but I assume it's still going on as well.

Overall, the issue is that the relative positions reported by PanResponder are impossibly to rely on. This forces developers to rely on absolute positions reported by layout calls, but since onLayout does not report absolute positions, messy measure() calls are necessary.

I do not think we should close this issue, as this problem prevents the use of PanResponder for correct relative positioning information. In my opinion, this is the kind of "wtf?" experience that turns off a lot of newcomers from React Native.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.

locationX/locationY is relative parent element, that is relative the red and blue color element , not the screen size.so ,it will shake ,change the position quickly

locationX and location y should be relative to the element itself, not parent element. right? see https://facebook.github.io/react-native/docs/panresponder.html:

locationX - The X position of the touch, relative to the element
locationY - The Y position of the touch, relative to the element

@zollipaul Yes,you are right.they relative to the element which you start touching

I am still having this issue - and very interested in a way to find the absolute location of my Animated.Image after I have released the panResponder. This is necessary for me to store where the user has placed the drag/drop element I've created, and then save that position so other users can see their image with the the element in the position the user placed it. If anyone has a workaround to accomplish this please let me know - but none of the values I'm receiving from my Event or GestureState can be computed in a reliable way to get this information.

I don't know if this has been fixed in later versions of react-native. However my rather simple workaround is based on the fact that locationX seems to be accurate in the onResponderGrant event so in the onResponderGrant handler I just do:

this.locationPageOffset = evt.nativeEvent.pageX - evt.nativeEvent.locationX;

then in the onResponderMove handler I can derive locationX from pageX like this.

const locationX = evt.nativeEvent.pageX - this.locationPageOffset

The first time I went down the onLayout handling route but found this simpler and more independent.

Has there been any update on this?

My locationX and Y values were off when I had children in the <Animated.View />. I added <View style={StyleSheet.absoluteFill} /> as the last child to act as a touch mask and now the values are consistent.

I'm reporting a similar problem where I have a touchable Opacity as a child component of a parent component with a panResponder (that cares about locationX/Y) and when clicking on an area outside the opacity the X/Y are correctly reported but when clicking an area within the touchable opacity the X/Y are wrong
(This is on pan responder grant and pan responder move)

I used the workaround described in the above comment by @selsamman.

I made it work with a transparent overlay. I believe the initial bug report demo is just not implemented correctly because of attaching the handlers to the box and not to a surface that should give coordinates where to move the box. The coordinates given in the demo are from its own surface.

Here is an example: https://snack.expo.io/Skb9J9MgB

import React, { PureComponent, Component } from 'react';
import { StyleSheet, Text, View, PanResponder, Dimensions } from 'react-native';

const { height, width } = Dimensions.get('window');

export default class TouchStartAndRelease extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      state: 'none',
      backgroundColor: 'red',
      marginTop: 100,
      marginLeft: 100,
    };
  }

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this._highlight();
      },
      onPanResponderMove: (evt, gestureState) => {
        this.setState({
          state: 'move',
          marginLeft: evt.nativeEvent.locationX,
          marginTop: evt.nativeEvent.locationY,
        });
      },
      onPanResponderRelease: (evt, gestureState) => {
        this._unhighlight();
      },
      onPanResponderTerminate: (evt, gestureState) => {
        this.setState({ state: 'none' });
      },
    });
  }

  _unhighlight() {
    this.setState({
      backgroundColor: 'red',
    });
  }

  _highlight() {
    this.setState({
      backgroundColor: 'blue',
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <View
          style={[
            styles.redView,
            {
              backgroundColor: this.state.backgroundColor,
              marginTop: this.state.marginTop,
              marginLeft: this.state.marginLeft,
            },
          ]} />
        <View style={styles.touchOverlay} {...this._panResponder.panHandlers} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    width,
    height,
  },
  touchOverlay: {
    position: 'absolute',
    width,
    height,
    // backgroundColor: 'black',
    zIndex: 999999,
  },
  redView: {
    width: 100,
    height: 100,
  },
});

But there is an issue :) I'm not sure what the expected behaviour is, but the current solution fails when your touch moves beyond the pan responder surface. Even stars to give the same problem as the initial bug report demo.

Example: https://snack.expo.io/rJOLrcfgr

import React, { PureComponent, Component } from 'react';
import { StyleSheet, Text, View, PanResponder, Dimensions } from 'react-native';

const { height, width } = Dimensions.get('window');

export default class TouchStartAndRelease extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      state: 'none',
      backgroundColor: 'red',
      marginTop: 100,
      marginLeft: 100,
    };
  }

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this._highlight();
      },
      onPanResponderMove: (evt, gestureState) => {
        this.setState({
          state: 'move',
          marginLeft: evt.nativeEvent.locationX,
          marginTop: evt.nativeEvent.locationY,
        });
      },
      onPanResponderRelease: (evt, gestureState) => {
        this._unhighlight();
      },
      onPanResponderTerminate: (evt, gestureState) => {
        this.setState({ state: 'none' })
      },
    });
  }

  _unhighlight() {
    this.setState({
      backgroundColor: 'red',
    });
  }

  _highlight() {
    this.setState({
      backgroundColor: 'blue',
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <View
          style={[
            styles.redView,
            {
              backgroundColor: this.state.backgroundColor,
              marginTop: this.state.marginTop,
              marginLeft: this.state.marginLeft,
            },
          ]} />
        <View style={styles.touchOverlay} {...this._panResponder.panHandlers} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    width,
    height,
  },
  touchOverlay: {
    position: 'absolute',
    top: 75,
    left: 75,
    width: width - 150,
    height: height - 150,
    borderWidth: 1,
    borderColor: 'black',
    zIndex: 999999,
  },
  redView: {
    width: 100,
    height: 100,
  },
});

I think that the layout coordinates should continue to be valid outside the pan responder surface, and the coords should be outside of the bounds of the surface responder if the touch is outside. The user should check if they are in the bounds if they need that.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.

Was this page helpful?
0 / 5 - 0 ratings