I’m using react-native-maps with clustering on the map. When zooming in, markers are displayed with images based on genres. The issue is that the markers are not rendering on Android, although they work as expected on iOS. I’ve tried downgrading to version 1.20.1 and upgrading to the latest version, but the issue persists. I also tried using SVG, PNG, and JPEG formats, but the markers still don’t render. Can someone help me resolve this issue?
`import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react';
import {
View,
Text,
StyleSheet,
Dimensions,
Animated,
PanResponder,
LayoutChangeEvent,
ActivityIndicator,
Platform,
Image,
TouchableOpacity,
ScrollView,
ViewStyle,
TextStyleIOS,
StatusBar,
ImageBackground,
} from 'react-native';
import {offsetOverlappingCoordinates} from '../utils/offSetSameCoordinate';
import NetInfo from '@react-native-community/netinfo';
import MapView, {Marker, Region} from 'react-native-maps';
import HeaderUser from '../components/HeaderUser';
import EventCard from '../components/EventCard';
import {config} from '../constants/config';
import SearchBottomSheet, {
SearchBottomSheetRef,
} from '../components/SearchBottomSheet';
import {
useGetUserNearbyEventsQuery,
useGetEventByIdQuery,
} from '../services/api/eventApi';
import {useRoute, RouteProp, useNavigation} from '@react-navigation/native';
import {EnumDuration, IEvent, IEventArtist, IGetNearbyEventParams} from '../types.d';
import {getGenreImage, getGenreBackgroundImage} from '../utils/getGenresImage';
import {createClusters} from '../utils/mapScreen';
import {requestUserLocation} from '../utils/locationHelper';
import AppImage from '../components/BaseImage';
const {height, width} = Dimensions.get('window');
const STATUS_BAR_HEIGHT =
StatusBar.currentHeight || (Platform.OS === 'ios' ? 44 : 0);
const HEADER_HEIGHT = 80;
const SAFE_TOP_MARGIN = STATUS_BAR_HEIGHT + HEADER_HEIGHT - 70;
const SNAP_BOTTOM = height - 170;
const MAX_SHEET_HEIGHT = height <= 700 ? height * 0.55 : height * 0.65;
const TOP_CONSTRAINT = SAFE_TOP_MARGIN;
interface EventCoordinate {
latitude: number;
longitude: number;
}
interface Event {
id: string;
name: string;
image: string;
end_time?: string;
date?: string;
dateRange: string;
time: string;
location: string;
priceRange: string;
coordinate: EventCoordinate;
isShare: boolean;
canLiked: boolean;
genre: string;
artist?: IEventArtist[];
timeFilter: string;
venue?: IEventArtist[];
}
type RootStackParamList = {
MapUser: {
latitude?: number;
longitude?: number;
skipApiCall?: boolean;
eventId?: string;
showSpecificEvent?: boolean;
};
};
type MapUserRouteProp = RouteProp;
const MapScreenUser = () => {
const route = useRoute();
const {
eventId,
showSpecificEvent,
latitude: paramLatitude,
longitude: paramLongitude,
} = route.params || {};
const mapRef = useRef(null);
const scrollViewRef = useRef(null);
const scrollY = useRef(0);
const [locationFetched, setLocationFetched] = useState(false);
const bottomSheetHeight = useRef(0);
const isDraggingSheet = useRef(false);
const isScrolling = useRef(false);
const [deviceSize, setDeviceSize] = useState({width, height});
const [singleEventCardHeight, setSingleEventCardHeight] = useState(
Math.min(250, height * (height <= 700 ? 0.25 : 0.3)),
);
const [doubleEventCardHeight, setDoubleEventCardHeight] = useState(
Math.min(520, height * (height <= 700 ? 0.45 : 0.6)),
);
// Update initial region state
const [region, setRegion] = useState({
latitude: 51.5074,
longitude: -0.1278,
latitudeDelta: showSpecificEvent ? 0.001 : 0.009,
longitudeDelta: showSpecificEvent ? 0.001 : 0.009,
});
const [_userLocation, setUserLocation] = useState(null);
const [selectedEventIndex, setSelectedEventIndex] = useState(
null,
);
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const searchBottomSheetRef = useRef(null);
const [isConnected, setIsConnected] = useState(true);
const [showNoResultsMessage, setShowNoResultsMessage] = useState(false);
const [viewMode, setViewMode] = useState<'clusters' | 'events'>('clusters');
const [activeClusterIndex, setActiveClusterIndex] = useState(
null,
);
const [visibleEvents, setVisibleEvents] = useState([]);
const [mapReady, setMapReady] = useState(false);
const [isSearchActive, setIsSearchActive] = useState(false);
const [searchResultsCount, setSearchResultsCount] = useState(0);
const [timeFilters, setTimeFilters] = useState('');
const [currentZoomLevel, setCurrentZoomLevel] = useState(0.1);
const navigation = useNavigation();
const [genreFilters, setGenresFilters] = useState<
{id: string; name: string}[]
([]);
const [activeFilters] = useState<{
timeFilters: string[];
genres: string[];
}>({
timeFilters: [],
genres: [],
});
const [filtersApplied] = useState(false);
const [isInitialMarkerLoad, setIsInitialMarkerLoad] = useState(true);
const [imageLoadedStates, setImageLoadedStates] = useState<{[key: string]: boolean}>({}); // Track image loads per marker
const shouldSkipNearbyEvents = showSpecificEvent && eventId;
setIsInitialMarkerLoad(true); const timer = setTimeout(() => { setIsInitialMarkerLoad(false); }, 1000); return () => clearTimeout(timer);
}, [apiEvents, specificEventData, mapIEventToEvent, showSpecificEvent]);
useEffect(() => {
if (error || specificEventError) {
console.error('Error fetching events:', error || specificEventError);
setShowNoResultsMessage(true);
}
}, [error, specificEventError]);
const panY = useRef(new Animated.Value(SNAP_BOTTOM)).current;
const [isSheetOpen, setIsSheetOpen] = useState(false);
const clusterArray = useMemo(() => {
if (showSpecificEvent) {
return [];
}
return createClusters(filteredEvents, currentZoomLevel);
}, [filteredEvents, currentZoomLevel, showSpecificEvent]);
const filteredClusters = useMemo(() => {
if (showSpecificEvent || !filtersApplied) {
return createClusters(events, currentZoomLevel);
}
return clusterArray;
}, [
events,
clusterArray,
filtersApplied,
currentZoomLevel,
showSpecificEvent,
]);
useEffect(() => {
if (showSpecificEvent) {
return;
}
if (viewMode === 'clusters') { setVisibleEvents(filteredEvents); } else if (activeClusterIndex !== null) { const currentCluster = filteredClusters[activeClusterIndex]; if (currentCluster) { setVisibleEvents(currentCluster.events); } }
}, [
filtersApplied,
filteredClusters,
filteredEvents,
viewMode,
activeClusterIndex,
showSpecificEvent,
]);
useEffect(() => {
if (filtersApplied && filteredEvents.length === 0 && !showSpecificEvent) {
setShowNoResultsMessage(true);
} else {
setShowNoResultsMessage(false);
}
}, [filtersApplied, filteredEvents, showSpecificEvent]);
const expandToFullScreen = () => {
Animated.spring(panY, {
toValue: TOP_CONSTRAINT,
useNativeDriver: false,
tension: 70,
friction: 12,
}).start(() => setIsSheetOpen(true));
};
const handleClusterPress = (clusterIndex: number) => {
if (showSpecificEvent) {
return;
}
setActiveClusterIndex(clusterIndex); setViewMode('events'); if (isSheetOpen) { closeBottomSheet(); setSelectedEventIndex(null); } const cluster = filteredClusters[clusterIndex]; if (mapRef.current) { mapRef.current.animateToRegion( { latitude: cluster.coordinate.latitude, longitude: cluster.coordinate.longitude, latitudeDelta: 0.03, longitudeDelta: 0.03, }, 1000, ); } setVisibleEvents(cluster.events); if (cluster.events.length > 3) { expandToFullScreen(); } else { openBottomSheet(); }
};
const handleEventMarkerPress = (eventIndex: number) => {
setIsLoading(true);
setSelectedEventIndex(eventIndex);
const currentEvent = visibleEvents[eventIndex];
if (mapRef.current) {
mapRef.current.animateToRegion(
{
latitude: currentEvent.coordinate.latitude,
longitude: currentEvent.coordinate.longitude,
latitudeDelta: 0.001,
longitudeDelta: 0.001,
},
500,
);
}
const targetHeight = determineBottomSheetHeight();
Animated.spring(panY, { toValue: targetHeight, useNativeDriver: false, tension: 70, friction: 12, }).start(() => setIsSheetOpen(true)); setTimeout(() => { setIsLoading(false); }, 250);
};
const handleCenterToUserLocation = async () => {
try {
setIsLoading(true);
const userRegion = await requestUserLocation();
setUserLocation(userRegion);
if (mapRef.current) {
mapRef.current.animateToRegion(
{
latitude: userRegion.latitude,
longitude: userRegion.longitude,
latitudeDelta: 0.0009,
longitudeDelta: 0.0009,
},
1000,
);
}
} catch (locationError) {
console.error('Failed to fetch user location:', locationError);
} finally {
setIsLoading(false);
}
};
const handleRegionChange = (region: Region) => {
if (showSpecificEvent) {
return;
}
setCurrentZoomLevel(region.latitudeDelta); if (region.latitudeDelta > 0.05 && viewMode === 'events') { setViewMode('clusters'); setVisibleEvents(filteredEvents); setActiveClusterIndex(null); if (isSheetOpen) { closeBottomSheet(); setSelectedEventIndex(null); } setSelectedEventIndex(null); } else if (region.latitudeDelta <= 0.04 && viewMode === 'clusters') { let closestClusterIndex = -1; let minDistance = Infinity; filteredClusters.forEach((cluster, index) => { const distance = Math.sqrt( Math.pow(region.latitude - cluster.coordinate.latitude, 2) + Math.pow(region.longitude - cluster.coordinate.longitude, 2), ); if (distance < minDistance) { minDistance = distance; closestClusterIndex = index; } }); if (closestClusterIndex !== -1 && minDistance < 0.02) { setActiveClusterIndex(closestClusterIndex); setViewMode('events'); if (isSheetOpen) { closeBottomSheet(); setSelectedEventIndex(null); } const cluster = filteredClusters[closestClusterIndex]; setVisibleEvents(cluster.events); // Don't automatically open or expand the bottom sheet on zoom // This prevents the sheet from opening when zooming in } }
};
const handleScrollBegin = () => {
isScrolling.current = true;
};
const handleScrollEnd = () => {
isScrolling.current = false;
};
const handleScroll = (event: any) => {
scrollY.current = event.nativeEvent.contentOffset.y;
const eventsToShow = isSearchActive
? events.filter(event => {
const lowercaseQuery = searchQuery.toLowerCase();
return (
event.location.toLowerCase().includes(lowercaseQuery) ||
event.name.toLowerCase().includes(lowercaseQuery)
);
})
: filtersApplied
? filteredEvents
: viewMode === 'events'
? visibleEvents
: events;
if (
eventsToShow.length > 3 &&
isSheetOpen &&
selectedEventIndex === null &&
!showSpecificEvent
) {
expandToFullScreen();
}
};
const handleBottomSheetLayout = (event: LayoutChangeEvent) => {
bottomSheetHeight.current = event.nativeEvent.layout.height;
};
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isSignificantVerticalGesture =
Math.abs(gestureState.dy) > 10 &&
Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
if (!isSignificantVerticalGesture) { return false; } const isDraggingHandle = evt.nativeEvent.locationY < 60; const isAtTopOfScrollView = scrollY.current <= 0; const isDraggingDown = gestureState.dy > 0; return isDraggingHandle || (isAtTopOfScrollView && isDraggingDown); }, onPanResponderGrant: () => { panY.setOffset(panY._value); panY.setValue(0); isDraggingSheet.current = true; }, onPanResponderMove: (_, gestureState) => { let newY = gestureState.dy; const minHeight = determineBottomSheetHeight(); const maxHeight = SNAP_BOTTOM; const currentPosition = panY._offset + newY; if (currentPosition < minHeight) { newY = minHeight - panY._offset; } else if (currentPosition > maxHeight) { newY = maxHeight - panY._offset; } panY.setValue(newY); }, onPanResponderRelease: (_, gestureState) => { panY.flattenOffset(); isDraggingSheet.current = false; const currentPosition = panY._value; const velocity = gestureState.vy; const shouldOpen = velocity < -0.3 || (velocity > -0.3 && velocity < 0.3 && currentPosition < SNAP_BOTTOM / 2); const targetValue = shouldOpen ? determineBottomSheetHeight() : SNAP_BOTTOM; Animated.spring(panY, { toValue: targetValue, useNativeDriver: false, tension: 70, friction: 12, }).start(() => { setIsSheetOpen(shouldOpen); }); }, onPanResponderTerminate: (_, _gestureState) => { panY.flattenOffset(); isDraggingSheet.current = false; const currentPosition = panY._value; const shouldOpen = currentPosition < SNAP_BOTTOM / 2; const targetValue = shouldOpen ? determineBottomSheetHeight() : SNAP_BOTTOM; Animated.spring(panY, { toValue: targetValue, useNativeDriver: false, tension: 70, friction: 12, }).start(() => { setIsSheetOpen(shouldOpen); }); }, }),
).current;
const handleEventCardLayout = (event: LayoutChangeEvent) => {
const {height: cardHeight} = event.nativeEvent.layout;
const isSmallDevice = deviceSize.height <= 700;
const newSingleCardHeight = Math.min(
cardHeight + 32,
deviceSize.height * (isSmallDevice ? 0.25 : 0.3),
);
setSingleEventCardHeight(newSingleCardHeight);
const newDoubleCardHeight = Math.min( cardHeight * 2 + 64, deviceSize.height * (isSmallDevice ? 0.45 : 0.6), ); setDoubleEventCardHeight(newDoubleCardHeight); if (isSheetOpen) { const targetHeight = determineBottomSheetHeight(); Animated.spring(panY, { toValue: targetHeight, useNativeDriver: false, tension: 70, friction: 12, }).start(); }
};
const handleEventCountTextPress = () => {
if (filtersApplied && filteredEvents.length === 0) {
return;
}
if (showSpecificEvent) {
return;
}
setSelectedEventIndex(null); const eventsToShow = isSearchActive ? events.filter(event => { const lowercaseQuery = searchQuery.toLowerCase(); return ( event.location.toLowerCase().includes(lowercaseQuery) || event.name.toLowerCase().includes(lowercaseQuery) ); }) : filtersApplied ? filteredEvents : viewMode === 'events' ? visibleEvents : events; if (eventsToShow.length > 3) { expandToFullScreen(); } else { openBottomSheet(); }
};
const handleSearch = () => {
if (!searchQuery) {
setIsSearchActive(false);
}
searchBottomSheetRef.current?.open();
};
const handleSelectSearchResult = (result: string) => {
setSearchQuery(result);
setIsSearchActive(true);
let matchedEvents = 0;
if (result) { const lowercaseResult = result.toLowerCase(); matchedEvents = events.filter( event => event.location.toLowerCase().includes(lowercaseResult) || event.name.toLowerCase().includes(lowercaseResult), ).length; } setSearchResultsCount(matchedEvents); if (matchedEvents > 0) { setSelectedEventIndex(null); if (matchedEvents > 3) { expandToFullScreen(); } else { openBottomSheet(); } }
};
const handleMapReady = () => {
setMapReady(true); // Trigger image re-render
};
// Fallback image source
const defaultGenreImage = {uri: 'https://via.placeholder.com/66'};
const getSafeGenreImage = useCallback((genre: string) => {
const imageSource = getGenreImage(genre);
if (!imageSource || (typeof imageSource === 'object' && !imageSource.uri)) {
console.warn(Invalid genre image for ${genre}, using default
);
return defaultGenreImage;
}
return imageSource;
}, []);
const getSafeGenreBackgroundImage = useCallback((genre: string) => {
const backgroundImage = getGenreBackgroundImage(genre);
if (
!backgroundImage ||
(typeof backgroundImage === 'object' && !backgroundImage.uri)
) {
console.warn(
Invalid genre background image for ${genre}, using checkmap fallback
,
);
return require('../assets/checkmap.png'); // fallback to your original image
}
return backgroundImage;
}, []);
const currentEvent =
selectedEventIndex !== null && viewMode === 'events'
? visibleEvents[selectedEventIndex]
: null;
const visibleEventsCount = filteredEvents.length;
const isLoadingEvents = showSpecificEvent
? isFetchingSpecificEvent
: isFetchingEvents;
return (
{isLoading && !locationFetched && (
Getting your location...
)}
barStyle="dark-content"
backgroundColor="transparent"
translucent={true}
/>
<MapView ref={mapRef} style={[ StyleSheet.absoluteFillObject, styles.map, !isConnected && styles.mapOffline, ]} initialRegion={{ latitude: region?.latitude || 51.5074, longitude: region?.longitude || -0.1278, latitudeDelta: showSpecificEvent ? 0.001 : 0.009, longitudeDelta: showSpecificEvent ? 0.001 : 0.009, }} onMapReady={handleMapReady} onRegionChangeComplete={handleRegionChange} showsUserLocation={false} showsMyLocationButton={false} showsCompass={false} showsScale={false} showsBuildings={false} showsIndoors={false} toolbarEnabled={false} loadingEnabled={false} moveOnMarkerPress={false}> {mapReady && isConnected && viewMode === 'clusters' && !showSpecificEvent && filteredClusters.map((cluster, index) => ( <Marker key={`cluster-${index}`} ref={ref => (markerRefs.current[`cluster-${index}`] = ref)} // Add ref coordinate={cluster.coordinate} onPress={() => handleClusterPress(index)} tracksViewChanges={isInitialMarkerLoad}> <View style={styles.markerContainer}> <View style={styles.clusterMarkerCircle}> <Text style={styles.markerText}>{cluster.count}</Text> </View> <View style={styles.markerTip} /> </View> </Marker> ))} {mapReady && isConnected && (viewMode === 'events' || showSpecificEvent) && (() => { const adjustedCoordinates = offsetOverlappingCoordinates(visibleEvents); return visibleEvents.map((event, index) => { const isSelected = index === selectedEventIndex; const isZoomedIn = currentZoomLevel <= 0.02 || showSpecificEvent; const showCallout = isSelected || isZoomedIn; return ( <Marker key={`event-${event.id}`} coordinate={adjustedCoordinates[index]} onPress={() => handleEventMarkerPress(index)} tracksViewChanges={isInitialMarkerLoad || showCallout}> <View style={styles.markerContainer}> {showCallout ? ( <View style={styles.calloutContainer}> <View style={styles.heartIconContainer}> <AppImage source={getSafeGenreImage(event.genre)} style={styles.heartIcon} resizeMode="contain" fallbackSource={require('../assets/genres/Blues.png')} /> </View> <ImageBackground source={getSafeGenreBackgroundImage(event.genre)} style={styles.calloutBackground}> <Text style={styles.calloutText}>{event.name}</Text> {event.artist?.map((artist, artistIndex) => ( <Text key={`artist-${artistIndex}`} style={styles.calloutText}> {artist.name} </Text> ))} {event.venue?.map((venue, venueIndex) => ( <Text key={`venue-${venueIndex}`} style={styles.calloutText}> {venue.name} </Text> ))} </ImageBackground> </View> ) : ( <> <View style={[ styles.markerCircle, isSelected && styles.selectedMarkerCircle, ]}> <Text style={[ styles.markerText, isSelected && styles.selectedMarkerText, ]}> {1} </Text> </View> <View style={styles.markerTip} /> </> )} </View> </Marker> ); }); })()} </MapView> <View style={styles.headerContainer}> <HeaderUser title="Map" showSearchBar={!showSpecificEvent} showBackButton={showSpecificEvent} onBackPress={handleBackPress} searchPlaceholder="Event Name" searchQuery={searchQuery} setSearchQuery={setSearchQuery} setTimeFilters={setTimeFilters} timeFilters={timeFilters} genresFilter={genreFilters} setGenresFilters={setGenresFilters} onSearch={handleSearch} /> <TouchableOpacity style={styles.locationButton} onPress={handleCenterToUserLocation} disabled={isLoading}> <Image source={require('../assets/gps.png')} style={styles.locationIcon} /> </TouchableOpacity> <Animated.View style={[ styles.bottomSheet, { transform: [{translateY}], zIndex: dynamicZIndex, ...Platform.select({ android: {elevation}, }), }, ]} onLayout={handleBottomSheetLayout}> <ScrollView ref={scrollViewRef} onLayout={event => { if ( event.nativeEvent.layout.height > 0 && doubleEventCardHeight === 520 ) { const isSmallDevice = deviceSize.height <= 700; setDoubleEventCardHeight( Math.min( event.nativeEvent.layout.height, deviceSize.height * (isSmallDevice ? 0.45 : 0.6), ), ); } }} style={styles.eventCardsList} contentContainerStyle={styles.scrollViewContent} showsVerticalScrollIndicator={true} onScrollBeginDrag={handleScrollBegin} onScrollEndDrag={handleScrollEnd} onMomentumScrollBegin={handleScrollBegin} onMomentumScrollEnd={handleScrollEnd} onScroll={handleScroll} scrollEventThrottle={16} nestedScrollEnabled={true} scrollEnabled={!isDraggingSheet.current} bounces={true} alwaysBounceVertical={false}> {(() => { const eventsToShow = showSpecificEvent ? events : isSearchActive ? events.filter(event => { const lowercaseQuery = searchQuery.toLowerCase(); return ( event.location.toLowerCase().includes(lowercaseQuery) || event.name.toLowerCase().includes(lowercaseQuery) ); }) : filtersApplied ? filteredEvents : viewMode === 'events' ? visibleEvents : events; if (eventsToShow.length === 0) { return ( <Text style={styles.noEventsText}>No events found</Text> ); } else if (eventsToShow.length === 1) { const singleEvent = eventsToShow[0]; return ( <View onLayout={handleEventCardLayout}> <EventCard id={singleEvent.id || ''} name={singleEvent.name} image={singleEvent.image} date={singleEvent.date || singleEvent.dateRange || ''} endTime={singleEvent.end_time || ''} time={singleEvent.time} location={singleEvent.location} priceRange={singleEvent.priceRange} isShare={singleEvent.isShare} canLiked={singleEvent.canLiked} /> </View> ); } return eventsToShow.map((event, index) => ( <View key={`event-card-${event.id}`} style={styles.eventCardContainer} onLayout={index === 0 ? handleEventCardLayout : undefined}> <EventCard id={event.id || ''} name={event.name} image={event.image} date={event.date || event.dateRange || ''} endTime={event.end_time || ''} time={event.time} location={event.location} priceRange={event.priceRange} type="customer" isShare={event.isShare} canLiked={event.canLiked} /> </View> )); })()} <View style={{height: 100}} /> </ScrollView> ) ) : null} </Animated.View> {!showSpecificEvent && ( <SearchBottomSheet ref={searchBottomSheetRef} onSelectResult={handleSelectSearchResult} searchQuery={searchQuery} setSearchQuery={setSearchQuery} /> )} </View>
);
};
export default MapScreenUser;
`
Top comments (0)