Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.rnmapbox.rnmbx.events.constants.EventKeys
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.RNMBXMapViewManagerDelegate
import com.facebook.react.viewmanagers.RNMBXMapViewManagerInterface
import com.mapbox.maps.ScreenCoordinate
import com.mapbox.maps.MapboxExperimental
import com.mapbox.maps.SymbolScaleBehavior
import com.mapbox.maps.extension.style.layers.properties.generated.ProjectionName
Expand All @@ -22,6 +23,7 @@ import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.ViewTagResolver
import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotBoolean
import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotDouble
import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotMap
import java.lang.Exception
import java.util.HashMap

Expand Down Expand Up @@ -170,13 +172,24 @@ open class RNMBXMapViewManager(context: ReactApplicationContext, val viewTagReso
override fun setGestureSettings(mapView: RNMBXMapView, settings: Dynamic) {
mapView.withMap {
it.gesturesPlugin {
val map = settings.asMap()
val map = if (settings.type == ReadableType.Map) settings.asMap() else null
if (map == null) {
Logger.e(LOG_TAG, "gestureSettings map is null")
mapView.pinchPanEnabledSetByApp = false
this.updateSettings { this.focalPoint = null }
return@gesturesPlugin Unit
}
mapView.pinchPanEnabledSetByApp = map.hasKey("pinchPanEnabled") && !map.isNull("pinchPanEnabled")
this.updateSettings {
val focalPoint = map.takeIf { it.hasKey("focalPoint") && !it.isNull("focalPoint") }
?.getAndLogIfNotMap("focalPoint", LOG_TAG)
val x = focalPoint?.getAndLogIfNotDouble("x", LOG_TAG)
val y = focalPoint?.getAndLogIfNotDouble("y", LOG_TAG)
this.focalPoint = if (x != null && y != null) {
val density = mapView.resources.displayMetrics.density
ScreenCoordinate(x * density, y * density)
} else {
null
}
map.getAndLogIfNotBoolean("doubleTapToZoomInEnabled", LOG_TAG)?.let {
this.doubleTapToZoomInEnabled = it
}
Expand Down
2 changes: 1 addition & 1 deletion docs/Camera.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ compatibility; the root `padding` prop should be used instead. */
heading: number; /* Heading (bearing, orientation) of the map, measured in degrees clockwise from true north. */
pitch: number; /* The pitch toward the horizon measured in degrees, with 0 degrees resulting in a top-down view for a two-dimensional map. */
zoomLevel: number; /* The zoom level of the map. */
padding: signature; /* The viewport padding in points. An omitted edge keeps its current padding. */
padding: {paddingLeft: number, paddingRight: number, paddingTop: number, paddingBottom: number}; /* The viewport padding in points. An omitted edge keeps its current padding. */
animationDuration: number; /* The duration the map takes to animate to a new configuration. */
animationMode: 'flyTo' \| 'easeTo' \| 'linearTo' \| 'moveTo' \| 'none'; /* The easing or path the camera uses to animate to a new configuration. */
}
Expand Down
8 changes: 7 additions & 1 deletion docs/MapView.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ Options a platform doesn't support are ignored there with a warning. An empty ar
type GestureSettings = {
doubleTapToZoomInEnabled: boolean; /* Whether double tapping the map with one touch results in a zoom-in animation. */
doubleTouchToZoomOutEnabled: boolean; /* Whether single tapping the map with two touches results in a zoom-out animation. */
focalPoint: {x: number, y: number}; /* By default, gestures rotate and zoom around the center of the gesture. Set
this property to rotate and zoom around a fixed point instead, given in
points (iOS) / dp (Android) relative to the top-left of the map view.
Applies to pinch, rotate, double-tap and two-finger tap zoom.
On iOS, pinch ignores the focal point while `pinchPanEnabled` is `true` (the default),
so set `pinchPanEnabled: false` to keep pinch anchored to it. */
pinchPanEnabled: boolean; /* Whether pan/scroll is enabled for the pinch gesture. */
pinchZoomEnabled: boolean; /* Whether zoom is enabled for the pinch gesture. */
pinchZoomDecelerationEnabled: boolean; /* Whether a deceleration animation following a pinch-zoom gesture is enabled. True by default.
Expand All @@ -388,7 +394,7 @@ On android set to 0 to disable deceleration, and non zero to enabled it. */
```
Gesture configuration allows to control the user touch interaction.


[Gesture Focal Point](../examples/Map/GestureFocalPoint)

### onPress

Expand Down
9 changes: 8 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@
{
"name": "padding",
"required": false,
"type": "signature",
"type": "{paddingLeft: number, paddingRight: number, paddingTop: number, paddingBottom: number}",
"default": "none",
"description": "The viewport padding in points. An omitted edge keeps its current padding."
},
Expand Down Expand Up @@ -5566,6 +5566,13 @@
"default": "none",
"description": "Whether single tapping the map with two touches results in a zoom-out animation."
},
{
"name": "focalPoint",
"required": false,
"type": "{x: number, y: number}",
"default": "none",
"description": "By default, gestures rotate and zoom around the center of the gesture. Set\nthis property to rotate and zoom around a fixed point instead, given in\npoints (iOS) / dp (Android) relative to the top-left of the map view.\nApplies to pinch, rotate, double-tap and two-finger tap zoom.\nOn iOS, pinch ignores the focal point while `pinchPanEnabled` is `true` (the default),\nso set `pinchPanEnabled: false` to keep pinch anchored to it."
},
{
"name": "pinchPanEnabled",
"required": false,
Expand Down
12 changes: 12 additions & 0 deletions docs/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,18 @@
"relPath": "Map/DynamicUrl.tsx",
"name": "DynamicUrl"
},
{
"metadata": {
"title": "Gesture Focal Point",
"tags": [
"MapView#gestureSettings"
],
"docs": "\nPinch, rotate and double-tap zoom pivot around the red marker instead of the touch location while `gestureSettings.focalPoint` is set. The coordinate under the marker stays the same across those gestures.\n"
},
"fullPath": "example/src/examples/Map/GestureFocalPoint.tsx",
"relPath": "Map/GestureFocalPoint.tsx",
"name": "GestureFocalPoint"
},
{
"metadata": {
"title": "Localize Labels",
Expand Down
92 changes: 92 additions & 0 deletions example/src/examples/Map/GestureFocalPoint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useCallback, useRef, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import Mapbox from '@rnmapbox/maps';
import { type ExampleWithMetadata } from '../common/ExampleMetadata';

const FOCAL_POINT = { x: 100, y: 150 };
const MARKER_SIZE = 24;

const GestureFocalPoint = () => {
const mapRef = useRef<Mapbox.MapView>(null);
const [focalPointEnabled, setFocalPointEnabled] = useState(true);
const [status, setStatus] = useState('');

const handleMapIdle = useCallback(async (state: Mapbox.MapState) => {
const coordinate = await mapRef.current?.getCoordinateFromView([
FOCAL_POINT.x,
FOCAL_POINT.y,
]);
if (!coordinate) return;
const [lon = 0, lat = 0] = coordinate;
setStatus(
`zoom ${state.properties.zoom.toFixed(
2,
)} · heading ${state.properties.heading.toFixed(0)}°\n` +
`under marker: ${lon.toFixed(4)}, ${lat.toFixed(4)}`,
);
}, []);

return (
<View style={styles.container}>
<View style={styles.mapContainer}>
<Mapbox.MapView
ref={mapRef}
testID="gesture-focal-point-map"
style={StyleSheet.absoluteFill}
gestureSettings={{
focalPoint: focalPointEnabled ? FOCAL_POINT : undefined,
}}
onMapIdle={handleMapIdle}
>
<Mapbox.Camera
defaultSettings={{
centerCoordinate: [-73.9857, 40.7484],
zoomLevel: 12,
}}
/>
</Mapbox.MapView>
{focalPointEnabled && (
<View pointerEvents="none" style={styles.marker} />
)}
</View>
<View style={styles.controls}>
<Text testID="gesture-focal-point-status">{status}</Text>
<Button
testID="gesture-focal-point-toggle"
title={
focalPointEnabled ? 'Disable focal point' : 'Enable focal point'
}
onPress={() => setFocalPointEnabled((enabled) => !enabled)}
/>
</View>
</View>
);
};

const styles = StyleSheet.create({
container: { flex: 1 },
mapContainer: { flex: 1 },
marker: {
position: 'absolute',
left: FOCAL_POINT.x - MARKER_SIZE / 2,
top: FOCAL_POINT.y - MARKER_SIZE / 2,
width: MARKER_SIZE,
height: MARKER_SIZE,
borderRadius: MARKER_SIZE / 2,
borderWidth: 3,
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.2)',
},
controls: { padding: 8, gap: 4 },
});

const metadata: ExampleWithMetadata['metadata'] = {
title: 'Gesture Focal Point',
tags: ['MapView#gestureSettings'],
docs: `
Pinch, rotate and double-tap zoom pivot around the red marker instead of the touch location while \`gestureSettings.focalPoint\` is set. The coordinate under the marker stays the same across those gestures.
`,
};
GestureFocalPoint.metadata = metadata;

export default GestureFocalPoint;
1 change: 1 addition & 0 deletions example/src/examples/Map/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { default as LocalizeLabels } from './LocalizeLabels';
export { default as SymbolScaleBehavior } from './SymbolScaleBehavior';
export { default as SetLanguage } from './SetLanguage';
export { default as ScreenCoordinates} from './ScreenCoordinates';
export { default as GestureFocalPoint } from './GestureFocalPoint';
export { default as DebugOptions } from './DebugOptions';

export const metadata = {
Expand Down
11 changes: 11 additions & 0 deletions ios/RNMBX/RNMBXMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ open class RNMBXMapView: UIView, RCTInvalidating {
var panEnabled: Bool? = nil;
var panDecelerationFactor: CGFloat? = nil;
var simultaneousRotateAndPinchZoomEnabled: Bool? = nil;
var focalPoint: CGPoint? = nil
}

var gestureSettings = GestureSettings()
Expand Down Expand Up @@ -839,12 +840,21 @@ open class RNMBXMapView: UIView, RCTInvalidating {
if let simultaneousRotateAndPinchZoomEnabled = value["simultaneousRotateAndPinchZoomEnabled"] as? NSNumber {
options.simultaneousRotateAndPinchZoomEnabled = simultaneousRotateAndPinchZoomEnabled.boolValue
}
if let focalPoint = value["focalPoint"] as? NSDictionary,
let x = focalPoint["x"] as? NSNumber, let y = focalPoint["y"] as? NSNumber {
options.focalPoint = CGPoint(x: x.doubleValue, y: y.doubleValue)
} else {
options.focalPoint = nil
}
/* android only
if let zoomAnimationAmount = value["zoomAnimationAmount"] as? NSNumber {
options.zoomAnimationAmount = zoomAnimationAmount.CGFloat
}*/
gestureSettings = options

changed(.gestureSettings)
} else if gestureSettings.focalPoint != nil {
gestureSettings.focalPoint = nil
changed(.gestureSettings)
}
}
Expand Down Expand Up @@ -887,6 +897,7 @@ open class RNMBXMapView: UIView, RCTInvalidating {
if let simultaneousRotateAndPinchZoomEnabled = settings.simultaneousRotateAndPinchZoomEnabled as? Bool {
options.simultaneousRotateAndPinchZoomEnabled = simultaneousRotateAndPinchZoomEnabled
}
options.focalPoint = settings.focalPoint
/* android only
if let zoomAnimationAmount = value["zoomAnimationAmount"] as? NSNumber {
options.zoomAnimationAmount = zoomAnimationAmount.CGFloat
Expand Down
2 changes: 1 addition & 1 deletion scripts/autogenHelpers/DocJSONBuilder.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class DocJSONBuilder {
function mapNestedProp(propMeta) {
const result = {
type: {
name: propMeta.name,
name: tsTypeIsObject(propMeta) ? tsTypeDump(propMeta) : propMeta.name,
value: propMeta.value,
},
description: propMeta.description,
Expand Down
9 changes: 9 additions & 0 deletions src/components/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export type GestureSettings = {
* Whether single tapping the map with two touches results in a zoom-out animation.
*/
doubleTouchToZoomOutEnabled?: boolean;
/**
* By default, gestures rotate and zoom around the center of the gesture. Set
* this property to rotate and zoom around a fixed point instead, given in
* points (iOS) / dp (Android) relative to the top-left of the map view.
* Applies to pinch, rotate, double-tap and two-finger tap zoom.
* On iOS, pinch ignores the focal point while `pinchPanEnabled` is `true` (the default),
* so set `pinchPanEnabled: false` to keep pinch anchored to it.
*/
focalPoint?: { x: number; y: number };
/**
* Whether pan/scroll is enabled for the pinch gesture.
*/
Expand Down
Loading