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)