-
-
Notifications
You must be signed in to change notification settings - Fork 928
Description
Mapbox Implementation
Mapbox
Mapbox Version
11.16.2
React Native Version
0.81.5
React Native Architecture
New Architecture (Fabric/TurboModules)
Platform
Android
@rnmapbox/maps version
10.2.10
Standalone component to reproduce
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { View, StyleSheet, Animated, Text, SafeAreaView, TextInput, TouchableOpacity, Keyboard } from 'react-native';
import Mapbox from '@rnmapbox/maps';
Mapbox.setAccessToken('pk.ey...');
const LONDON_COORDINATES = [-0.1278, 51.5074];
const POOL_SIZE = 2000;
const generateMarkers = (count: number) => {
const markers = [];
for (let i = 0; i < count; i++) {
markers.push({
id: `marker-${i}`,
coordinate: [
LONDON_COORDINATES[0] + (Math.random() - 0.5) * 0.2, // Increased spread slightly
LONDON_COORDINATES[1] + (Math.random() - 0.5) * 0.2,
],
});
}
return markers;
};
// Generate a pool of markers
const MARKER_POOL = generateMarkers(POOL_SIZE);
const PulsingMarker = ({ isAnimating }: { isAnimating: boolean }) => {
const scale = useRef(new Animated.Value(1)).current;
const opacity = useRef(new Animated.Value(1)).current;
useEffect(() => {
let pulse: Animated.CompositeAnimation;
if (isAnimating) {
pulse = Animated.loop(
Animated.parallel([
Animated.sequence([
Animated.timing(scale, {
toValue: 2,
duration: 1500,
useNativeDriver: false, // MarkerView children on Android might need this off or handled carefully
}),
Animated.timing(scale, {
toValue: 1,
duration: 0,
useNativeDriver: false,
}),
]),
Animated.sequence([
Animated.timing(opacity, {
toValue: 0,
duration: 1500,
useNativeDriver: false,
}),
Animated.timing(opacity, {
toValue: 1,
duration: 0,
useNativeDriver: false,
}),
]),
])
);
pulse.start();
} else {
scale.setValue(1);
opacity.setValue(1);
}
return () => {
if (pulse) {
pulse.stop();
}
};
}, [scale, opacity, isAnimating]);
return (
<View style={styles.markerContainer}>
<Animated.View
style={[
styles.ring,
{
transform: [{ scale }],
opacity,
},
]}
/>
<View style={styles.marker} />
</View>
);
};
export default function App() {
const [markerCount, setMarkerCount] = useState(30);
const [inputText, setInputText] = useState('30');
const [isAnimating, setIsAnimating] = useState(true);
const visibleMarkers = useMemo(() => {
// Clamp to pool size
const count = Math.min(markerCount, POOL_SIZE);
return MARKER_POOL.slice(0, count);
}, [markerCount]);
const handleApply = () => {
const count = parseInt(inputText, 10);
if (!isNaN(count) && count >= 0) {
setMarkerCount(count);
Keyboard.dismiss();
}
};
return (
<View style={styles.container}>
<Mapbox.MapView style={styles.map}>
<Mapbox.Camera
zoomLevel={11}
centerCoordinate={LONDON_COORDINATES}
/>
{visibleMarkers.map((marker) => (
<Mapbox.MarkerView
key={marker.id}
id={marker.id}
coordinate={marker.coordinate}
>
<PulsingMarker isAnimating={isAnimating} />
</Mapbox.MarkerView>
))}
</Mapbox.MapView>
<SafeAreaView style={styles.controlsContainer}>
<Text style={styles.label}>Marker Count: {markerCount}</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
keyboardType="number-pad"
value={inputText}
onChangeText={setInputText}
placeholder="Enter count"
placeholderTextColor="#ccc"
/>
<TouchableOpacity style={styles.button} onPress={handleApply}>
<Text style={styles.buttonText}>Apply</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.button, styles.toggleButton]}
onPress={() => setIsAnimating(!isAnimating)}
>
<Text style={styles.buttonText}>
{isAnimating ? 'Stop Animation' : 'Start Animation'}
</Text>
</TouchableOpacity>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
flex: 1,
},
controlsContainer: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 16,
padding: 16,
gap: 12,
},
label: {
color: 'white',
fontSize: 16,
fontWeight: '600',
textAlign: 'center',
},
inputContainer: {
flexDirection: 'row',
gap: 12,
},
input: {
flex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 8,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
paddingHorizontal: 16,
paddingVertical: 12,
color: 'white',
fontSize: 16,
},
button: {
backgroundColor: '#007AFF',
borderRadius: 8,
paddingHorizontal: 24,
justifyContent: 'center',
alignItems: 'center',
},
toggleButton: {
backgroundColor: '#34C759',
paddingVertical: 12,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
markerContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
},
ring: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: 'rgba(255, 0, 0, 0.5)',
},
marker: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: 'red',
borderWidth: 2,
borderColor: 'white',
},
});
Observed behavior and steps to reproduce
I'm experiencing a significant synchronization issue with MarkerView on Android using @rnmapbox/maps. When panning or zooming the map, the markers lag behind the map movement, appearing to "float" or drift away from their coordinate before snapping back into place once the map movement stops. This does not happen on iOS, where the markers track perfectly.
I initially tried using PointAnnotation, but I needed to implement a continuous pulsing animation. Since PointAnnotation renders its children to a static bitmap on Android (preventing continuous animation), I switched to MarkerView to support the Animated views. However, the performance/synchronization on Android is creating a poor user experience.
What I've observed:
PointAnnotation: Renders correctly and sticks to map, but animations (Animated.View) are frozen/static on Android because of the bitmap snapshotting.
MarkerView: Animations play perfectly, but the view position lags significantly behind the map camera updates during gestures.
Has anyone found a workaround to improve the synchronization of MarkerView on Android, or a way to get performant continuous animations working with PointAnnotation (or another method) without the drift?
Thanks!
Expected behavior
No response
Notes / preliminary analysis
No response
Additional links and references
No response