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

"Follow Me Button": Map stutters slowly when using onPanDrag + scrollEnabled={false} on iOS #1038

Closed
yaronlevi opened this issue Feb 12, 2017 · 21 comments
Labels

Comments

@yaronlevi
Copy link

I am trying to implement a simple "follow me" switch (like Google Maps).
When the switch is ON, the map region is automatically updated every couple of seconds and follows the user.
But when the user manually dose any drag gesture on the map, the switch will go to OFF mode and the map will not follow the user.

I used onPanDrag to know when the user manually moves the map. On android it's working fine:

<MapView 
            region={this.state.region}
            onPanDrag={(e) => { this.setState({isFollowingUser:false}) }}
            onRegionChange={(region) => { this.setState({region}) }}>

The code above works great: When the user drags the map, I set isFollowingUser to FALSE. Later in the geoLocationWatchPosition callback I see that isFollowingUser equals FALSE, and do not update the region to the new location.

But on iOS , I must set scrollEnabled:false in order make onPanDrag work. But now, because scrollEnabled is FALSE, the map won't move when the user interacts with it. So I tried to do this inside the onPanDrag:

onPanDrag(e) {
    const coord = e.nativeEvent.coordinate;
    const newRegion = {
      latitude: coord.latitude,
      longitude: coord.longitude,
      latitudeDelta: LATITUDE_DELTA,
      longitudeDelta: LONGITUDE_DELTA
    };
      this.setState({
        region: newRegion
      });
  }

The map moves, but it is barley usable. It's very slow and stutters. I think it is related to the fact that setState is called many times a second, but how can I do it otherwise?

@yaronlevi yaronlevi changed the title Map stutters slowly when using onPanDrag + scrollEnabled={false} on iOS "Follow Me Button": Map stutters slowly when using onPanDrag + scrollEnabled={false} on iOS Feb 12, 2017
@tianhaoz95
Copy link

I ran into the same problem. I guess the problem is that the coordinates are not that accurate and re-render too frequently will make it unusable. So I tried something similar to a PID controller. When I get a new coordinate, I compare with the last one and only set a small change. It worked better than before, but still kind of shitty.

var new_latitude = this.state.show_latitude + (e.nativeEvent.coordinate.latitude - this.state.show_latitude) * 0.2;

var new_longitude = this.state.show_longitude + (e.nativeEvent.coordinate.longitude - this.state.show_longitude) * 0.2;

this.setState({ show_latitude: new_latitude, show_longitude: new_longitude, });

@svenadlung
Copy link

Having the same problem. Any solutions on this?

@Rafkraft
Copy link

+1

1 similar comment
@rexlow
Copy link

rexlow commented Jun 25, 2017

+1

@chriscohoat
Copy link

I haven't tested it @tianhaoz95, but you could use lodash.debounce (with leading and trailing enabled and a wait of a few milliseconds) in order to only receive a few of the many updates received. Docs are here: https://lodash.com/docs/4.17.4#debounce

It's less than ideal, but I think I'm going to keep track of followsUserLocation in state:

state = {
    followsUserLocation: true,
};

And setup my Mapview such that scrollEnabled is always enabled for Android and for iOS right now scrollEnabled is the opposite of followsUserLocation

<MapView
    ref={map => {
        this._map = map;
    }}
    style={styles.map}
    showsUserLocation={true}
    followsUserLocation={this.state.followsUserLocation}
    onPanDrag={this._onPanDrag}
    scrollEnabled={require('react-native').Platform.OS === 'android' ? true : !this.state.followsUserLocation}
/>

Where _onPanDrag uses a debounced approach to updating the map's region, and inside _onPanDrag I disable the follow flag with this.setState({followsUserLocation: false});

@rexlow
Copy link

rexlow commented Jun 28, 2017

@tianhaoz95 I've tried your solution but PID still has some margin of errors, which is not ideal until we have figured out the precise formula. I have then implemented my own solution. It works exactly as I want.

Let the component know users are interacting with the map, inject this prop into the Map component.
onMoveShouldSetResponder={this.onMapDrag}

where this.onMapDrag dispatch an action to update the store (watching: false)

