Skip to content

Commit ba623f7

Browse files
miracle2krborn
authored andcommitted
Provide a camera system (react-native-maps#2563)
* Add initialCamera and camera props. * Support camera and initialCamera properties on Android. * Declare camera and initialCamera properties in JS. * Add animateCamera, setCamera, getCamera on iOS/Mapkit. Deprecate the old animateTo* methods and warn on use. * Add an example for camera control. * Add animateCamera, setCamera, getCamera on iOS/GMaps. * Support setCamera, animateCamera, getCamera in Android. * Fix eslint errors. * Update documentation. * Remove use of deprecated methods from examples. * Fix crash in animateCamera() if duration not given. * Add new methods and props to TypeScript typings. * Fix bug in Android method map setup.
1 parent 0f63870 commit ba623f7

20 files changed

+654
-15
lines changed

docs/mapview.md

+29-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
| `provider` | `string` | | The map framework to use. <br/><br/>Either `"google"` for GoogleMaps, otherwise `null` or `undefined` to use the native map framework (`MapKit` in iOS and `GoogleMaps` in android).
88
| `region` | `Region` | | The region to be displayed by the map. <br/><br/>The region is defined by the center coordinates and the span of coordinates to display.
99
| `initialRegion` | `Region` | | The initial region to be displayed by the map. Use this prop instead of `region` only if you don't want to control the viewport of the map besides the initial region.<br/><br/> Changing this prop after the component has mounted will not result in a region change.<br/><br/> This is similar to the `initialValue` prop of a text input.
10+
| `camera` | `Camera` | | The camera view the map should display. If you use this, the `region` property is ignored.
11+
| `initialCamera` | `Camera` | | Like `initialRegion`, use this prop instead of `camera` only if you don't want to control the viewport of the map besides the initial camera setting.<br/><br/> Changing this prop after the component has mounted will not result in a region change.<br/><br/> This is similar to the `initialValue` prop of a text input.
1012
| `mapPadding` | `EdgePadding` | | Adds custom padding to each side of the map. Useful when map elements/markers are obscured. **Note** Google Maps only.
1113
| `paddingAdjustmentBehavior` | 'always'\|'automatic'\|'never' | 'never' | Indicates how/when to affect padding with safe area insets (`GoogleMaps` in iOS only)
1214
| `liteMode` | `Boolean` | `false` | Enable lite mode. **Note**: Android only.
@@ -71,11 +73,14 @@ To access event data, you will need to use `e.nativeEvent`. For example, `onPres
7173

7274
| Method Name | Arguments | Notes
7375
|---|---|---|
74-
| `animateToNavigation` | `location: LatLng`, `bearing: Number`, `angle: Number`, `duration: Number` |
76+
| `getCamera` | | Returns a `Camera` structure indicating the current camera configuration.
77+
| `animateCamera` | `camera: Camera`, `{ duration: Number }` | Animate the camera to a new view. You can pass a partial camera object here; any property not given will remain unmodified.
78+
| `setCamera` | `camera: Camera`, `{ duration: Number }` | Like `animateCamera`, but sets the new view instantly, without an animation.
7579
| `animateToRegion` | `region: Region`, `duration: Number` |
76-
| `animateToCoordinate` | `coordinate: LatLng`, `duration: Number` |
77-
| `animateToBearing` | `bearing: Number`, `duration: Number` |
78-
| `animateToViewingAngle` | `angle: Number`, `duration: Number` |
80+
| `animateToNavigation` | `location: LatLng`, `bearing: Number`, `angle: Number`, `duration: Number` | Deprecated. Use `animateCamera` instead.
81+
| `animateToCoordinate` | `coordinate: LatLng`, `duration: Number` | Deprecated. Use `animateCamera` instead.
82+
| `animateToBearing` | `bearing: Number`, `duration: Number` | Deprecated. Use `animateCamera` instead.
83+
| `animateToViewingAngle` | `angle: Number`, `duration: Number` | Deprecated. Use `animateCamera` instead.
7984
| `getMapBoundaries` | | `Promise<{northEast: LatLng, southWest: LatLng}>`
8085
| `setMapBoundaries` | `northEast: LatLng`, `southWest: LatLng` | `GoogleMaps only`
8186
| `setIndoorActiveLevelIndex` | `levelIndex: Number` |
@@ -98,6 +103,26 @@ type Region {
98103
}
99104
```
100105

106+
```
107+
type Camera = {
108+
center: {
109+
latitude: number,
110+
longitude: number,
111+
},
112+
pitch: number,
113+
heading: number
114+
115+
// Only on iOS MapKit, in meters. The property is ignored by Google Maps.
116+
altitude: number.
117+
118+
// Only when using Google Maps.
119+
zoom: number
120+
}
121+
```
122+
123+
Note that when using the `Camera`, MapKit on iOS and Google Maps differ in how the height is specified. For a cross-platform app, it is necessary
124+
to specify both the zoom level and the altitude separately.
125+
101126
```
102127
type LatLng {
103128
latitude: Number,

example/App.js

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import ImageOverlayWithURL from './examples/ImageOverlayWithURL';
4444
import AnimatedNavigation from './examples/AnimatedNavigation';
4545
import OnPoiClick from './examples/OnPoiClick';
4646
import IndoorMap from './examples/IndoorMap';
47+
import CameraControl from './examples/CameraControl';
4748

4849
const IOS = Platform.OS === 'ios';
4950
const ANDROID = Platform.OS === 'android';
@@ -166,6 +167,7 @@ class App extends React.Component {
166167
[AnimatedNavigation, 'Animated Map Navigation', true],
167168
[OnPoiClick, 'On Poi Click', true],
168169
[IndoorMap, 'Indoor Map', true],
170+
[CameraControl, 'CameraControl', true],
169171
]
170172
// Filter out examples that are not yet supported for Google Maps on iOS.
171173
.filter(example => ANDROID || (IOS && (example[2] || !this.state.useGoogleMaps)))

example/examples/AnimatedNavigation.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class NavigationMap extends Component {
4343
updateMap() {
4444
const { curPos, prevPos, curAng } = this.state;
4545
const curRot = this.getRotation(prevPos, curPos);
46-
this.map.animateToNavigation(curPos, curRot, curAng);
46+
this.map.animateCamera({ heading: curRot, center: curPos, pitch: curAng });
4747
}
4848

4949
render() {

example/examples/CameraControl.js

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React from 'react';
2+
import {
3+
StyleSheet,
4+
View,
5+
TouchableOpacity,
6+
Text,
7+
Alert,
8+
} from 'react-native';
9+
10+
import MapView, { ProviderPropType } from 'react-native-maps';
11+
12+
const LATITUDE = 37.78825;
13+
const LONGITUDE = -122.4324;
14+
15+
16+
class CameraControl extends React.Component {
17+
async getCamera() {
18+
const camera = await this.map.getCamera();
19+
Alert.alert(
20+
'Current camera',
21+
JSON.stringify(camera),
22+
[
23+
{ text: 'OK' },
24+
],
25+
{ cancelable: true }
26+
);
27+
}
28+
29+
async setCamera() {
30+
const camera = await this.map.getCamera();
31+
// Note that we do not have to pass a full camera object to setCamera().
32+
// Similar to setState(), we can pass only the properties you like to change.
33+
this.map.setCamera({
34+
heading: camera.heading + 10,
35+
});
36+
}
37+
38+
async animateCamera() {
39+
const camera = await this.map.getCamera();
40+
camera.heading += 40;
41+
camera.pitch += 10;
42+
camera.altitude += 1000;
43+
camera.zoom -= 1;
44+
camera.center.latitude += 0.5;
45+
this.map.animateCamera(camera, { duration: 2000 });
46+
}
47+
48+
render() {
49+
return (
50+
<View style={styles.container}>
51+
<MapView
52+
provider={this.props.provider}
53+
ref={ref => {
54+
this.map = ref;
55+
}}
56+
style={styles.map}
57+
initialCamera={{
58+
center: {
59+
latitude: LATITUDE,
60+
longitude: LONGITUDE,
61+
},
62+
pitch: 45,
63+
heading: 90,
64+
altitude: 1000,
65+
zoom: 10,
66+
}}
67+
/>
68+
<View style={styles.buttonContainer}>
69+
<TouchableOpacity
70+
onPress={() => this.getCamera()}
71+
style={[styles.bubble, styles.button]}
72+
>
73+
<Text>Get current camera</Text>
74+
</TouchableOpacity>
75+
<TouchableOpacity
76+
onPress={() => this.setCamera()}
77+
style={[styles.bubble, styles.button]}
78+
>
79+
<Text>Set Camera</Text>
80+
</TouchableOpacity>
81+
<TouchableOpacity
82+
onPress={() => this.animateCamera()}
83+
style={[styles.bubble, styles.button]}
84+
>
85+
<Text>Animate Camera</Text>
86+
</TouchableOpacity>
87+
</View>
88+
</View>
89+
);
90+
}
91+
}
92+
93+
CameraControl.propTypes = {
94+
provider: ProviderPropType,
95+
};
96+
97+
98+
const styles = StyleSheet.create({
99+
container: {
100+
...StyleSheet.absoluteFillObject,
101+
justifyContent: 'flex-end',
102+
alignItems: 'center',
103+
},
104+
map: {
105+
...StyleSheet.absoluteFillObject,
106+
},
107+
bubble: {
108+
backgroundColor: 'rgba(255,255,255,0.7)',
109+
paddingHorizontal: 18,
110+
paddingVertical: 12,
111+
borderRadius: 20,
112+
},
113+
button: {
114+
marginTop: 12,
115+
paddingHorizontal: 12,
116+
alignItems: 'center',
117+
marginHorizontal: 10,
118+
},
119+
buttonContainer: {
120+
flexDirection: 'column',
121+
marginVertical: 20,
122+
backgroundColor: 'transparent',
123+
},
124+
});
125+
126+
export default CameraControl;

example/examples/DisplayLatLng.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ class DisplayLatLng extends React.Component {
4444
}
4545

4646
animateRandomCoordinate() {
47-
this.map.animateToCoordinate(this.randomCoordinate());
47+
this.map.animateCamera({ center: this.randomCoordinate() });
4848
}
4949

5050
animateToRandomBearing() {
51-
this.map.animateToBearing(this.getRandomFloat(-360, 360));
51+
this.map.animateCamera({ heading: this.getRandomFloat(-360, 360) });
5252
}
5353

5454
animateToRandomViewingAngle() {
55-
this.map.animateToViewingAngle(this.getRandomFloat(0, 90));
55+
this.map.animateCamera({ pitch: this.getRandomFloat(0, 90) });
5656
}
5757

5858
getRandomFloat(min, max) {

index.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ declare module "react-native-maps" {
2121
longitude: number;
2222
}
2323

24+
export interface Camera {
25+
center: LatLng;
26+
heading: number;
27+
pitch: number;
28+
zoom: number;
29+
altitude: number;
30+
}
31+
2432
export interface Point {
2533
x: number;
2634
y: number;
@@ -186,6 +194,8 @@ declare module "react-native-maps" {
186194
mapType?: MapTypes;
187195
region?: Region;
188196
initialRegion?: Region;
197+
camera?: Camera;
198+
initialCamera?: Camera;
189199
liteMode?: boolean;
190200
mapPadding?: EdgePadding;
191201
maxDelta?: number;
@@ -215,6 +225,9 @@ declare module "react-native-maps" {
215225
}
216226

217227
export default class MapView extends React.Component<MapViewProps, any> {
228+
getCamera(): Promise<Camera>;
229+
setCamera(camera: Partial<Camera>);
230+
animateCamera(camera: Partial<Camera>, opts?: {duration?: number});
218231
animateToNavigation(location: LatLng, bearing: number, angle: number, duration?: number): void;
219232
animateToRegion(region: Region, duration?: number): void;
220233
animateToCoordinate(latLng: LatLng, duration?: number): void;

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public class AirMapManager extends ViewGroupManager<AirMapView> {
3939
private static final int SET_MAP_BOUNDARIES = 8;
4040
private static final int ANIMATE_TO_NAVIGATION = 9;
4141
private static final int SET_INDOOR_ACTIVE_LEVEL_INDEX = 10;
42+
private static final int SET_CAMERA = 11;
43+
private static final int ANIMATE_CAMERA = 12;
4244

4345

4446
private final Map<String, Integer> MAP_TYPES = MapBuilder.of(
@@ -88,6 +90,16 @@ public void setInitialRegion(AirMapView view, ReadableMap initialRegion) {
8890
view.setInitialRegion(initialRegion);
8991
}
9092

93+
@ReactProp(name = "camera")
94+
public void setCamera(AirMapView view, ReadableMap camera) {
95+
view.setCamera(camera);
96+
}
97+
98+
@ReactProp(name = "initialCamera")
99+
public void setInitialCamera(AirMapView view, ReadableMap initialCamera) {
100+
view.setInitialCamera(initialCamera);
101+
}
102+
91103
@ReactProp(name = "mapType")
92104
public void setMapType(AirMapView view, @Nullable String mapType) {
93105
int typeId = MAP_TYPES.get(mapType);
@@ -252,8 +264,20 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr
252264
float bearing;
253265
float angle;
254266
ReadableMap region;
267+
ReadableMap camera;
255268

256269
switch (commandId) {
270+
case SET_CAMERA:
271+
camera = args.getMap(0);
272+
view.animateToCamera(camera, 0);
273+
break;
274+
275+
case ANIMATE_CAMERA:
276+
camera = args.getMap(0);
277+
duration = args.getInt(1);
278+
view.animateToCamera(camera, duration);
279+
break;
280+
257281
case ANIMATE_TO_NAVIGATION:
258282
region = args.getMap(0);
259283
lng = region.getDouble("longitude");
@@ -356,6 +380,8 @@ public Map getExportedCustomDirectEventTypeConstants() {
356380
@Override
357381
public Map<String, Integer> getCommandsMap() {
358382
Map<String, Integer> map = this.CreateMap(
383+
"setCamera", SET_CAMERA,
384+
"animateCamera", ANIMATE_CAMERA,
359385
"animateToRegion", ANIMATE_TO_REGION,
360386
"animateToCoordinate", ANIMATE_TO_COORDINATE,
361387
"animateToViewingAngle", ANIMATE_TO_VIEWING_ANGLE,
@@ -375,7 +401,7 @@ public Map<String, Integer> getCommandsMap() {
375401
}
376402

377403
public static <K, V> Map<K, V> CreateMap(
378-
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) {
404+
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, K k9, V v9, K k10, V v10) {
379405
Map map = new HashMap<K, V>();
380406
map.put(k1, v1);
381407
map.put(k2, v2);
@@ -385,6 +411,8 @@ public static <K, V> Map<K, V> CreateMap(
385411
map.put(k6, v6);
386412
map.put(k7, v7);
387413
map.put(k8, v8);
414+
map.put(k9, v9);
415+
map.put(k10, v10);
388416
return map;
389417
}
390418

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

+38
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.facebook.react.uimanager.UIManagerModule;
2020
import com.google.android.gms.maps.GoogleMap;
2121
import com.google.android.gms.common.GoogleApiAvailability;
22+
import com.google.android.gms.maps.model.CameraPosition;
2223
import com.google.android.gms.maps.model.LatLng;
2324

2425
import java.io.ByteArrayOutputStream;
@@ -140,6 +141,43 @@ public void onSnapshotReady(@Nullable Bitmap snapshot) {
140141
});
141142
}
142143

144+
@ReactMethod
145+
public void getCamera(final int tag, final Promise promise) {
146+
final ReactApplicationContext context = getReactApplicationContext();
147+
148+
UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class);
149+
uiManager.addUIBlock(new UIBlock()
150+
{
151+
@Override
152+
public void execute(NativeViewHierarchyManager nvhm)
153+
{
154+
AirMapView view = (AirMapView) nvhm.resolveView(tag);
155+
if (view == null) {
156+
promise.reject("AirMapView not found");
157+
return;
158+
}
159+
if (view.map == null) {
160+
promise.reject("AirMapView.map is not valid");
161+
return;
162+
}
163+
164+
CameraPosition position = view.map.getCameraPosition();
165+
166+
WritableMap centerJson = new WritableNativeMap();
167+
centerJson.putDouble("latitude", position.target.latitude);
168+
centerJson.putDouble("longitude", position.target.longitude);
169+
170+
WritableMap cameraJson = new WritableNativeMap();
171+
cameraJson.putMap("center", centerJson);
172+
cameraJson.putDouble("heading", (double)position.bearing);
173+
cameraJson.putDouble("zoom", (double)position.zoom);
174+
cameraJson.putDouble("pitch", (double)position.tilt);
175+
176+
promise.resolve(cameraJson);
177+
}
178+
});
179+
}
180+
143181
@ReactMethod
144182
public void pointForCoordinate(final int tag, ReadableMap coordinate, final Promise promise) {
145183
final ReactApplicationContext context = getReactApplicationContext();

0 commit comments

Comments
 (0)