onPanDrag doesn't fire while camera animating with animateCamera, cancel camera animation not implemented.
Summary
I want to cancel camera animation when user interacts with map (pan/zoom).
I'm attempting to implement a feature like iOS maps where camera follows user location until user interacts with map. Location is passed to my component that wraps map view at some throttle interval (eg. 5 seconds) and my animation time matches that timing to allow for smooth movement.
Out of the box followsUserLocation would be ideal, but it 1) Is not smooth between location updates, 2) docs say it doesn't work on Android. So I'm essentially recreating the functionality with moving marker and camera.
Reproducible sample code
import { useEffect, useState, useCallback ,useRef } from 'react';
import MapView from 'react-native-maps';
const location = {
latitude: 40.1000000,
longitude: -84.1000000,
};
const initialCamera = {
center: {
latitude: 39.1000000,
longitude: -86.1000000,
},
pitch: 0,
heading: 0,
altitude: 1_000_000,
}
export default function App() {
const mapRef = useRef(null)
const [userDidInteract, setUserDidInteract] = useState(false)
const animateCamera = useCallback(() => {
const c = {
center: {
latitude: location.latitude,
longitude: location.longitude,
},
pitch: 0,
heading: 0,
altitude: 100_000,
}
mapRef.current.animateCamera(c, { duration: 5000 });
}, []);
useEffect(() => {
setTimeout(() => {
animateCamera()
}, 1000)
}, [animateCamera]);
const onPanDrag = useCallback(async () => {
// Doesn't fire during animation
if (!userDidInteract) {
const currentCamera = await mapRef.current.getCamera()
// Cancel animation workaround
mapRef.current.animateCamera(currentCamera, { duration: 1 });
setUserDidInteract(true)
}
}, [userDidInteract])
return (
<MapView
style={{flex: 1}}
ref={mapRef}
initialCamera={initialCamera}
onPanDrag={onPanDrag}
/>
);
}
Steps to reproduce
Animate camera and do a user pan gesture during animation.
Expected result
Expect onPanDrag to fire. Then, if it did, in the onPanDrag handler I'd like to cancel camera animation.
Actual result
onPanDrag callback never fires while animating. Aside, I'm able to achieve cancel with a workaround mapRef.current.animateCamera(currentCamera, { duration: 1 }). Duration 0 doesn't work as expected as it still blocks gesture callbacks for some duration after stopping.
React Native Maps Version
1.8.0
What platforms are you seeing the problem on?
iOS (Apple Maps)
React Native Version
0.72.7
What version of Expo are you using?
SDK 48
Device(s)
iPhone 14 Pro
Additional information
EXPO 49
I've run across the same problem. I'm implementing a workaround that puts a transparent layer over the map and using onTouchStart I fire the function you pointed out to cancel the animation
mapRef.current.animateCamera(currentCamera, { duration: 1 });
I'm only rendering that hidden layer while camera animation occurs so you can pan and drag right after.
That was one of my early workarounds, but I found behavior unacceptable: user has to tap, release, THEN pan vs immediately initiate a pan/zoom. I ended up driving my camera animation off of the marker animation progress with setCamera instead of animateCamera. Then no transparent layer necessary. Curious if you find a better way though.
I agree that behavior is not user friendly. How are you calculating the camera center? Calculating the line between the old and new Coords and finding the position based on elapsed animation time?
I'm running an animation loop on cameraAnimationProgress (0 to 1), attaching a listener to get callbacks for every animation progress tick. Before running loop, I get current camera with await mapRef.current.getCamera(). In the callback, calculate all my next camera attributes based on current camera, coords from instantaneous marker position (markerCoord.__getValue()) and camera animation progress fed to callback. Then set without animation mapRef.current.setCamera(nextCamera).
All this keeps camera glued to the marker. Animating TO the marker can be done with the same code, but running an animation on the camera before the loop. All this frees up onTouchStart to fire naturally so i can toggle the 'following' state. When that state changes, I stop animation, remove listeners, etc.
It's pretty complicated tbh. I can go into more detail if you want.
Have you found this method that works smoothly for android? It's looking great on iOS. I am using the previous method I mentioned for Android still as Android seems to have a harder time keeping up with that listener and animating smoothly.
For anyone else who is reading this. You can hook into the AnimatedRegion class using
AnimatedRegion.addListener(callback: (region: Region) => void)
I implemented it like this for iOS:
const [coord, setCoord] = useState(new AnimatedRegion({
latitude,
longitude,
latitudeDelta,
longitudeDelta
}))
// ....Attach listener when you want to follow a specific marker
if(isTrackingMarker){
const newCoordinate = {
latitude,
longitude,
latitudeDelta,
longitudeDelta,
duration
}
//only add one listener at a time
if(listenerId !== null){
const id = coord.addListener(animateCamera)
setListenerId(v=>id)
}
coord.timing(newCoordinate).start()
}
//clearing listener on unmount
useEffect(() => {
return ()=>{
if(listenerId){
coord.removeListener(listenerId)
}
}
}, []);
const animateCamera = (r: Region) => {
//If the camera reaches the new coordinate, unmount it
if (r.latitude === coord.latitude && r.longitude === coord.longitude) {
//clear listeners
l.trace('clearing listeners for camera: ' + listenerIds.length);
coord.removeListener(listenerId);
setListenerId((v) => null);
}
const camera: Camera = {
center: {
latitude: r.latitude,
longitude: r.longitude
},
pitch: 0,
heading: 0,
altitude: 1000,
zoom: 17
};
mapRef.current.setCamera(camera, { duration: 1 });
};
I haven't attempted Android, but I may accept degraded performance/behavior given its a much smaller user base. Is it Android or Google maps on iOS that's not smooth?
Android specifically. The animation process is slightly different to animate markers for android. For now, our user base is less than 10% Android, so we are considering it an acceptable user experience hit for right now.
I will say the animation loop idea was fantastic! I didn't realize there was an addListener method right on the AnimatedRegion. And for the number of updates being performed, it really runs incredibly smooth.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If the issue remains relevant, simply comment Still relevant and the issue will remain open. Thank you for your contributions.
Has anyone come up with a good solution for this, we are dealing with the exact same issue as @sspread