diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt
index 0c9f710ee0..722903d706 100644
--- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt
+++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt
@@ -1,13 +1,20 @@
package com.rnmapbox.rnmbx.components.camera
-import com.facebook.react.bridge.Callback
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.view.animation.LinearInterpolator
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
+import com.mapbox.maps.ScreenCoordinate
+import com.mapbox.maps.plugin.animation.MapAnimationOptions
+import com.mapbox.maps.plugin.animation.easeTo
+import com.mapbox.maps.plugin.animation.moveBy
+import com.mapbox.maps.plugin.animation.scaleBy
+import com.mapbox.maps.toCameraOptions
import com.rnmapbox.rnmbx.NativeRNMBXCameraModuleSpec
+import com.rnmapbox.rnmbx.components.camera.constants.CameraMode
import com.rnmapbox.rnmbx.components.mapview.CommandResponse
import com.rnmapbox.rnmbx.utils.ViewRefTag
import com.rnmapbox.rnmbx.utils.ViewTagResolver
@@ -49,4 +56,60 @@ class RNMBXCameraModule(context: ReactApplicationContext, val viewTagResolver: V
promise.resolve(null)
}
}
+
+ private fun getAnimationOptions(
+ animationMode: Double,
+ animationDuration: Double
+ ): MapAnimationOptions {
+ return MapAnimationOptions.Builder()
+ .apply {
+ when (animationMode.toInt()) {
+ CameraMode.LINEAR -> interpolator(LinearInterpolator())
+ CameraMode.EASE -> interpolator(AccelerateDecelerateInterpolator())
+ }
+ animationDuration.let { duration ->
+ duration(duration.toLong())
+ }
+ }
+ .build()
+ }
+
+ override fun moveBy(
+ viewRef: ViewRefTag?,
+ x: Double,
+ y: Double,
+ animationMode: Double,
+ animationDuration: Double,
+ promise: Promise
+ ) {
+ withViewportOnUIThread(viewRef, promise) {
+ it.mapboxMap?.let { map ->
+ val animationOptions = getAnimationOptions(animationMode, animationDuration)
+ map.moveBy(ScreenCoordinate(x, y), animationOptions)
+
+ promise.resolve(null)
+ }
+ }
+ }
+
+ override fun scaleBy(
+ viewRef: ViewRefTag?,
+ x: Double,
+ y: Double,
+ animationMode: Double,
+ animationDuration: Double,
+ scaleFactor: Double,
+ promise: Promise
+ ) {
+ withViewportOnUIThread(viewRef, promise) {
+ it.mapboxMap?.let { map ->
+ val animationOptions =
+ getAnimationOptions(animationMode, animationDuration)
+
+ map.scaleBy(scaleFactor, ScreenCoordinate(x, y), animationOptions)
+
+ promise.resolve(null)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java
index 4cdf8937e0..35444a4a89 100644
--- a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java
+++ b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java
@@ -38,4 +38,12 @@ public NativeRNMBXCameraModuleSpec(ReactApplicationContext reactContext) {
@ReactMethod
@DoNotStrip
public abstract void updateCameraStop(@Nullable Double viewRef, ReadableMap stop, Promise promise);
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void moveBy(@Nullable Double viewRef, double x, double y, double animationMode, double animationDuration, Promise promise);
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void scaleBy(@Nullable Double viewRef, double x, double y, double animationMode, double animationDuration, double scaleFactor, Promise promise);
}
diff --git a/docs/Camera.md b/docs/Camera.md
index 8c8a017636..18fcac86c6 100644
--- a/docs/Camera.md
+++ b/docs/Camera.md
@@ -347,4 +347,24 @@ camera.zoomTo(16, 100);
```
-[Fit](../examples/Camera/Fit)
+[Fit](../examples/Camera/Fit)### moveBy()
+
+Move the map by a given screen coordinate offset with optional animation.
Can be used to get the Android Auto (onScroll) or Carplay(mapTemplate didUpdatePanGestureWithTranslation) pan gesture applied, for these to work properly do not specify animationDuration.
+
+#### arguments
+| Name | Type | Required | Description |
+| ---- | :--: | :------: | :----------: |
+
+
+
+### scaleBy()
+
+Scale the map with optional animation.
Can be used to get Android Auto pinch gesture (onScale with scaleFactor > 0.0 and < 2.0) or Android Auto double tap (onScale with scaleFactor == 2.0) applied.
+
+#### arguments
+| Name | Type | Required | Description |
+| ---- | :--: | :------: | :----------: |
+
+
+
+
diff --git a/docs/docs.json b/docs/docs.json
index e5da3472f4..c9b3b040f8 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -625,6 +625,24 @@
"examples": [
"\ncamera.zoomTo(16);\ncamera.zoomTo(16, 100);\n\n"
]
+ },
+ {
+ "name": "moveBy",
+ "docblock": "Move the map by a given screen coordinate offset with optional animation.\nCan be used to get the Android Auto (onScroll) or Carplay(mapTemplate didUpdatePanGestureWithTranslation) pan gesture applied, for these to work properly do not specify animationDuration.\n\n@param {number} x screen coordinate offset\n@param {number} y screen coordinate offset\n@param {NativeAnimationMode} animationMode mode used for the animation\n@param {number} animationDuration The transition duration\n@param {number} scaleFactor scale factor value > 0.0 and < 2.0 when 1.0 means no scaling, > 1.0 zoom in and < 1.0 zoom out",
+ "modifiers": [],
+ "params": [],
+ "returns": null,
+ "description": "Move the map by a given screen coordinate offset with optional animation.\nCan be used to get the Android Auto (onScroll) or Carplay(mapTemplate didUpdatePanGestureWithTranslation) pan gesture applied, for these to work properly do not specify animationDuration.",
+ "examples": []
+ },
+ {
+ "name": "scaleBy",
+ "docblock": "Scale the map with optional animation.\nCan be used to get Android Auto pinch gesture (onScale with scaleFactor > 0.0 and < 2.0) or Android Auto double tap (onScale with scaleFactor == 2.0) applied.\n\n@param {number} x center screen coordinate\n@param {number} y center screen coordinate\n@param {number} scaleFactor scale factor value > 0.0 and < 2.0 when 1.0 means no scaling, > 1.0 zoom in and < 1.0 zoom out\n@param {NativeAnimationMode} animationMode mode used for the animation\n@param {number} animationDuration The transition duration",
+ "modifiers": [],
+ "params": [],
+ "returns": null,
+ "description": "Scale the map with optional animation.\nCan be used to get Android Auto pinch gesture (onScale with scaleFactor > 0.0 and < 2.0) or Android Auto double tap (onScale with scaleFactor == 2.0) applied.",
+ "examples": []
}
],
"props": [
diff --git a/ios/RNMBX/RNMBXCamera.swift b/ios/RNMBX/RNMBXCamera.swift
index bb37cfbcfc..69c7e2777a 100644
--- a/ios/RNMBX/RNMBXCamera.swift
+++ b/ios/RNMBX/RNMBXCamera.swift
@@ -22,8 +22,12 @@ public protocol RNMBXMapComponent: AnyObject {
func waitForStyleLoad() -> Bool
}
-enum CameraMode: String, CaseIterable {
- case flight, ease, linear, none
+enum CameraMode: Int {
+ case flight = 1
+ case ease = 2
+ case linear = 3
+ case move = 4
+ case none = 5
}
enum UserTrackingMode: String {
@@ -477,7 +481,7 @@ open class RNMBXCamera : RNMBXMapComponentBase {
}
var mode: CameraMode = .flight
- if let m = stop["mode"] as? String, let m = CameraMode(rawValue: m) {
+ if let m = stop["mode"] as? NSNumber, let m = CameraMode(rawValue: m.intValue) {
mode = m
}
@@ -528,6 +532,63 @@ open class RNMBXCamera : RNMBXMapComponentBase {
map.mapView.viewport.removeStatusObserver(self)
return super.removeFromMap(map, reason:reason)
}
+
+ @objc public func moveBy(x: Double, y: Double, animationMode: NSNumber?, animationDuration: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
+ withMapView { mapView in
+ let contentFrame = mapView.bounds.inset(by: mapView.safeAreaInsets)
+ let centerPoint = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
+ let endCameraPoint = CGPoint(x: centerPoint.x + x, y: centerPoint.y + y)
+ let cameraOptions = mapView.mapboxMap.dragCameraOptions(from: centerPoint, to: endCameraPoint)
+
+ let duration = (animationDuration?.doubleValue ?? 0.0) / 1000
+
+ if (duration == 0.0) {
+ mapView.mapboxMap.setCamera(to: cameraOptions)
+ resolve(nil)
+ return
+ }
+
+ var curve: UIView.AnimationCurve = .linear
+ if let m = animationMode?.intValue, let m = CameraMode(rawValue: m) {
+ curve = m == CameraMode.ease ? .easeInOut : .linear
+ }
+
+ mapView.camera.ease(to: cameraOptions, duration: duration, curve: curve, completion: { _ in resolve(nil) })
+ }
+ }
+
+ @objc public func scaleBy(
+ x: Double,
+ y: Double,
+ scaleFactor: NSNumber,
+ animationMode: NSNumber?,
+ animationDuration: NSNumber?,
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ withMapView { mapView in
+ let currentZoom = mapView.cameraState.zoom
+ let newZoom = currentZoom + log2(scaleFactor.doubleValue)
+ let anchor = CGPoint(x: x, y: y)
+ let cameraOptions = CameraOptions(anchor: anchor, zoom: newZoom)
+ let duration = (animationDuration?.doubleValue ?? 0.0) / 1000
+
+ if (duration == 0.0) {
+ mapView.mapboxMap.setCamera(to: cameraOptions)
+ resolve(nil)
+ return
+ }
+
+ var curve: UIView.AnimationCurve = .linear
+ if let m = animationMode?.intValue, let m = CameraMode(rawValue: m) {
+ curve = m == CameraMode.ease ? .easeInOut : .linear
+ }
+
+ mapView.camera.ease(to: cameraOptions, duration: duration, curve: curve) { _ in
+ resolve(nil)
+ }
+ }
+ }
}
// MARK: - ViewportStatusObserver
diff --git a/ios/RNMBX/RNMBXCameraModule.mm b/ios/RNMBX/RNMBXCameraModule.mm
index 101ec8b140..b9a1577d9a 100644
--- a/ios/RNMBX/RNMBXCameraModule.mm
+++ b/ios/RNMBX/RNMBXCameraModule.mm
@@ -62,7 +62,32 @@ - (void)withCamera:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXCamera *))blo
[self withCamera:viewRef block:^(RNMBXCamera *view) {
[view updateCameraStop: stop];
resolve(@true);
- } reject:reject methodName:@"someMethod"];
+ } reject:reject methodName:@"updateCameraStop"];
}
+RCT_EXPORT_METHOD(moveBy:(nonnull NSNumber *)viewRef
+ x:(double)x
+ y:(double)y
+ animationMode:(nonnull NSNumber *)animationMode
+ animationDuration:(nonnull NSNumber *)animationDuration
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self withCamera:viewRef block:^(RNMBXCamera *camera) {
+ [camera moveByX:x y:y animationMode:animationMode animationDuration:animationDuration resolve:resolve reject:reject];
+ } reject:reject methodName:@"moveBy"];
+}
+
+ RCT_EXPORT_METHOD(scaleBy:(nonnull NSNumber *)viewRef
+ x:(double)x
+ y:(double)y
+ animationMode:(nonnull NSNumber *)animationMode
+ animationDuration:(nonnull NSNumber *)animationDuration
+ scaleFactor:(nonnull NSNumber *)scaleFactor
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self withCamera:viewRef block:^(RNMBXCamera *camera) {
+ [camera scaleByX:x y:y scaleFactor:scaleFactor animationMode:animationMode animationDuration:animationDuration resolve:resolve reject:reject];
+ } reject:reject methodName:@"scaleBy"];
+ }
+
@end
diff --git a/src/components/Camera.tsx b/src/components/Camera.tsx
index cc6c7be921..ee6dbc228b 100644
--- a/src/components/Camera.tsx
+++ b/src/components/Camera.tsx
@@ -61,7 +61,13 @@ const nativeAnimationMode = (
// Native module types.
-type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move';
+type FLIGHT = 1;
+type EASE = 2;
+type LINEAR = 3;
+type MOVE = 4;
+type NONE = 5;
+
+type NativeAnimationMode = FLIGHT | EASE | LINEAR | MOVE | NONE;
interface NativeCameraProps extends CameraFollowConfig {
testID?: string;
@@ -100,6 +106,31 @@ export interface CameraRef {
flyTo: (centerCoordinate: Position, animationDuration?: number) => void;
moveTo: (centerCoordinate: Position, animationDuration?: number) => void;
zoomTo: (zoomLevel: number, animationDuration?: number) => void;
+ moveBy: (
+ props:
+ | { x: number; y: number }
+ | {
+ x: number;
+ y: number;
+ animationMode: 'easeTo' | 'linearTo';
+ animationDuration: number;
+ },
+ ) => void;
+ scaleBy: (
+ props:
+ | {
+ x: number;
+ y: number;
+ scaleFactor: number;
+ }
+ | {
+ x: number;
+ y: number;
+ scaleFactor: number;
+ animationMode: 'easeTo' | 'linearTo';
+ animationDuration: number;
+ },
+ ) => void;
}
export type CameraStop = {
@@ -520,6 +551,35 @@ export const Camera = memo(
};
const zoomTo = useCallback(_zoomTo, [setCamera]);
+ const moveBy: CameraRef['moveBy'] = useCallback(
+ (
+ moveProps,
+ ) => {
+ commands.call('moveBy', [
+ moveProps.x,
+ moveProps.y,
+ 'animationMode' in moveProps ? nativeAnimationMode(moveProps.animationMode) : nativeAnimationMode('linearTo'),
+ 'animationDuration' in moveProps ? moveProps.animationDuration : 0,
+ ]);
+ },
+ [commands],
+ );
+
+ const scaleBy: CameraRef['scaleBy'] = useCallback(
+ (
+ scaleProps
+ ) => {
+ commands.call('scaleBy', [
+ scaleProps.x,
+ scaleProps.y,
+ 'animationMode' in scaleProps ? nativeAnimationMode(scaleProps.animationMode) : nativeAnimationMode('linearTo'),
+ 'animationDuration' in scaleProps ? scaleProps.animationDuration : 0,
+ scaleProps.scaleFactor,
+ ]);
+ },
+ [commands],
+ );
+
useImperativeHandle(ref, () => ({
/**
* Sets any camera properties, with default fallbacks if unspecified.
@@ -580,6 +640,28 @@ export const Camera = memo(
* @param {number} animationDuration The transition duration
*/
zoomTo,
+ /**
+ * Move the map by a given screen coordinate offset with optional animation.
+ * Can be used to get the Android Auto (onScroll) or Carplay(mapTemplate didUpdatePanGestureWithTranslation) pan gesture applied, for these to work properly do not specify animationDuration.
+ *
+ * @param {number} x screen coordinate offset
+ * @param {number} y screen coordinate offset
+ * @param {NativeAnimationMode} animationMode mode used for the animation
+ * @param {number} animationDuration The transition duration
+ * @param {number} scaleFactor scale factor value > 0.0 and < 2.0 when 1.0 means no scaling, > 1.0 zoom in and < 1.0 zoom out
+ */
+ moveBy,
+ /**
+ * Scale the map with optional animation.
+ * Can be used to get Android Auto pinch gesture (onScale with scaleFactor > 0.0 and < 2.0) or Android Auto double tap (onScale with scaleFactor == 2.0) applied.
+ *
+ * @param {number} x center screen coordinate
+ * @param {number} y center screen coordinate
+ * @param {number} scaleFactor scale factor value > 0.0 and < 2.0 when 1.0 means no scaling, > 1.0 zoom in and < 1.0 zoom out
+ * @param {NativeAnimationMode} animationMode mode used for the animation
+ * @param {number} animationDuration The transition duration
+ */
+ scaleBy,
}));
return (
diff --git a/src/specs/NativeRNMBXCameraModule.ts b/src/specs/NativeRNMBXCameraModule.ts
index c153c9cba0..e87dc1a888 100644
--- a/src/specs/NativeRNMBXCameraModule.ts
+++ b/src/specs/NativeRNMBXCameraModule.ts
@@ -15,7 +15,7 @@ interface NativeCameraStop {
paddingTop?: number;
paddingBottom?: number;
duration?: number;
- mode?: NativeAnimationMode;
+ mode?: number;
}
type Stop =
@@ -24,13 +24,26 @@ type Stop =
}
| NativeCameraStop;
-type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move';
-
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-unused-vars
type ObjectOr = Object;
export interface Spec extends TurboModule {
updateCameraStop(viewRef: ViewRef, stop: ObjectOr): Promise;
+ moveBy: (
+ viewRef: ViewRef,
+ x: number,
+ y: number,
+ animationMode: number,
+ animationDuration: number,
+ ) => Promise;
+ scaleBy: (
+ viewRef: ViewRef,
+ x: number,
+ y: number,
+ animationMode: number,
+ animationDuration: number,
+ scaleFactor: number,
+ ) => Promise;
}
export default TurboModuleRegistry.getEnforcing('RNMBXCameraModule');
diff --git a/src/specs/codegenUtils.ts b/src/specs/codegenUtils.ts
index a15d962c37..c55e702e8e 100644
--- a/src/specs/codegenUtils.ts
+++ b/src/specs/codegenUtils.ts
@@ -25,5 +25,5 @@ export type NativeCameraStop = {
paddingTop?: Double;
paddingBottom?: Double;
duration?: Double;
- mode?: string;
+ mode?: number;
} | null;
diff --git a/src/web/components/Camera.tsx b/src/web/components/Camera.tsx
index 4b146db2bd..2bde971a14 100644
--- a/src/web/components/Camera.tsx
+++ b/src/web/components/Camera.tsx
@@ -52,7 +52,7 @@ class Camera
'centerCoordinate' | 'zoomLevel' | 'minZoomLevel' | 'maxZoomLevel'
>
>
- implements Omit
+ implements Omit
{
context!: ContextType;