This content originally appeared on DEV Community and was authored by Victor Olufade
If you have worked with React-Native in building a ride-hailing/Carpooling app or just any app that would require you to animate a marker(your custom image) on the map by coordinates, then you might have encountered the problems I did while building a carpooling app recently.
My initial set-up required me to use the onUserLocationChange prop on the MapView from react-native-maps to animate the custom marker as illustrated here:
const { operations, models } = useTripsMapScreen({navigation, rider_id });
//In the useTripMapsScreen hook
const animateMarker = (newCoordinate: LatLng) => {
if (Platform.OS == 'android') {
if (animatedMarkerRef.current) {
animatedMarkerRef.current.animateMarkerToCoordinate(newCoordinate, 500);
}
} else {
animatedMarkerCoord
.timing({
duration: 500,
useNativeDriver: true,
latitude: newCoordinate.latitude,
longitude: newCoordinate.longitude,
})
.start();
}
};
const handleUserLocationChange = ({
nativeEvent: {coordinate},
}: UserLocationChangeEvent) => {
const newUserLocation = {
coords: {
latitude: coordinate?.latitude as number,
longitude: coordinate?.longitude as number,
heading: coordinate?.heading ?? 0,
},
};
setUserLocation(newUserLocation);
animateMarker({
latitude: coordinate?.latitude,
longitude: coordinate?.longitude,
});
};
// end of useTripsMapScreen
<StyledMapView
ref={models.mapRef}
showsCompass={false}
showsUserLocation={true}
onUserLocationChange={operations.handleUserLocationChange}
showsMyLocationButton={false}
provider={PROVIDER_GOOGLE}
customMapStyle={mapStyle}>
{renderMapMarkers()}
<Marker.Animated
ref={models.animatedMarkerRef}
coordinate={models.animatedMarkerCoord}>
<Image
source={carmaps}
style={{
width: 40,
height: 40,
transform: [{rotate: `${models.heading}deg`}],
}}
resizeMode="contain"
/>
</Marker.Animated>
<MapViewDirections
origin={models.mapMarkers[0]}
destination={models.mapMarkers[1]}
apikey={GOOGLE_MAPS_API_KEY}
strokeColor={theme?.colors?.screens?.mapScreen?.directionsStroke}
strokeWidth={scale(5)}
onReady={operations.handleMapDirectionsReady}
/>
</StyledMapView>;
This set-up worked perfectly on Android devices, but on iOS devices it behaved very strangely irrespective of whether I used the native driver or not. The marker, in my case a car image in png, would vibrate vigorously as it moved. I just could not get it to move smoothly.
After so much frustration, I had to look for a solution with react-native-reanimated. Under the hood react-native-maps uses the native Animated library, but what I did was to make react-native-maps work with react-native-reanimated.
Firstly, with some help from other devs, I had to create a useAnimatedRegion hook that did the actual animation using Reanimated’s withTiming:
import React, {useCallback} from 'react';
import {MapMarker, MapMarkerProps} from 'react-native-maps';
import Animated, {
Easing,
EasingFunction,
EasingFunctionFactory,
SharedValue,
useAnimatedProps,
useAnimatedReaction,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
interface LatLng {
latitude: number;
longitude: number;
longitudeDelta?: number;
latitudeDelta?: number;
}
interface AnimateOptions extends LatLng {
duration?: number;
easing?: EasingFunction | EasingFunctionFactory;
rotation?: number;
callback?: () => void;
}
type MarkerProps = Omit<MapMarkerProps, 'coordinate'> & {
coordinate?: MapMarkerProps['coordinate'];
};
export const AnimatedMarker = Animated.createAnimatedComponent(
MapMarker as React.ComponentClass<MarkerProps>,
);
export const useAnimatedRegion = (
location: Partial<LatLng> = {},
animatedPosition?: SharedValue,
) => {
const latitude = useSharedValue(location.latitude);
const longitude = useSharedValue(location.longitude);
const latitudeDelta = useSharedValue(location.latitudeDelta);
const longitudeDelta = useSharedValue(location.longitudeDelta);
const rotation = useSharedValue(undefined);
const animatedProps = useAnimatedProps(() => ({
coordinate: {
latitude: latitude.value ?? 0,
longitude: longitude.value ?? 0,
latitudeDelta: latitudeDelta.value ?? 0,
longitudeDelta: longitudeDelta.value ?? 0,
rotation: undefined,
},
}));
useAnimatedReaction(
() => {
return {
latitude: latitude.value ?? 0,
longitude: longitude.value ?? 0,
};
},
(result, previous) => {
if (animatedPosition) {
animatedPosition.value = result;
}
},
[],
);
const animate = useCallback(
(options: AnimateOptions) => {
const {duration = 500, easing = Easing.linear} = options;
const animateValue = (
value: SharedValue<number | undefined>,
toValue?: number,
callback?: () => void,
) => {
if (!toValue) {
return;
}
value.value = withTiming(
toValue,
{
duration,
easing,
},
callback,
);
};
animateValue(latitude, options.latitude);
animateValue(longitude, options.longitude, options.callback);
animateValue(latitudeDelta, options?.latitudeDelta);
animateValue(longitudeDelta, options?.longitudeDelta);
//@ts-ignore
animateValue(rotation, options?.rotation);
},
[latitude, longitude, latitudeDelta, longitudeDelta, rotation],
);
return {
props: animatedProps,
animate,
};
};
Next I had to create the actual marker component that took a ref and used the useImperativeHandle hook from react to call the animate function on my png image.
import React, { forwardRef, useImperativeHandle, useRef } from "react";
import { SharedValue } from "react-native-reanimated";
import { AnimatedMarker, useAnimatedRegion } from "./useAnimatedMarker";
import { LatLng } from "react-native-maps";
import { LATITUDE_DELTA, LONGITUDE_DELTA } from "@env";
import { Image } from "react-native";
const carmaps = require("../../../assets/images/carmaps.png");
export interface MovingCarMarkerProps {
defaultLocation: {
latitude: number;
longitude: number;
heading: number;
};
heading?: number;
animatedPosition?: SharedValue;
}
export interface MovingCarMarker {
animateCarToPosition: (
newCoords: LatLng,
speed?: number,
callback?: () => void
) => void;
}
export const MovingCarMarker = forwardRef(
(props: MovingCarMarkerProps, ref) => {
const defaultCarRef = useRef({
latitude: props?.defaultLocation?.latitude,
longitude: props?.defaultLocation?.longitude,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
});
const animatedRegion = useAnimatedRegion(
{
latitude: parseFloat(
defaultCarRef?.current?.latitude
? defaultCarRef?.current?.latitude?.toString()
: "0"
),
longitude: parseFloat(
defaultCarRef?.current?.longitude
? defaultCarRef?.current?.longitude?.toString()
: "0"
),
},
props?.animatedPosition
);
useImperativeHandle(ref, () => ({
animateCarToPosition: (
newCoords: LatLng,
speed?: number,
callback?: () => void
) => {
animatedRegion.animate({
latitude: newCoords?.latitude,
longitude: newCoords?.longitude,
duration: speed || 500,
callback,
});
},
}));
return (
<AnimatedMarker
zIndex={20}
anchor={{ x: 0.5, y: 0.5 }}
rotation={props?.defaultLocation?.heading}
animatedProps={animatedRegion.props}
>
<Image
source={carmaps}
style={{
width: 40,
height: 40
}}
resizeMode="contain"
/>
</AnimatedMarker>
);
}
);
Once I had these two set up, all I had to do was to use my MovingCarMarker component within the styled MapView from react-native-maps:
// in my custom hook
const carMarkerRef = useRef<MovingCarMarker | null>(null);
const handleUserLocationChange = ({
nativeEvent: {coordinate},
}: UserLocationChangeEvent) => {
const newUserLocation = {
coords: {
latitude: coordinate?.latitude as number,
longitude: coordinate?.longitude as number,
heading: coordinate?.heading ?? 0,
},
};
setUserLocation(newUserLocation);
carMarkerRef?.current?.animateCarToPosition({
latitude: newUserLocation?.coords?.latitude,
longitude: newUserLocation?.coords?.longitude,
});
};
// end of custom hook
<StyledMapView
ref={models.mapRef}
showsCompass={false}
showsUserLocation={true}
onUserLocationChange={operations.handleUserLocationChange}
showsMyLocationButton={false}
provider={PROVIDER_GOOGLE}
customMapStyle={mapStyle}
onRegionChangeComplete={operations.onregionChangeComplete}>
{renderMapMarkers()}
<MovingCarMarker
ref={models?.carMarkerRef}
defaultLocation={models?.defaultLocation}
/>
{operations?.returnPolyLine()}
</StyledMapView>;
Note that in the handleUserLocationChange function, I not only use the ref to animate to a new position, I also update the models?.defaultLocation state been passed to the MovingCarMarker as defaultLocation with the new coordinates.
With this set-up your custom marker would animate very smoothly between coordinates in your react-native app. Please let me know if this helps you in any way. Questions and contributions are also very welcome.
Even with this solution, there’s still a lot of room for improvement, so feel free to customize the implementation as you deem fit.
Cheers!
This content originally appeared on DEV Community and was authored by Victor Olufade