Instead of using onPanDrag that updates hundreds of times (it's worse if you do this to state), we can simply get the region from onRegionChange prop, and pass it to a function.

onRegionChange = (region) => {
    let storeLatDelta = this.props.api.latestRegion.latitudeDelta
    let currentDelta = region.latitudeDelta
    let diff = Math.abs(currentDelta - storeLatDelta)
  
    // detect if user zoom or pinch the map
    if (diff > 0.0002 && diff < 0.04) {  // edit the range to your liking
      this.props.userChangeRegion()
    }
  }

Then have your componentWillReceiveProps do this

componentWillReceiveProps(nextProps) {
    // avoid moving to center during first time, coz its going back to center
    if (!(nextProps.api.watching === true && this.props.api.watching === false)) {
      if (!this.props.api.watching) {
        this.moveUserCenter()
      } 
    }
  }

Programmatically update the map,
moveUserCenter = () => this.map.animateToRegion(this.props.api.latestRegion, 200);

And yeah, since we are programmatically updating the map, followUserLocation can safely set to false, and scrollEnabled to true.

@yaronlevi To implement the follow user method, just have your switch button subscribe to this, hope it helps.

centerToUser = () => {
    if (this.props.api.watching) {
      this.props.centerToUser()
      this.map.animateToRegion(this.props.api.latestRegion, 500)
    }
  }

@martinezguillaume
Copy link

But why onPanDrag works with scrollEnabled at false while android works with scrollEnabled at true ? Is it not better to make it works with scrollEnabled at true ?

@yentheo
Copy link

yentheo commented May 25, 2018

How come this is still not addressed, I still have this problem. It's almost been a year.

@otaviogaiao
Copy link

I'm having this issue now. Any workaround that doesn't involve redux?

@otaviogaiao
Copy link

for me, instead of using onPanDrag, I used onMoveShouldSetResponder.

<MapView
              provider={PROVIDER_GOOGLE}
              showsUserLocation
              style={styles.mapContainer}
              showsMyLocationButton={false}
              initialRegion={this.region}
              loadingEnabled
              onUserLocationChange={(event) => followUserLocation && this.userLocationChanged(event)}
              onRegionChange={this.regionChanged}
              followUserLocation={followUserLocation}
              ref={ref => this.map = ref}
              onMoveShouldSetResponder={this.onDrag}
              key="mapa"
            >

My methods:

  userLocationChanged(event) {
    const newRegion = event.nativeEvent.coordinate;

    this.region = {
      ...this.region,
      latitude: newRegion.latitude,
      longitude: newRegion.longitude
    };

    this.animateToRegion();
  }

  animateToRegion() {
    if(this.map) {
      this.map.animateToRegion({latitude: this.region.latitude, longitude: this.region.longitude,
        latitudeDelta: this.region.latitudeDelta, longitudeDelta: this.region.longitudeDelta}, 1000);
    }
  }

  regionChanged(event) {
    this.region = {
        longitudeDelta: event.longitudeDelta,
        latitudeDelta: event.latitudeDelta,
        latitude: event.latitude,
        longitude: event.longitude
      }
  }

  onDrag(event) {
    this.setState({
      followUserLocation: false
    });
  }

My state:

  this.state = {
        erro: false,
        hasPermission: true,
        loading: true,
        followUserLocation: true
    };

I found out that storing region on state doesnt work well.

I hope this helps someone

@bluenex
Copy link

bluenex commented Nov 5, 2018

@otaviogaiao where do you find that prop onMoveShouldSetResponder? It really works for me on both Android and IOS, however, I still don't know how it works. Is it the same as onMoveShouldSetPanResponder described here?

@outaTiME
Copy link

When using react-navigation v3 onMoveShouldSetResponder stop working, any advice with that? thanks !!!

@salah-ghanim
Copy link
Collaborator

@outaTiME you will need to disable gestures in React-Navigation

@salah-ghanim
Copy link
Collaborator

@alvelig @christopherdro it seems this is an application design issue rather than a bug in the library itself, using state to manage high frequency update for complex components such as mapview itself is expected to have bad performance.

I would suggest closing this issue

@outaTiME
Copy link

@outaTiME you will need to disable gestures in React-Navigation

@salah-ghanim i disable gestures and no luck probably something else =(

@salah-ghanim
Copy link
Collaborator

@outaTiME I would suggest you create a new issue then with a sample project, I'm more than happy to take a look then.

@jerryryle
Copy link

jerryryle commented Mar 21, 2019

@outaTiME I'm having a similar problem. If I use a drawer navigator or a stack navigator, which both respond to gestures, then my onMoveShouldSetResponder on the MapView is never invoked--even if I've disabled the navigator gestures. I think it might be down inside react-native-gesture-handler, which react-navigation now uses. There are a couple of open issues in react-native-gesture-handler that might be related, such as this one: 465 - Bug with react-native-maps. My workaround for now has been to use onStartShouldSetResponder instead; however, this is not ideal as it fires whenever the user touches the map--not just when they move it.

@grifotv
Copy link
Contributor

grifotv commented Apr 25, 2019

@jerryryle I'm having the exact same issue as you. Only difference is that I rely on both onMoveShouldSetResponder and onResponderRelease in a wrapper view around MapView.

Do you know what would the best way to detect touch start and touch end on MapView when displayed inside one of the GestureHandler 's from react-native-gesture-handler?

@IAmMarcellus
Copy link

IAmMarcellus commented May 19, 2019

@outaTiME @jerryryle @grifotv For me, onMoveShouldSetResponder works on Android, but not on iOS. I think this may be related to software-mansion/react-native-gesture-handler#255, where onMoveShouldSetResponder can't fire because the React-Native-Gesture-Handler panGestureHandler is still active. However, RNGH's panGestureHandler doesn't work with the map either as discussed in the issue @jerryryle referenced.

Anyone found an efficient workaround?

@sam17896
Copy link

onDrag

When the followUserLocation state turns true after the user finish dragging ?

@stale
Copy link

stale bot commented Oct 2, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Oct 2, 2020
@stale stale bot closed this as completed Oct 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests