Skip to content

Commit

Permalink
[Android] Marker share icon image (react-native-maps#2741)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
zupaox authored and christopherdro committed Apr 12, 2019
1 parent b7cba7d commit 723db2c
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 6 deletions.
2 changes: 2 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import AnimatedNavigation from './examples/AnimatedNavigation';
import OnPoiClick from './examples/OnPoiClick';
import IndoorMap from './examples/IndoorMap';
import CameraControl from './examples/CameraControl';
import MassiveCustomMarkers from './examples/MassiveCustomMarkers';

const IOS = Platform.OS === 'ios';
const ANDROID = Platform.OS === 'android';
Expand Down Expand Up @@ -169,6 +170,7 @@ export default class App extends React.Component<Props> {
[OnPoiClick, 'On Poi Click', true],
[IndoorMap, 'Indoor Map', true],
[CameraControl, 'CameraControl', true],
[MassiveCustomMarkers, 'MassiveCustomMarkers', true],
]
// Filter out examples that are not yet supported for Google Maps on iOS.
.filter(example => ANDROID || (IOS && (example[2] || !this.state.useGoogleMaps)))
Expand Down
131 changes: 131 additions & 0 deletions example/examples/MassiveCustomMarkers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import {
StyleSheet,
View,
Text,
Dimensions,
TouchableOpacity,
} from 'react-native';

import MapView, { Marker, ProviderPropType } from 'react-native-maps';
import flagPinkImg from './assets/flag-pink.png';

const { width, height } = Dimensions.get('window');

const ASPECT_RATIO = width / height;
const LATITUDE = 37.78825;
const LONGITUDE = -122.4324;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
let id = 0;

class MassiveCustomMarkers extends React.Component {
constructor(props) {
super(props);

this.state = {
region: {
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
},
markers: [],
};

this.onMapPress = this.onMapPress.bind(this);
}

generateMarkers(fromCoordinate) {
const result = [];
const { latitude, longitude } = fromCoordinate;
for (let i = 0; i < 100; i++) {
const newMarker = {
coordinate: {
latitude: latitude + (0.001 * i),
longitude: longitude + (0.001 * i),
},
key: `foo${id++}`,
};
result.push(newMarker);
}
return result;
}

onMapPress(e) {
this.setState({
markers: [
...this.state.markers,
...this.generateMarkers(e.nativeEvent.coordinate),
],
});
}

render() {
return (
<View style={styles.container}>
<MapView
provider={this.props.provider}
style={styles.map}
initialRegion={this.state.region}
onPress={this.onMapPress}
>
{this.state.markers.map(marker => (
<Marker
title={marker.key}
image={flagPinkImg}
key={marker.key}
coordinate={marker.coordinate}
/>
))}
</MapView>
<View style={styles.buttonContainer}>
<TouchableOpacity
onPress={() => this.setState({ markers: [] })}
style={styles.bubble}
>
<Text>Tap to create 100 markers</Text>
</TouchableOpacity>
</View>
</View>
);
}
}

MassiveCustomMarkers.propTypes = {
provider: ProviderPropType,
};

const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
justifyContent: 'flex-end',
alignItems: 'center',
},
map: {
...StyleSheet.absoluteFillObject,
},
bubble: {
backgroundColor: 'rgba(255,255,255,0.7)',
paddingHorizontal: 18,
paddingVertical: 12,
borderRadius: 20,
},
latlng: {
width: 200,
alignItems: 'stretch',
},
button: {
width: 80,
paddingHorizontal: 12,
alignItems: 'center',
marginHorizontal: 10,
},
buttonContainer: {
flexDirection: 'row',
marginVertical: 20,
backgroundColor: 'transparent',
},
});

export default MassiveCustomMarkers;
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.maps.android.data.kml.KmlLayer;

import java.util.Map;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -52,6 +51,7 @@ public class AirMapManager extends ViewGroupManager<AirMapView> {
);

private final ReactApplicationContext appContext;
private AirMapMarkerManager markerManager;

protected GoogleMapOptions googleMapOptions;

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

public AirMapMarkerManager getMarkerManager() {
return this.markerManager;
}
public void setMarkerManager(AirMapMarkerManager markerManager) {
this.markerManager = markerManager;
}

@Override
public String getName() {
return REACT_CLASS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public class AirMapMarker extends AirMapFeature {
private boolean hasViewChanges = true;

private boolean hasCustomMarkerView = false;
private final AirMapMarkerManager markerManager;
private String imageUri;

private final DraweeHolder<?> logoHolder;
private DataSource<CloseableReference<CloseableImage>> dataSource;
Expand Down Expand Up @@ -110,20 +112,26 @@ public void onFinalImageSet(
CloseableReference.closeSafely(imageReference);
}
}
if (AirMapMarker.this.markerManager != null && AirMapMarker.this.imageUri != null) {
AirMapMarker.this.markerManager.getSharedIcon(AirMapMarker.this.imageUri)
.updateIcon(iconBitmapDescriptor, iconBitmap);
}
update(true);
}
};

public AirMapMarker(Context context) {
public AirMapMarker(Context context, AirMapMarkerManager markerManager) {
super(context);
this.context = context;
this.markerManager = markerManager;
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
logoHolder.onAttach();
}

public AirMapMarker(Context context, MarkerOptions options) {
public AirMapMarker(Context context, MarkerOptions options, AirMapMarkerManager markerManager) {
super(context);
this.context = context;
this.markerManager = markerManager;
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
logoHolder.onAttach();

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

boolean shouldLoadImage = true;

if (this.markerManager != null) {
// remove marker from previous shared icon if needed, to avoid future updates from it.
// remove the shared icon completely if no markers on it as well.
// this is to avoid memory leak due to orphan bitmaps.
//
// However in case where client want to update all markers from icon A to icon B
// and after some time to update back from icon B to icon A
// it may be better to keep it though. We assume that is rare.
if (this.imageUri != null) {
this.markerManager.getSharedIcon(this.imageUri).removeMarker(this);
this.markerManager.removeSharedIconIfEmpty(this.imageUri);
}
if (uri != null) {
// listening for marker bitmap descriptor update, as well as check whether to load the image.
AirMapMarkerManager.AirMapMarkerSharedIcon sharedIcon = this.markerManager.getSharedIcon(uri);
sharedIcon.addMarker(this);
shouldLoadImage = sharedIcon.shouldLoadImage();
}
}

this.imageUri = uri;
if (!shouldLoadImage) {return;}

if (uri == null) {
iconBitmapDescriptor = null;
update(true);
Expand Down Expand Up @@ -346,10 +379,24 @@ public void setImage(String uri) {
drawable.draw(canvas);
}
}
if (this.markerManager != null && uri != null) {
this.markerManager.getSharedIcon(uri).updateIcon(iconBitmapDescriptor, iconBitmap);
}
update(true);
}
}

public void setIconBitmapDescriptor(BitmapDescriptor bitmapDescriptor, Bitmap bitmap) {
this.iconBitmapDescriptor = bitmapDescriptor;
this.iconBitmap = bitmap;
this.hasViewChanges = true;
this.update(true);
}

public void setIconBitmap(Bitmap bitmap) {
this.iconBitmap = bitmap;
}

public MarkerOptions getMarkerOptions() {
if (markerOptions == null) {
markerOptions = new MarkerOptions();
Expand Down
Loading

0 comments on commit 723db2c

Please sign in to comment.