Skip to content

Commit 723db2c

Browse files
zupaoxchristopherdro
authored andcommitted
[Android] Marker share icon image (#2741)
* Add a test example to add massive number of markers on map * Share image for markers to reduce memory usage Use a shared image icon for markers instead of loading and creating bitmap for each marker, which uses more memory in case when we have a lot of markers but only use a limited set of images.
1 parent b7cba7d commit 723db2c

File tree

7 files changed

+320
-6
lines changed

7 files changed

+320
-6
lines changed

example/App.js

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import AnimatedNavigation from './examples/AnimatedNavigation';
4545
import OnPoiClick from './examples/OnPoiClick';
4646
import IndoorMap from './examples/IndoorMap';
4747
import CameraControl from './examples/CameraControl';
48+
import MassiveCustomMarkers from './examples/MassiveCustomMarkers';
4849

4950
const IOS = Platform.OS === 'ios';
5051
const ANDROID = Platform.OS === 'android';
@@ -169,6 +170,7 @@ export default class App extends React.Component<Props> {
169170
[OnPoiClick, 'On Poi Click', true],
170171
[IndoorMap, 'Indoor Map', true],
171172
[CameraControl, 'CameraControl', true],
173+
[MassiveCustomMarkers, 'MassiveCustomMarkers', true],
172174
]
173175
// Filter out examples that are not yet supported for Google Maps on iOS.
174176
.filter(example => ANDROID || (IOS && (example[2] || !this.state.useGoogleMaps)))
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from 'react';
2+
import {
3+
StyleSheet,
4+
View,
5+
Text,
6+
Dimensions,
7+
TouchableOpacity,
8+
} from 'react-native';
9+
10+
import MapView, { Marker, ProviderPropType } from 'react-native-maps';
11+
import flagPinkImg from './assets/flag-pink.png';
12+
13+
const { width, height } = Dimensions.get('window');
14+
15+
const ASPECT_RATIO = width / height;
16+
const LATITUDE = 37.78825;
17+
const LONGITUDE = -122.4324;
18+
const LATITUDE_DELTA = 0.0922;
19+
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
20+
let id = 0;
21+
22+
class MassiveCustomMarkers extends React.Component {
23+
constructor(props) {
24+
super(props);
25+
26+
this.state = {
27+
region: {
28+
latitude: LATITUDE,
29+
longitude: LONGITUDE,
30+
latitudeDelta: LATITUDE_DELTA,
31+
longitudeDelta: LONGITUDE_DELTA,
32+
},
33+
markers: [],
34+
};
35+
36+
this.onMapPress = this.onMapPress.bind(this);
37+
}
38+
39+
generateMarkers(fromCoordinate) {
40+
const result = [];
41+
const { latitude, longitude } = fromCoordinate;
42+
for (let i = 0; i < 100; i++) {
43+
const newMarker = {
44+
coordinate: {
45+
latitude: latitude + (0.001 * i),
46+
longitude: longitude + (0.001 * i),
47+
},
48+
key: `foo${id++}`,
49+
};
50+
result.push(newMarker);
51+
}
52+
return result;
53+
}
54+
55+
onMapPress(e) {
56+
this.setState({
57+
markers: [
58+
...this.state.markers,
59+
...this.generateMarkers(e.nativeEvent.coordinate),
60+
],
61+
});
62+
}
63+
64+
render() {
65+
return (
66+
<View style={styles.container}>
67+
<MapView
68+
provider={this.props.provider}
69+
style={styles.map}
70+
initialRegion={this.state.region}
71+
onPress={this.onMapPress}
72+
>
73+
{this.state.markers.map(marker => (
74+
<Marker
75+
title={marker.key}
76+
image={flagPinkImg}
77+
key={marker.key}
78+
coordinate={marker.coordinate}
79+
/>
80+
))}
81+
</MapView>
82+
<View style={styles.buttonContainer}>
83+
<TouchableOpacity
84+
onPress={() => this.setState({ markers: [] })}
85+
style={styles.bubble}
86+
>
87+
<Text>Tap to create 100 markers</Text>
88+
</TouchableOpacity>
89+
</View>
90+
</View>
91+
);
92+
}
93+
}
94+
95+
MassiveCustomMarkers.propTypes = {
96+
provider: ProviderPropType,
97+
};
98+
99+
const styles = StyleSheet.create({
100+
container: {
101+
...StyleSheet.absoluteFillObject,
102+
justifyContent: 'flex-end',
103+
alignItems: 'center',
104+
},
105+
map: {
106+
...StyleSheet.absoluteFillObject,
107+
},
108+
bubble: {
109+
backgroundColor: 'rgba(255,255,255,0.7)',
110+
paddingHorizontal: 18,
111+
paddingVertical: 12,
112+
borderRadius: 20,
113+
},
114+
latlng: {
115+
width: 200,
116+
alignItems: 'stretch',
117+
},
118+
button: {
119+
width: 80,
120+
paddingHorizontal: 12,
121+
alignItems: 'center',
122+
marginHorizontal: 10,
123+
},
124+
buttonContainer: {
125+
flexDirection: 'row',
126+
marginVertical: 20,
127+
backgroundColor: 'transparent',
128+
},
129+
});
130+
131+
export default MassiveCustomMarkers;

lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
import com.google.android.gms.maps.model.LatLng;
2020
import com.google.android.gms.maps.model.LatLngBounds;
2121
import com.google.android.gms.maps.model.MapStyleOptions;
22-
import com.google.maps.android.data.kml.KmlLayer;
2322

24-
import java.util.Map;
2523
import java.util.HashMap;
24+
import java.util.Map;
2625

2726
import javax.annotation.Nullable;
2827

@@ -52,6 +51,7 @@ public class AirMapManager extends ViewGroupManager<AirMapView> {
5251
);
5352

5453
private final ReactApplicationContext appContext;
54+
private AirMapMarkerManager markerManager;
5555

5656
protected GoogleMapOptions googleMapOptions;
5757

@@ -60,6 +60,13 @@ public AirMapManager(ReactApplicationContext context) {
6060
this.googleMapOptions = new GoogleMapOptions();
6161
}
6262

63+
public AirMapMarkerManager getMarkerManager() {
64+
return this.markerManager;
65+
}
66+
public void setMarkerManager(AirMapMarkerManager markerManager) {
67+
this.markerManager = markerManager;
68+
}
69+
6370
@Override
6471
public String getName() {
6572
return REACT_CLASS;

lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public class AirMapMarker extends AirMapFeature {
7979
private boolean hasViewChanges = true;
8080

8181
private boolean hasCustomMarkerView = false;
82+
private final AirMapMarkerManager markerManager;
83+
private String imageUri;
8284

8385
private final DraweeHolder<?> logoHolder;
8486
private DataSource<CloseableReference<CloseableImage>> dataSource;
@@ -110,20 +112,26 @@ public void onFinalImageSet(
110112
CloseableReference.closeSafely(imageReference);
111113
}
112114
}
115+
if (AirMapMarker.this.markerManager != null && AirMapMarker.this.imageUri != null) {
116+
AirMapMarker.this.markerManager.getSharedIcon(AirMapMarker.this.imageUri)
117+
.updateIcon(iconBitmapDescriptor, iconBitmap);
118+
}
113119
update(true);
114120
}
115121
};
116122

