diff --git a/docs/mapview.md b/docs/mapview.md
index dab74f93e..b4656c526 100644
--- a/docs/mapview.md
+++ b/docs/mapview.md
@@ -68,6 +68,7 @@ To access event data, you will need to use `e.nativeEvent`. For example, `onPres
| Method Name | Arguments | Notes
|---|---|---|
+| `animateToNavigation` | `location: LatLng`, `bearing: Number`, `angle: Number`, `duration: Number` |
| `animateToRegion` | `region: Region`, `duration: Number` |
| `animateToCoordinate` | `coordinate: LatLng`, `duration: Number` |
| `animateToBearing` | `bearing: Number`, `duration: Number` |
diff --git a/example/App.js b/example/App.js
index ebd4d8c80..7b6231e61 100644
--- a/example/App.js
+++ b/example/App.js
@@ -40,6 +40,7 @@ import MapKml from './examples/MapKml';
import BugMarkerWontUpdate from './examples/BugMarkerWontUpdate';
import ImageOverlayWithAssets from './examples/ImageOverlayWithAssets';
import ImageOverlayWithURL from './examples/ImageOverlayWithURL';
+import AnimatedNavigation from './examples/AnimatedNavigation';
import OnPoiClick from './examples/OnPoiClick';
const IOS = Platform.OS === 'ios';
@@ -159,6 +160,7 @@ class App extends React.Component {
[BugMarkerWontUpdate, 'BUG: Marker Won\'t Update (Android)', true],
[ImageOverlayWithAssets, 'Image Overlay Component with Assets', true],
[ImageOverlayWithURL, 'Image Overlay Component with URL', true],
+ [AnimatedNavigation, 'Animated Map Navigation', true],
[OnPoiClick, 'On Poi Click', true],
]
// Filter out examples that are not yet supported for Google Maps on iOS.
diff --git a/example/android/app/.project b/example/android/app/.project
new file mode 100644
index 000000000..ca3856c62
--- /dev/null
+++ b/example/android/app/.project
@@ -0,0 +1,23 @@
+
+
+ example-android
+ Project example-android created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
diff --git a/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/ExampleApplication.class b/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/ExampleApplication.class
new file mode 100644
index 000000000..3ed43fa99
Binary files /dev/null and b/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/ExampleApplication.class differ
diff --git a/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/MainActivity.class b/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/MainActivity.class
new file mode 100644
index 000000000..41f9343a1
Binary files /dev/null and b/example/android/app/bin/src/main/java/com/airbnb/android/react/maps/example/MainActivity.class differ
diff --git a/example/examples/AnimatedNavigation.js b/example/examples/AnimatedNavigation.js
new file mode 100644
index 000000000..ce7b85f99
--- /dev/null
+++ b/example/examples/AnimatedNavigation.js
@@ -0,0 +1,137 @@
+import React, { Component } from 'react';
+
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Text,
+} from 'react-native';
+
+import MapView from 'react-native-maps';
+import carImage from './assets/car.png';
+
+export default class NavigationMap extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ prevPos: null,
+ curPos: { latitude: 37.420814, longitude: -122.081949 },
+ curAng: 45,
+ latitudeDelta: 0.0922,
+ longitudeDelta: 0.0421,
+ };
+ this.changePosition = this.changePosition.bind(this);
+ this.getRotation = this.getRotation.bind(this);
+ this.updateMap = this.updateMap.bind(this);
+ }
+
+ changePosition(latOffset, lonOffset) {
+ const latitude = this.state.curPos.latitude + latOffset;
+ const longitude = this.state.curPos.longitude + lonOffset;
+ this.setState({ prevPos: this.state.curPos, curPos: { latitude, longitude } });
+ this.updateMap();
+ }
+
+ getRotation(prevPos, curPos) {
+ if (!prevPos) return 0;
+ const xDiff = curPos.latitude - prevPos.latitude;
+ const yDiff = curPos.longitude - prevPos.longitude;
+ return (Math.atan2(yDiff, xDiff) * 180.0) / Math.PI;
+ }
+
+ updateMap() {
+ const { curPos, prevPos, curAng } = this.state;
+ const curRot = this.getRotation(prevPos, curPos);
+ this.map.animateToNavigation(curPos, curRot, curAng);
+ }
+
+ render() {
+ return (
+
+ (this.map = el)}
+ style={styles.flex}
+ minZoomLevel={15}
+ initialRegion={{
+ ...this.state.curPos,
+ latitudeDelta: this.state.latitudeDelta,
+ longitudeDelta: this.state.longitudeDelta,
+ }}
+ >
+
+
+
+ this.changePosition(0.0001, 0)}
+ >
+ + Lat
+
+ this.changePosition(-0.0001, 0)}
+ >
+ - Lat
+
+
+
+ this.changePosition(0, -0.0001)}
+ >
+ - Lon
+
+ this.changePosition(0, 0.0001)}
+ >
+ + Lon
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ flex: {
+ flex: 1,
+ width: '100%',
+ },
+ buttonContainerUpDown: {
+ ...StyleSheet.absoluteFillObject,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ buttonContainerLeftRight: {
+ ...StyleSheet.absoluteFillObject,
+ flexDirection: 'column',
+ justifyContent: 'center',
+ },
+ button: {
+ backgroundColor: 'rgba(100,100,100,0.2)',
+ position: 'absolute',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 20,
+ height: 50,
+ width: 50,
+ },
+ up: {
+ alignSelf: 'flex-start',
+ },
+ down: {
+ alignSelf: 'flex-end',
+ },
+ left: {
+ alignSelf: 'flex-start',
+ },
+ right: {
+ alignSelf: 'flex-end',
+ },
+});
diff --git a/example/examples/assets/car.png b/example/examples/assets/car.png
new file mode 100644
index 000000000..ebe9aa163
Binary files /dev/null and b/example/examples/assets/car.png differ
diff --git a/index.d.ts b/index.d.ts
index 6b15f0f45..673e66d6e 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -98,6 +98,7 @@ declare module "react-native-maps" {
}
export default class MapView extends React.Component {
+ animateToNavigation(location: LatLng, bearing: number, angle: number, duration?: number): void;
animateToRegion(region: Region, duration?: number): void;
animateToCoordinate(latLng: LatLng, duration?: number): void;
animateToBearing(bearing: number, duration?: number): void;
diff --git a/lib/android/.project b/lib/android/.project
new file mode 100644
index 000000000..2a84911fc
--- /dev/null
+++ b/lib/android/.project
@@ -0,0 +1,23 @@
+
+
+ react-native-maps-lib
+ Project react-native-maps-lib created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
diff --git a/lib/android/bin/src/main/java/com/airbnb/android/react/maps b/lib/android/bin/src/main/java/com/airbnb/android/react/maps
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java
index 43651f7e2..ee80725c1 100644
--- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java
+++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java
@@ -22,6 +22,7 @@
import com.google.maps.android.data.kml.KmlLayer;
import java.util.Map;
+import java.util.HashMap;
import javax.annotation.Nullable;
@@ -36,6 +37,7 @@ public class AirMapManager extends ViewGroupManager {
private static final int FIT_TO_SUPPLIED_MARKERS = 6;
private static final int FIT_TO_COORDINATES = 7;
private static final int SET_MAP_BOUNDARIES = 8;
+ private static final int ANIMATE_TO_NAVIGATION = 9;
private final Map MAP_TYPES = MapBuilder.of(
@@ -251,6 +253,17 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr
ReadableMap region;
switch (commandId) {
+ case ANIMATE_TO_NAVIGATION:
+ region = args.getMap(0);
+ lng = region.getDouble("longitude");
+ lat = region.getDouble("latitude");
+ LatLng location = new LatLng(lat, lng);
+ bearing = (float)args.getDouble(1);
+ angle = (float)args.getDouble(2);
+ duration = args.getInt(3);
+ view.animateToNavigation(location, bearing, angle, duration);
+ break;
+
case ANIMATE_TO_REGION:
region = args.getMap(0);
duration = args.getInt(1);
@@ -332,14 +345,15 @@ public Map getExportedCustomDirectEventTypeConstants() {
@Nullable
@Override
public Map getCommandsMap() {
- Map map = MapBuilder.of(
+ Map map = this.CreateMap(
"animateToRegion", ANIMATE_TO_REGION,
"animateToCoordinate", ANIMATE_TO_COORDINATE,
"animateToViewingAngle", ANIMATE_TO_VIEWING_ANGLE,
"animateToBearing", ANIMATE_TO_BEARING,
"fitToElements", FIT_TO_ELEMENTS,
"fitToSuppliedMarkers", FIT_TO_SUPPLIED_MARKERS,
- "fitToCoordinates", FIT_TO_COORDINATES
+ "fitToCoordinates", FIT_TO_COORDINATES,
+ "animateToNavigation", ANIMATE_TO_NAVIGATION
);
map.putAll(MapBuilder.of(
@@ -349,6 +363,20 @@ public Map getCommandsMap() {
return map;
}
+ public static Map CreateMap(
+ K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) {
+ Map map = new HashMap();
+ map.put(k1, v1);
+ map.put(k2, v2);
+ map.put(k3, v3);
+ map.put(k4, v4);
+ map.put(k5, v5);
+ map.put(k6, v6);
+ map.put(k7, v7);
+ map.put(k8, v8);
+ return map;
+ }
+
@Override
public LayoutShadowNode createShadowNodeInstance() {
// A custom shadow node is needed in order to pass back the width/height of the map to the
diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java
index f024ad923..60a27609b 100644
--- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java
+++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java
@@ -609,6 +609,16 @@ public void updateExtraData(Object extraData) {
}
}
+ public void animateToNavigation(LatLng location, float bearing, float angle, int duration) {
+ if (map == null) return;
+ CameraPosition cameraPosition = new CameraPosition.Builder(map.getCameraPosition())
+ .bearing(bearing)
+ .tilt(angle)
+ .target(location)
+ .build();
+ map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), duration, null);
+ }
+
public void animateToRegion(LatLngBounds bounds, int duration) {
if (map == null) return;
map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), duration, null);
diff --git a/lib/components/MapView.js b/lib/components/MapView.js
index 03f50bf0d..a72b5a391 100644
--- a/lib/components/MapView.js
+++ b/lib/components/MapView.js
@@ -538,6 +538,10 @@ class MapView extends React.Component {
}
}
+ animateToNavigation(location, bearing, angle, duration) {
+ this._runCommand('animateToNavigation', [location, bearing, angle, duration || 500]);
+ }
+
animateToRegion(region, duration) {
this._runCommand('animateToRegion', [region, duration || 500]);
}
diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m
index d22155814..b768bf523 100644
--- a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m
+++ b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m
@@ -78,6 +78,29 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(kmlSrc, NSString)
+RCT_EXPORT_METHOD(animateToNavigation:(nonnull NSNumber *)reactTag
+ withRegion:(MKCoordinateRegion)region
+ withBearing:(CGFloat)bearing
+ withAngle:(double)angle
+ withDuration:(CGFloat)duration)
+{
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
+ id view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[AIRGoogleMap class]]) {
+ RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
+ } else {
+ [CATransaction begin];
+ [CATransaction setAnimationDuration:duration/1000];
+ AIRGoogleMap *mapView = (AIRGoogleMap *)view;
+ GMSCameraPosition *camera = [AIRGoogleMap makeGMSCameraPositionFromMap:mapView andMKCoordinateRegion:region];
+ [mapView animateToCameraPosition:camera];
+ [mapView animateToViewingAngle:angle];
+ [mapView animateToBearing:bearing];
+ [CATransaction commit];
+ }
+ }];
+}
+
RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag
withRegion:(MKCoordinateRegion)region
withDuration:(CGFloat)duration)
diff --git a/lib/ios/AirMaps/AIRMapManager.m b/lib/ios/AirMaps/AIRMapManager.m
index e0e112b5a..47f38ac7f 100644
--- a/lib/ios/AirMaps/AIRMapManager.m
+++ b/lib/ios/AirMaps/AIRMapManager.m
@@ -129,6 +129,30 @@ - (UIView *)view
#pragma mark exported MapView methods
+RCT_EXPORT_METHOD(animateToNavigation:(nonnull NSNumber *)reactTag
+ withRegion:(MKCoordinateRegion)region
+ withBearing:(CGFloat)bearing
+ withAngle:(double)angle
+ withDuration:(CGFloat)duration)
+{
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
+ id view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[AIRMap class]]) {
+ RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
+ } else {
+ AIRMap *mapView = (AIRMap *)view;
+ MKMapCamera *mapCamera = [[mapView camera] copy];
+ [mapCamera setPitch:angle];
+ [mapCamera setHeading:bearing];
+
+ [AIRMap animateWithDuration:duration/1000 animations:^{
+ [(AIRMap *)view setRegion:region animated:YES];
+ [mapView setCamera:mapCamera animated:YES];
+ }];
+ }
+ }];
+}
+
RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag
withRegion:(MKCoordinateRegion)region
withDuration:(CGFloat)duration)