117-
public AirMapMarker(Context context) {
123+
public AirMapMarker(Context context, AirMapMarkerManager markerManager) {
118124
super(context);
119125
this.context = context;
126+
this.markerManager = markerManager;
120127
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
121128
logoHolder.onAttach();
122129
}
123130

124-
public AirMapMarker(Context context, MarkerOptions options) {
131+
public AirMapMarker(Context context, MarkerOptions options, AirMapMarkerManager markerManager) {
125132
super(context);
126133
this.context = context;
134+
this.markerManager = markerManager;
127135
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
128136
logoHolder.onAttach();
129137

@@ -316,6 +324,31 @@ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
316324
public void setImage(String uri) {
317325
hasViewChanges = true;
318326

327+
boolean shouldLoadImage = true;
328+
329+
if (this.markerManager != null) {
330+
// remove marker from previous shared icon if needed, to avoid future updates from it.
331+
// remove the shared icon completely if no markers on it as well.
332+
// this is to avoid memory leak due to orphan bitmaps.
333+
//
334+
// However in case where client want to update all markers from icon A to icon B
335+
// and after some time to update back from icon B to icon A
336+
// it may be better to keep it though. We assume that is rare.
337+
if (this.imageUri != null) {
338+
this.markerManager.getSharedIcon(this.imageUri).removeMarker(this);
339+
this.markerManager.removeSharedIconIfEmpty(this.imageUri);
340+
}
341+
if (uri != null) {
342+
// listening for marker bitmap descriptor update, as well as check whether to load the image.
343+
AirMapMarkerManager.AirMapMarkerSharedIcon sharedIcon = this.markerManager.getSharedIcon(uri);
344+
sharedIcon.addMarker(this);
345+
shouldLoadImage = sharedIcon.shouldLoadImage();
346+
}
347+
}
348+
349+
this.imageUri = uri;
350+
if (!shouldLoadImage) {return;}
351+
319352
if (uri == null) {
320353
iconBitmapDescriptor = null;
321354
update(true);
@@ -346,10 +379,24 @@ public void setImage(String uri) {
346379
drawable.draw(canvas);
347380
}
348381
}
382+
if (this.markerManager != null && uri != null) {
383+
this.markerManager.getSharedIcon(uri).updateIcon(iconBitmapDescriptor, iconBitmap);
384+
}
349385
update(true);
350386
}
351387
}
352388

389+
public void setIconBitmapDescriptor(BitmapDescriptor bitmapDescriptor, Bitmap bitmap) {
390+
this.iconBitmapDescriptor = bitmapDescriptor;
391+
this.iconBitmap = bitmap;
392+
this.hasViewChanges = true;
393+
this.update(true);
394+
}
395+
396+
public void setIconBitmap(Bitmap bitmap) {
397+
this.iconBitmap = bitmap;
398+
}
399+
353400
public MarkerOptions getMarkerOptions() {
354401
if (markerOptions == null) {
355402
markerOptions = new MarkerOptions();

0 commit comments

Comments
 (0)