From d741569abbb6c9b6219de959bc82ab44c3a9ac7b Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 00:28:39 +0700 Subject: [PATCH 01/15] ci: disable dev release commits --- release.config.cjs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/release.config.cjs b/release.config.cjs index bfb8b1b..56f59d8 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -69,10 +69,15 @@ module.exports = { ], [ '@semantic-release/git', - { - assets: ['package.json', 'CHANGELOG.md', 'example/package.json'], - message: - '🔖 release: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + (context) => { + if (context.branch.name === 'dev') { + return false; + } + return { + assets: ['package.json', 'CHANGELOG.md', 'example/package.json'], + message: + '🔖 release: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }; }, ], ], From 9ec12afb9cc897152d1c40b4c48df16b466ae1e3 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 00:34:58 +0700 Subject: [PATCH 02/15] ci: disable dev release commits --- release.config.cjs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/release.config.cjs b/release.config.cjs index 56f59d8..e151830 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -7,6 +7,20 @@ const rules = [ { type: 'chore', release: false, title: '🛠️ Other changes' }, ]; +const isDev = + process.env.BRANCH_NAME === 'dev' || process.env.GITHUB_REF_NAME === 'dev'; + +const gitPlugin = isDev + ? false // auf dev kein git plugin + : [ + '@semantic-release/git', + { + assets: ['package.json', 'CHANGELOG.md', 'example/package.json'], + message: + '🔖 release: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }, + ]; + const sortMap = Object.fromEntries( rules.map((rule, index) => [rule.title, index]) ); @@ -67,18 +81,6 @@ module.exports = { ], }, ], - [ - '@semantic-release/git', - (context) => { - if (context.branch.name === 'dev') { - return false; - } - return { - assets: ['package.json', 'CHANGELOG.md', 'example/package.json'], - message: - '🔖 release: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', - }; - }, - ], + [...(gitPlugin ? [gitPlugin] : [])], ], }; From 29665344e6b52884b94578e341f1bfe1b1c810b6 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 00:38:44 +0700 Subject: [PATCH 03/15] ci: disable dev release commits --- release.config.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release.config.cjs b/release.config.cjs index e151830..5333499 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -11,7 +11,7 @@ const isDev = process.env.BRANCH_NAME === 'dev' || process.env.GITHUB_REF_NAME === 'dev'; const gitPlugin = isDev - ? false // auf dev kein git plugin + ? false : [ '@semantic-release/git', { @@ -81,6 +81,6 @@ module.exports = { ], }, ], - [...(gitPlugin ? [gitPlugin] : [])], + ...(gitPlugin ? [gitPlugin] : []), ], }; From f2b897dbe2ac1b94262519cebe58f0b4e13a88b1 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 00:41:01 +0700 Subject: [PATCH 04/15] chore: update CHANGELOG.md --- CHANGELOG.md | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7bada3..4e6c8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,24 +17,6 @@ * fix CHANGELOG.md ([2f2bb2c](https://github.com/pinpong/react-native-google-maps-plus/commit/2f2bb2c617260166551abbc07dfa9a8ae27cf31e)) * merge dev into main ([a510e2a](https://github.com/pinpong/react-native-google-maps-plus/commit/a510e2a1bebabde03e319256b2a1246f10ce1b95)) -## [1.2.0-dev.1](https://github.com/pinpong/react-native-google-maps-plus/compare/v1.1.0...v1.2.0-dev.1) (2025-10-09) - -### ✨ Features - -* add kml layer support ([4faf558](https://github.com/pinpong/react-native-google-maps-plus/commit/4faf558425831cc18a6e9c9e2d20ef0c4f42e702)) -* add kml layer support ([35098bd](https://github.com/pinpong/react-native-google-maps-plus/commit/35098bd4c75b825f96f58696cbb37a4fcdebbdb8)) - -### 🐛 Bug Fixes - -* **example:** build issues ([cee0708](https://github.com/pinpong/react-native-google-maps-plus/commit/cee0708dfdee185ee4c8bb2836abd2a3c022fc93)) - -### 🛠️ Other changes - -* **ci:** move PR template to root for auto-apply ([03e8a84](https://github.com/pinpong/react-native-google-maps-plus/commit/03e8a8438b0d5edab80fcdf2f2c8abf3372288c2)) -* **example:** beautify example app UI ([4f390ec](https://github.com/pinpong/react-native-google-maps-plus/commit/4f390ecd9ebc2f3e559913882ac56d33a30ac45b)) -* **example:** beautify example app UI ([73c997c](https://github.com/pinpong/react-native-google-maps-plus/commit/73c997c69f23deeb48eb9b2be5df76a36ff0afea)) -* fix CHANGELOG.md ([2f2bb2c](https://github.com/pinpong/react-native-google-maps-plus/commit/2f2bb2c617260166551abbc07dfa9a8ae27cf31e)) - ## [1.1.0](https://github.com/pinpong/react-native-google-maps-plus/compare/v1.0.2...v1.1.0) (2025-10-08) ### ✨ Features @@ -78,18 +60,6 @@ * update to react-native 0.82.0 ([31d5ff5](https://github.com/pinpong/react-native-google-maps-plus/commit/31d5ff5157ec8357b9d699d4dcc09bda09e11afb)) * update to react-native 0.82.0 ([8c8e8ae](https://github.com/pinpong/react-native-google-maps-plus/commit/8c8e8ae1c4fcf97e04059d873461f083e4c346cf)) -## [1.1.0-dev.5](https://github.com/pinpong/react-native-google-maps-plus/compare/v1.1.0-dev.4...v1.1.0-dev.5) (2025-10-08) - -### 🐛 Bug Fixes - -* **example:** build issues ([cee0708](https://github.com/pinpong/react-native-google-maps-plus/commit/cee0708dfdee185ee4c8bb2836abd2a3c022fc93)) - -### 🛠️ Other changes - -* **example:** beautify example app UI ([4f390ec](https://github.com/pinpong/react-native-google-maps-plus/commit/4f390ecd9ebc2f3e559913882ac56d33a30ac45b)) -* **example:** beautify example app UI ([73c997c](https://github.com/pinpong/react-native-google-maps-plus/commit/73c997c69f23deeb48eb9b2be5df76a36ff0afea)) -* fix CHANGELOG.md ([2f2bb2c](https://github.com/pinpong/react-native-google-maps-plus/commit/2f2bb2c617260166551abbc07dfa9a8ae27cf31e)) - ## [1.1.0](https://github.com/pinpong/react-native-google-maps-plus/compare/v1.0.2...v1.1.0) (2025-10-08) ### ✨ Features From 0320a78967f21ec05835b5d3b7a18ac0c59d649d Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 01:13:55 +0700 Subject: [PATCH 05/15] chore(example): update example --- example/ios/Podfile.lock | 6 +-- example/src/App.tsx | 10 ++++- example/src/components/ControlPanel.tsx | 9 +++- example/src/screens/BlankScreen.tsx | 57 +++++++++++++++++++++++++ example/src/types/navigation.ts | 18 ++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 example/src/screens/BlankScreen.tsx create mode 100644 example/src/types/navigation.ts diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 413288b..5935e1a 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2474,7 +2474,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNGoogleMapsPlus (1.1.0-dev.5): + - RNGoogleMapsPlus (1.2.0): - boost - DoubleConversion - fast_float @@ -3086,7 +3086,7 @@ SPEC CHECKSUMS: ReactCodegen: 3873d7ac09960375f7845384ff47d53e478462dc ReactCommon: f5527f5d97a9957ab46eb5db78875d3579e03b97 RNGestureHandler: e1cc4de7646eb557ad62d1271d8eac73c304a896 - RNGoogleMapsPlus: cdea400ea1e69740d91e07dbb5882d93be4c0a77 + RNGoogleMapsPlus: 43e90cbedb2f3deec67a3a0a14fb5065f608ea3c RNReanimated: 8f0185df21f0dea34ee8c9611ba88c17a290ed9a RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf @@ -3094,6 +3094,6 @@ SPEC CHECKSUMS: SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea Yoga: ce55ebb197c21e22b6700cd36e3f36b7ec26e6f8 -PODFILE CHECKSUM: 246331f3f9b61838ac0bd43aa0f04db450c4bd52 +PODFILE CHECKSUM: 18d25340bc263a2eab86e2d8e5cfd9ad55ef6458 COCOAPODS: 1.15.2 diff --git a/example/src/App.tsx b/example/src/App.tsx index f44ddd3..6add98f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -19,8 +19,10 @@ import CustomStyleScreen from './screens/CustomStyleScreen'; import StressTestScreen from './screens/StressTestScreen'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useColorScheme } from 'react-native'; +import BlankScreen from './screens/BlankScreen'; +import type { RootStackParamList } from './types/navigation'; -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); export default function App() { const scheme = useColorScheme(); @@ -42,7 +44,11 @@ export default function App() { component={HomeScreen} options={{ title: 'Google Maps Examples' }} /> - + void }; @@ -25,6 +27,7 @@ type Props = { export default function ControlPanel({ mapRef, buttons }: Props) { const theme = useAppTheme(); + const navigation = useNavigation(); const progress = useSharedValue(0); const toggle = () => { @@ -36,6 +39,10 @@ export default function ControlPanel({ mapRef, buttons }: Props) { const finalButtons = useMemo( () => [ ...buttons, + { + title: `Navigate to blank screen`, + onPress: () => navigation.navigate('Blank'), + }, { title: 'Request location permission', onPress: async () => { @@ -57,7 +64,7 @@ export default function ControlPanel({ mapRef, buttons }: Props) { console.log(mapRef.current?.isGooglePlayServicesAvailable()), }, ], - [buttons, mapRef] + [buttons, mapRef, navigation] ); const buttonHeight = 52; diff --git a/example/src/screens/BlankScreen.tsx b/example/src/screens/BlankScreen.tsx new file mode 100644 index 0000000..5eeff57 --- /dev/null +++ b/example/src/screens/BlankScreen.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation, useTheme } from '@react-navigation/native'; +import type { RootNavigationProp } from '../types/navigation'; + +export default function BlankScreen() { + const navigation = useNavigation(); + const { colors } = useTheme(); + + return ( + + Blank Screen + + + This is an empty placeholder screen. + + + navigation.goBack()} + > + + ← Go Back + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + title: { + fontSize: 24, + fontWeight: '700', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + opacity: 0.8, + marginBottom: 32, + }, + button: { + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 10, + }, + buttonText: { + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts new file mode 100644 index 0000000..052a36a --- /dev/null +++ b/example/src/types/navigation.ts @@ -0,0 +1,18 @@ +export type RootStackParamList = { + Home: undefined; + Blank: undefined; + BasicMap: undefined; + Markers: undefined; + Polygons: undefined; + Polylines: undefined; + Circles: undefined; + Heatmap: undefined; + KmlLayer: undefined; + Location: undefined; + CustomStyle: undefined; + StressTest: undefined; +}; + +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +export type RootNavigationProp = NativeStackNavigationProp; From 6b177a49c853197afa28302de3f047c46f778afc Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 09:26:42 +0700 Subject: [PATCH 06/15] feat: add more supported marker props --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 1 + .../com/rngooglemapsplus/MapMarkerBuilder.kt | 14 +++++++++++-- example/src/utils/mapGenerators.ts | 21 ++++++++++++------- ios/GoogleMapViewImpl.swift | 1 + ios/MapMarkerBuilder.swift | 12 +++++++++-- src/types.ts | 6 ++++++ 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 5a0486d..c8d479c 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -954,6 +954,7 @@ class GoogleMapsViewImpl( } override fun onMarkerClick(marker: Marker): Boolean { + marker.showInfoWindow() onMarkerPress?.invoke(marker.tag?.toString() ?: "unknown") return true } diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index abd0d86..74278de 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -41,8 +41,13 @@ class MapMarkerBuilder( ): MarkerOptions = MarkerOptions().apply { position(LatLng(m.coordinate.latitude, m.coordinate.longitude)) - anchor((m.anchor?.x ?: 0.5).toFloat(), (m.anchor?.y ?: 0.5).toFloat()) icon(icon) + m.title?.let { title(it) } + m.snippet?.let { snippet(it) } + m.opacity?.let { alpha(it.toFloat()) } + m.flat?.let { flat(it) } + m.draggable?.let { draggable(it) } + anchor((m.anchor?.x ?: 0.5).toFloat(), (m.anchor?.y ?: 0.5).toFloat()) m.zIndex?.let { zIndex(it.toFloat()) } } @@ -56,17 +61,22 @@ class MapMarkerBuilder( next.coordinate.latitude, next.coordinate.longitude, ) - marker.zIndex = next.zIndex?.toFloat() ?: 0f if (!prev.markerStyleEquals(next)) { buildIconAsync(marker.id, next) { icon -> marker.setIcon(icon) } } + marker.title = next.title + marker.snippet = next.snippet + marker.alpha = next.opacity?.toFloat() ?: 0f + marker.isFlat = next.flat ?: false + marker.isDraggable = next.draggable ?: false marker.setAnchor( (next.anchor?.x ?: 0.5).toFloat(), (next.anchor?.y ?: 0.5).toFloat(), ) + marker.zIndex = next.zIndex?.toFloat() ?: 0f } fun buildIconAsync( diff --git a/example/src/utils/mapGenerators.ts b/example/src/utils/mapGenerators.ts index f83e01b..03e5172 100644 --- a/example/src/utils/mapGenerators.ts +++ b/example/src/utils/mapGenerators.ts @@ -106,17 +106,22 @@ export const makeHeatmap = (id: number): RNHeatmap => ({ opacity: 1, }); -export const makeMarker = (id: number): RNMarker => ({ - id: id.toString(), - zIndex: id, - coordinate: randomCoordinates(37.7749, -122.4194, 0.2), - anchor: { x: 0.5, y: 1.0 }, - iconSvg: - id % 2 === 0 +export function makeMarker(id: number): RNMarker { + const customIcon = id % 2 === 0; + return { + id: id.toString(), + zIndex: id, + coordinate: randomCoordinates(37.7749, -122.4194, 0.2), + anchor: customIcon ? { x: 0.5, y: 1.0 } : undefined, + title: `Marker title ${id}`, + snippet: `Marker snippet ${id}`, + draggable: customIcon, + iconSvg: customIcon ? { width: (64 / 100) * 50, height: (88 / 100) * 50, svgString: makeSvgIcon(64, 88), } : undefined, -}); + }; +} diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index a800ebe..46401e8 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -722,6 +722,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { } func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { + mapView.selectedMarker = marker let id = (marker.userData as? String) ?? "unknown" onMarkerPress?(id) return true diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index 98e2fe6..8e803bf 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -21,6 +21,11 @@ final class MapMarkerBuilder { marker.userData = m.id marker.tracksViewChanges = true marker.icon = icon + m.title.map { marker.title = $0 } + m.snippet.map { marker.snippet = $0 } + m.opacity.map { marker.iconView?.alpha = CGFloat($0) } + m.flat.map { marker.isFlat = $0 } + m.draggable.map { marker.isDraggable = $0 } marker.groundAnchor = CGPoint( x: m.anchor?.x ?? 0.5, y: m.anchor?.y ?? 0.5 @@ -42,13 +47,16 @@ final class MapMarkerBuilder { longitude: next.coordinate.longitude ) + m.title = next.title + m.snippet = next.snippet + m.iconView?.alpha = CGFloat(next.opacity ?? 0) + m.isFlat = next.flat ?? false + m.isDraggable = next.draggable ?? false m.zIndex = Int32(next.zIndex ?? 0) - m.groundAnchor = CGPoint( x: next.anchor?.x ?? 0.5, y: next.anchor?.y ?? 0.5 ) - if !prev.markerStyleEquals(next) { buildIconAsync(next.id, next) { img in m.tracksViewChanges = true diff --git a/src/types.ts b/src/types.ts index fedb7b8..7fdf3ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,6 +136,12 @@ export type RNMarker = { zIndex?: number; coordinate: RNLatLng; anchor?: RNPosition; + showInfoWindow?: boolean; + title?: string; + snippet?: string; + opacity?: number; + flat?: boolean; + draggable?: boolean; iconSvg?: RNMarkerSvg; }; From 51044c8c1e53434a4513d1907965d3bc9a5359ee Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 10:17:06 +0700 Subject: [PATCH 07/15] feat: add marker drag support --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 43 ++++- .../com/rngooglemapsplus/MapMarkerBuilder.kt | 4 +- .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 23 ++- example/src/components/MapWrapper.tsx | 157 +++++++++--------- example/src/utils/mapGenerators.ts | 4 +- ios/GoogleMapViewImpl.swift | 44 +++-- ios/MapMarkerBuilder.swift | 13 +- ios/RNGoogleMapsPlusView.swift | 17 +- src/RNGoogleMapsPlusView.nitro.ts | 11 +- 9 files changed, 194 insertions(+), 122 deletions(-) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index c8d479c..29e51e0 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -47,6 +47,7 @@ class GoogleMapsViewImpl( GoogleMap.OnPolylineClickListener, GoogleMap.OnPolygonClickListener, GoogleMap.OnCircleClickListener, + GoogleMap.OnMarkerDragListener, LifecycleEventListener { private var initialized = false private var mapReady = false @@ -136,6 +137,7 @@ class GoogleMapsViewImpl( googleMap?.setOnPolygonClickListener(this@GoogleMapsViewImpl) googleMap?.setOnCircleClickListener(this@GoogleMapsViewImpl) googleMap?.setOnMapClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMarkerDragListener(this@GoogleMapsViewImpl) } initLocationCallbacks() applyPending() @@ -481,10 +483,13 @@ class GoogleMapsViewImpl( var onLocationUpdate: ((RNLocation) -> Unit)? = null var onLocationError: ((RNLocationErrorCode) -> Unit)? = null var onMapPress: ((RNLatLng) -> Unit)? = null - var onMarkerPress: ((String) -> Unit)? = null - var onPolylinePress: ((String) -> Unit)? = null - var onPolygonPress: ((String) -> Unit)? = null - var onCirclePress: ((String) -> Unit)? = null + var onMarkerPress: ((String?) -> Unit)? = null + var onPolylinePress: ((String?) -> Unit)? = null + var onPolygonPress: ((String?) -> Unit)? = null + var onCirclePress: ((String?) -> Unit)? = null + var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null + var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null + var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null @@ -900,6 +905,7 @@ class GoogleMapsViewImpl( setOnPolygonClickListener(null) setOnCircleClickListener(null) setOnMapClickListener(null) + setOnMarkerDragListener(null) } googleMap = null mapView?.apply { @@ -955,20 +961,20 @@ class GoogleMapsViewImpl( override fun onMarkerClick(marker: Marker): Boolean { marker.showInfoWindow() - onMarkerPress?.invoke(marker.tag?.toString() ?: "unknown") + onMarkerPress?.invoke(marker.tag?.toString()) return true } override fun onPolylineClick(polyline: Polyline) { - onPolylinePress?.invoke(polyline.tag?.toString() ?: "unknown") + onPolylinePress?.invoke(polyline.tag?.toString()) } override fun onPolygonClick(polygon: Polygon) { - onPolygonPress?.invoke(polygon.tag?.toString() ?: "unknown") + onPolygonPress?.invoke(polygon.tag?.toString()) } override fun onCircleClick(circle: Circle) { - onCirclePress?.invoke(circle.tag?.toString() ?: "unknown") + onCirclePress?.invoke(circle.tag?.toString()) } override fun onMapClick(coordinates: LatLng) { @@ -976,6 +982,27 @@ class GoogleMapsViewImpl( RNLatLng(coordinates.latitude, coordinates.longitude), ) } + + override fun onMarkerDragStart(marker: Marker) { + onMarkerDragStart?.invoke( + marker.tag?.toString(), + RNLatLng(marker.position.latitude, marker.position.longitude), + ) + } + + override fun onMarkerDrag(marker: Marker) { + onMarkerDrag?.invoke( + marker.tag?.toString(), + RNLatLng(marker.position.latitude, marker.position.longitude), + ) + } + + override fun onMarkerDragEnd(marker: Marker) { + onMarkerDragEnd?.invoke( + marker.tag?.toString(), + RNLatLng(marker.position.latitude, marker.position.longitude), + ) + } } private inline fun onUi(crossinline block: () -> Unit) { diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index 74278de..1e2fe56 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -47,7 +47,7 @@ class MapMarkerBuilder( m.opacity?.let { alpha(it.toFloat()) } m.flat?.let { flat(it) } m.draggable?.let { draggable(it) } - anchor((m.anchor?.x ?: 0.5).toFloat(), (m.anchor?.y ?: 0.5).toFloat()) + m.anchor?.let { anchor((m.anchor.x).toFloat(), (m.anchor.y).toFloat()) } m.zIndex?.let { zIndex(it.toFloat()) } } @@ -74,7 +74,7 @@ class MapMarkerBuilder( marker.isDraggable = next.draggable ?: false marker.setAnchor( (next.anchor?.x ?: 0.5).toFloat(), - (next.anchor?.y ?: 0.5).toFloat(), + (next.anchor?.y ?: 1.0).toFloat(), ) marker.zIndex = next.zIndex?.toFloat() ?: 0f } diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index ede216d..3173f46 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -280,26 +280,41 @@ class RNGoogleMapsPlusView( view.onMapPress = cb } - override var onMarkerPress: ((String) -> Unit)? = null + override var onMarkerPress: ((String?) -> Unit)? = null set(cb) { view.onMarkerPress = cb } - override var onPolylinePress: ((String) -> Unit)? = null + override var onPolylinePress: ((String?) -> Unit)? = null set(cb) { view.onPolylinePress = cb } - override var onPolygonPress: ((String) -> Unit)? = null + override var onPolygonPress: ((String?) -> Unit)? = null set(cb) { view.onPolygonPress = cb } - override var onCirclePress: ((String) -> Unit)? = null + override var onCirclePress: ((String?) -> Unit)? = null set(cb) { view.onCirclePress = cb } + override var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null + set(cb) { + view.onMarkerDragStart = cb + } + + override var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null + set(cb) { + view.onMarkerDrag = cb + } + + override var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null + set(cb) { + view.onMarkerDragEnd = cb + } + override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb diff --git a/example/src/components/MapWrapper.tsx b/example/src/components/MapWrapper.tsx index d6075fd..984ccc4 100644 --- a/example/src/components/MapWrapper.tsx +++ b/example/src/components/MapWrapper.tsx @@ -15,6 +15,7 @@ import { } from 'react-native-google-maps-plus'; import type { ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { callback } from 'react-native-nitro-modules'; type Props = ViewProps & RNGoogleMapsPlusViewProps & { @@ -94,86 +95,82 @@ export default function MapWrapper(props: Props) { mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} mapPadding={props.mapPadding ?? mapPadding} locationConfig={props.locationConfig ?? locationConfig} - onMapReady={ - props.onMapReady - ? { - f: (ready: boolean) => console.log('Map is ready! ' + ready), - } - : undefined - } - onMapPress={ - props.onMapPress - ? { - f: (c: RNLatLng) => console.log('Map press:', c), - } - : undefined - } - onMarkerPress={ - props.onMarkerPress - ? { - f: (id: string) => console.log('Marker press:', id), - } - : undefined - } - onPolylinePress={ - props.onPolylinePress - ? { - f: (id: string) => console.log('Polyline press:', id), - } - : undefined - } - onPolygonPress={ - props.onPolygonPress - ? { - f: (id: string) => console.log('Polygon press:', id), - } - : undefined - } - onCirclePress={ - props.onCirclePress - ? { - f: (id: string) => console.log('Circle press:', id), - } - : undefined - } - onCameraChangeStart={ - props.onCameraChangeStart - ? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam start', r, cam, g), - } - : undefined - } - onCameraChange={ - props.onCameraChange - ? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam', r, cam, g), - } - : undefined - } - onCameraChangeComplete={ - props.onCameraChangeComplete - ? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam complete', r, cam, g), - } - : undefined - } - onLocationUpdate={ - props.onLocationUpdate - ? { - f: (l: RNLocation) => console.log('Location', l), - } - : undefined - } - onLocationError={ - props.onLocationError - ? { - f: (e: any) => console.log('Location error', e), - } - : undefined - } + onMapReady={callback( + props.onMapReady ?? { + f: (ready: boolean) => console.log('Map is ready! ' + ready), + } + )} + onMapPress={callback( + props.onMapPress ?? { + f: (c: RNLatLng) => console.log('Map press:', c), + } + )} + onMarkerPress={callback( + props.onMarkerPress ?? { + f: (id: string | undefined) => console.log('Marker press:', id), + } + )} + onPolylinePress={callback( + props.onPolylinePress ?? { + f: (id: string | undefined) => console.log('Polyline press:', id), + } + )} + onPolygonPress={callback( + props.onPolygonPress ?? { + f: (id: string | undefined) => console.log('Polygon press:', id), + } + )} + onCirclePress={callback( + props.onCirclePress ?? { + f: (id: string | undefined) => console.log('Circle press:', id), + } + )} + onMarkerDragStart={callback( + props.onMarkerDragStart ?? { + f: (id: string | undefined, latLng: RNLatLng) => + console.log('Marker drag start', id, latLng), + } + )} + onMarkerDrag={callback( + props.onMarkerDrag ?? { + f: (id: string | undefined, latLng: RNLatLng) => + console.log('Marker drag', id, latLng), + } + )} + onMarkerDragEnd={callback( + props.onMarkerDragEnd ?? { + f: (id: string | undefined, latLng: RNLatLng) => + console.log('Marker drag end', id, latLng), + } + )} + onCameraChangeStart={callback( + props.onCameraChangeStart ?? { + f: (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Cam start', r, cam, g), + } + )} + onCameraChange={callback( + props.onCameraChange ?? { + f: (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Cam', r, cam, g), + } + )} + onCameraChangeComplete={callback( + props.onCameraChangeComplete ?? { + f: (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Cam complete', r, cam, g), + } + )} + onLocationUpdate={callback( + props.onLocationUpdate ?? { + f: (l: RNLocation) => console.log('Location', l), + } + )} + onLocationError={callback( + props.onLocationError ?? { + f: (e: any) => console.log('Location error', e), + } + )} /> {children} diff --git a/example/src/utils/mapGenerators.ts b/example/src/utils/mapGenerators.ts index 03e5172..af0720d 100644 --- a/example/src/utils/mapGenerators.ts +++ b/example/src/utils/mapGenerators.ts @@ -113,8 +113,8 @@ export function makeMarker(id: number): RNMarker { zIndex: id, coordinate: randomCoordinates(37.7749, -122.4194, 0.2), anchor: customIcon ? { x: 0.5, y: 1.0 } : undefined, - title: `Marker title ${id}`, - snippet: `Marker snippet ${id}`, + title: `Marker title id: ${id}`, + snippet: `Marker snippet id: ${id}`, draggable: customIcon, iconSvg: customIcon ? { diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 46401e8..3080a84 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -297,10 +297,13 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var onLocationUpdate: ((RNLocation) -> Void)? var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? var onMapPress: ((RNLatLng) -> Void)? - var onMarkerPress: ((String) -> Void)? - var onPolylinePress: ((String) -> Void)? - var onPolygonPress: ((String) -> Void)? - var onCirclePress: ((String) -> Void)? + var onMarkerPress: ((String?) -> Void)? + var onPolylinePress: ((String?) -> Void)? + var onPolygonPress: ((String?) -> Void)? + var onCirclePress: ((String?) -> Void)? + var onMarkerDragStart: ((String?, RNLatLng) -> Void)? + var onMarkerDrag: ((String?, RNLatLng) -> Void)? + var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? @@ -723,27 +726,44 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { mapView.selectedMarker = marker - let id = (marker.userData as? String) ?? "unknown" - onMarkerPress?(id) + onMarkerPress?(marker.userData as? String, ) return true } func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { switch overlay { case let circle as GMSCircle: - let id = (circle.userData as? String) ?? "unknown" - onCirclePress?(id) + onCirclePress?(circle.userData as? String, ) case let polygon as GMSPolygon: - let id = (polygon.userData as? String) ?? "unknown" - onPolygonPress?(id) + onPolygonPress?(polygon.userData as? String, ) case let polyline as GMSPolyline: - let id = (polyline.userData as? String) ?? "unknown" - onPolylinePress?(id) + onPolylinePress?(polyline.userData as? String, ) default: break } } + + func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { + onMarkerDragStart?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } + + func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { + onMarkerDrag?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } + + func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { + onMarkerDragEnd?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index 8e803bf..d0e5803 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -26,11 +26,12 @@ final class MapMarkerBuilder { m.opacity.map { marker.iconView?.alpha = CGFloat($0) } m.flat.map { marker.isFlat = $0 } m.draggable.map { marker.isDraggable = $0 } - marker.groundAnchor = CGPoint( - x: m.anchor?.x ?? 0.5, - y: m.anchor?.y ?? 0.5 - ) - + m.anchor.map { + marker.groundAnchor = CGPoint( + x: $0.x, + y: $0.y + ) + } m.zIndex.map { marker.zIndex = Int32($0) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak marker] in @@ -55,7 +56,7 @@ final class MapMarkerBuilder { m.zIndex = Int32(next.zIndex ?? 0) m.groundAnchor = CGPoint( x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 0.5 + y: next.anchor?.y ?? 1 ) if !prev.markerStyleEquals(next) { buildIconAsync(next.id, next) { img in diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 49c498f..a39c764 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -298,18 +298,27 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { var onMapPress: ((RNLatLng) -> Void)? { didSet { impl.onMapPress = onMapPress } } - var onMarkerPress: ((String) -> Void)? { + var onMarkerPress: ((String?) -> Void)? { didSet { impl.onMarkerPress = onMarkerPress } } - var onPolylinePress: ((String) -> Void)? { + var onPolylinePress: ((String?) -> Void)? { didSet { impl.onPolylinePress = onPolylinePress } } - var onPolygonPress: ((String) -> Void)? { + var onPolygonPress: ((String?) -> Void)? { didSet { impl.onPolygonPress = onPolygonPress } } - var onCirclePress: ((String) -> Void)? { + var onCirclePress: ((String?) -> Void)? { didSet { impl.onCirclePress = onCirclePress } } + var onMarkerDragStart: ((String?, RNLatLng) -> Void)? { + didSet { impl.onMarkerDragStart = onMarkerDragStart } + } + var onMarkerDrag: ((String?, RNLatLng) -> Void)? { + didSet { impl.onMarkerDrag = onMarkerDrag } + } + var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { + didSet { impl.onMarkerDragEnd = onMarkerDragEnd } + } var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index 07f104f..f3b7dda 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -50,10 +50,13 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { onLocationUpdate?: (location: RNLocation) => void; onLocationError?: (error: RNLocationErrorCode) => void; onMapPress?: (coordinate: RNLatLng) => void; - onMarkerPress?: (id: string) => void; - onPolylinePress?: (id: string) => void; - onPolygonPress?: (id: string) => void; - onCirclePress?: (id: string) => void; + onMarkerPress?: (id?: string | undefined) => void; + onPolylinePress?: (id?: string | undefined) => void; + onPolygonPress?: (id?: string | undefined) => void; + onCirclePress?: (id?: string | undefined) => void; + onMarkerDragStart?: (id: string | undefined, location: RNLatLng) => void; + onMarkerDrag?: (id: string | undefined, location: RNLatLng) => void; + onMarkerDragEnd?: (id: string | undefined, location: RNLatLng) => void; onCameraChangeStart?: ( region: RNRegion, camera: RNCamera, From 64073d432d48a8898282c246385b7a5ee7843080 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 14:58:16 +0700 Subject: [PATCH 08/15] feat: add indoor building focus and level activation listener support --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 21 ++++++++++++ .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 10 ++++++ .../extensions/IndoorBuildingExtension.kt | 33 +++++++++++++++++++ example/src/App.tsx | 6 ++++ example/src/components/MapWrapper.tsx | 18 +++++++++- example/src/screens/HomeScreen.tsx | 1 + example/src/screens/IndoorLevelMapScreen.tsx | 26 +++++++++++++++ example/src/types/navigation.ts | 1 + ios/GoogleMapViewImpl.swift | 30 +++++++++++++++-- ios/RNGoogleMapsPlusView.swift | 6 ++++ ios/extensions/IndoorBuilding+Extension.swift | 33 +++++++++++++++++++ src/RNGoogleMapsPlusView.nitro.ts | 4 +++ src/types.ts | 14 ++++++++ 13 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt create mode 100644 example/src/screens/IndoorLevelMapScreen.tsx create mode 100644 ios/extensions/IndoorBuilding+Extension.swift diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 29e51e0..bdf02dd 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -15,6 +15,7 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.IndoorBuilding import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.MapColorScheme @@ -30,6 +31,8 @@ import com.google.android.gms.maps.model.TileOverlayOptions import com.google.maps.android.data.kml.KmlLayer import com.rngooglemapsplus.extensions.toGooglePriority import com.rngooglemapsplus.extensions.toLocationErrorCode +import com.rngooglemapsplus.extensions.toRNIndoorBuilding +import com.rngooglemapsplus.extensions.toRNIndoorLevel import java.io.ByteArrayInputStream import java.nio.charset.StandardCharsets @@ -48,6 +51,7 @@ class GoogleMapsViewImpl( GoogleMap.OnPolygonClickListener, GoogleMap.OnCircleClickListener, GoogleMap.OnMarkerDragListener, + GoogleMap.OnIndoorStateChangeListener, LifecycleEventListener { private var initialized = false private var mapReady = false @@ -490,6 +494,8 @@ class GoogleMapsViewImpl( var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Unit)? = null + var onIndoorLevelActivated: ((RNIndoorLevel) -> Unit)? = null var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null @@ -1003,6 +1009,21 @@ class GoogleMapsViewImpl( RNLatLng(marker.position.latitude, marker.position.longitude), ) } + + override fun onIndoorBuildingFocused() { + val building = googleMap?.focusedBuilding ?: return + onIndoorBuildingFocused?.invoke(building.toRNIndoorBuilding()) + } + + override fun onIndoorLevelActivated(indoorBuilding: IndoorBuilding) { + val activeLevel = indoorBuilding.levels.getOrNull(indoorBuilding.activeLevelIndex) ?: return + onIndoorLevelActivated?.invoke( + activeLevel.toRNIndoorLevel( + indoorBuilding.activeLevelIndex, + true, + ), + ) + } } private inline fun onUi(crossinline block: () -> Unit) { diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 3173f46..7bbf97b 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -315,6 +315,16 @@ class RNGoogleMapsPlusView( view.onMarkerDragEnd = cb } + override var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Unit)? = null + set(cb) { + view.onIndoorBuildingFocused = cb + } + + override var onIndoorLevelActivated: ((RNIndoorLevel) -> Unit)? = null + set(cb) { + view.onIndoorLevelActivated = cb + } + override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt new file mode 100644 index 0000000..eace541 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt @@ -0,0 +1,33 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.IndoorBuilding +import com.google.android.gms.maps.model.IndoorLevel +import com.rngooglemapsplus.RNIndoorBuilding +import com.rngooglemapsplus.RNIndoorLevel + +fun IndoorBuilding.toRNIndoorBuilding(): RNIndoorBuilding { + val mappedLevels = + levels + .mapIndexed { index, level -> + val active = index == activeLevelIndex + level.toRNIndoorLevel(index, active) + }.toTypedArray() + + return RNIndoorBuilding( + activeLevelIndex = activeLevelIndex.toDouble(), + defaultLevelIndex = defaultLevelIndex.toDouble(), + levels = mappedLevels, + underground = isUnderground, + ) +} + +fun IndoorLevel.toRNIndoorLevel( + index: Int, + active: Boolean, +): RNIndoorLevel = + RNIndoorLevel( + index = index.toDouble(), + name = name, + shortName = shortName, + active = active, + ) diff --git a/example/src/App.tsx b/example/src/App.tsx index 6add98f..fe469a5 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -20,6 +20,7 @@ import StressTestScreen from './screens/StressTestScreen'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useColorScheme } from 'react-native'; import BlankScreen from './screens/BlankScreen'; +import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen'; import type { RootStackParamList } from './types/navigation'; const Stack = createStackNavigator(); @@ -94,6 +95,11 @@ export default function App() { component={CustomStyleScreen} options={{ title: 'Custom Map Style' }} /> + + console.log('Indoor building focused', building), + } + )} + onIndoorLevelActivated={callback( + props.onIndoorLevelActivated ?? { + f: (level: RNIndoorLevel) => + console.log('Indoor level activated', level), + } + )} onCameraChangeStart={callback( props.onCameraChangeStart ?? { f: (r: RNRegion, cam: RNCamera, g: boolean) => diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index fb721b1..66333cc 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -14,6 +14,7 @@ const screens = [ { name: 'KmlLayer', title: 'KML Layer' }, { name: 'Location', title: 'Location & Permissions' }, { name: 'CustomStyle', title: 'Custom Map Style' }, + { name: 'IndoorLevelMap', title: 'Indoor level map' }, { name: 'StressTest', title: 'Stress Test' }, ]; diff --git a/example/src/screens/IndoorLevelMapScreen.tsx b/example/src/screens/IndoorLevelMapScreen.tsx new file mode 100644 index 0000000..6e09919 --- /dev/null +++ b/example/src/screens/IndoorLevelMapScreen.tsx @@ -0,0 +1,26 @@ +import React, { useRef } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import type { GoogleMapsViewRef } from 'react-native-google-maps-plus'; +import ControlPanel from '../components/ControlPanel'; + +export default function IndoorLevelMapScreen() { + const mapRef = useRef(null); + return ( + + + + ); +} diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 052a36a..32d67ee 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -10,6 +10,7 @@ export type RootStackParamList = { KmlLayer: undefined; Location: undefined; CustomStyle: undefined; + IndoorLevelMap: undefined; StressTest: undefined; }; diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 3080a84..500be6b 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -3,7 +3,8 @@ import GoogleMaps import GoogleMapsUtils import UIKit -final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { +final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate, +GMSIndoorDisplayDelegate { private let locationHandler: LocationHandler private let markerBuilder: MapMarkerBuilder @@ -136,7 +137,10 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { myLocationEnabled.map { mapView?.isMyLocationEnabled = $0 } buildingEnabled.map { mapView?.isBuildingsEnabled = $0 } trafficEnabled.map { mapView?.isTrafficEnabled = $0 } - indoorEnabled.map { mapView?.isIndoorEnabled = $0 } + indoorEnabled.map { + mapView?.isIndoorEnabled = $0 + mapView?.indoorDisplay.delegate = $0 == true ? self : nil + } customMapStyle.map { mapView?.mapStyle = $0 } mapType.map { mapView?.mapType = $0 } userInterfaceStyle.map { mapView?.overrideUserInterfaceStyle = $0 } @@ -236,6 +240,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var indoorEnabled: Bool? { didSet { mapView?.isIndoorEnabled = indoorEnabled ?? false + mapView?.indoorDisplay.delegate = indoorEnabled == true ? self : nil } } @@ -304,6 +309,8 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var onMarkerDragStart: ((String?, RNLatLng) -> Void)? var onMarkerDrag: ((String?, RNLatLng) -> Void)? var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? + var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? @@ -766,4 +773,23 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { RNLatLng(marker.position.latitude, marker.position.longitude) ) } + + func didChangeActiveBuilding(_ building: GMSIndoorBuilding?) { + guard let display = mapView?.indoorDisplay, let building else { return } + onIndoorBuildingFocused?(building.toRNIndoorBuilding(from: display)) + } + + func didChangeActiveLevel(_ level: GMSIndoorLevel?) { + guard + let display = mapView?.indoorDisplay, + let building = display.activeBuilding, + let level, + let index = building.levels.firstIndex(where: { + $0.name == level.name && $0.shortName == level.shortName + }) + else { return } + + onIndoorLevelActivated?(level.toRNIndoorLevel(index: index, active: true)) + } + } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index a39c764..1cb8c16 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -319,6 +319,12 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? { + didSet { impl.onIndoorBuildingFocused = onIndoorBuildingFocused } + } + var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? { + didSet { impl.onIndoorLevelActivated = onIndoorLevelActivated } + } var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } diff --git a/ios/extensions/IndoorBuilding+Extension.swift b/ios/extensions/IndoorBuilding+Extension.swift new file mode 100644 index 0000000..87620b5 --- /dev/null +++ b/ios/extensions/IndoorBuilding+Extension.swift @@ -0,0 +1,33 @@ +import GoogleMaps +import NitroModules + +extension GMSIndoorLevel { + func toRNIndoorLevel(index: Int, active: Bool) -> RNIndoorLevel { + RNIndoorLevel( + index: Double(index), + name: self.name, + shortName: self.shortName, + active: active + ) + } +} + +extension GMSIndoorBuilding { + func toRNIndoorBuilding(from indoorDisplay: GMSIndoorDisplay) + -> RNIndoorBuilding { + let activeLevel = indoorDisplay.activeLevel + let levels = self.levels.enumerated().map { + (index, level) -> RNIndoorLevel in + let isActive = (level == activeLevel) + return level.toRNIndoorLevel(index: index, active: isActive) + } + let activeIndex = self.levels.firstIndex(where: { $0 == activeLevel }) + + return RNIndoorBuilding( + activeLevelIndex: activeIndex.map { Double($0) } ?? nil, + defaultLevelIndex: nil, + levels: levels, + underground: self.isUnderground + ) + } +} diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index f3b7dda..fbc41e4 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -24,6 +24,8 @@ import type { RNMapZoomConfig, RNHeatmap, RNKMLayer, + RNIndoorBuilding, + RNIndoorLevel, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -57,6 +59,8 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { onMarkerDragStart?: (id: string | undefined, location: RNLatLng) => void; onMarkerDrag?: (id: string | undefined, location: RNLatLng) => void; onMarkerDragEnd?: (id: string | undefined, location: RNLatLng) => void; + onIndoorBuildingFocused?: (indoorBuilding: RNIndoorBuilding) => void; + onIndoorLevelActivated?: (indoorLevel: RNIndoorLevel) => void; onCameraChangeStart?: ( region: RNRegion, camera: RNCamera, diff --git a/src/types.ts b/src/types.ts index 7fdf3ec..ab856ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -210,6 +210,20 @@ export type RNKMLayer = { kmlString: string; }; +export type RNIndoorBuilding = { + activeLevelIndex?: number; + defaultLevelIndex?: number; + levels: RNIndoorLevel[]; + underground?: boolean; +}; + +export type RNIndoorLevel = { + index: number; + name?: string; + shortName?: string; + active?: boolean; +}; + export type RNLocationConfig = { android?: RNAndroidLocationConfig; ios?: RNIOSLocationConfig; From 7fe037ace3fc9f9413eeb69ab94595072e084c8c Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 21:13:29 +0700 Subject: [PATCH 09/15] feat: snapshot feature --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 91 +++- .../com/rngooglemapsplus/LocationHandler.kt | 1 - .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 42 +- .../extensions/RNLatLngBoundsExtension.kt | 17 + .../com/rngooglemapsplus/extensions/RNSize.kt | 7 + .../extensions/RNSnapshotFormat.kt | 16 + .../extensions/RNSnapshotResultType.kt | 9 + example/index.js | 3 + example/src/App.tsx | 14 +- example/src/components/ControlPanel.tsx | 132 +++--- example/src/components/MapWrapper.tsx | 20 +- example/src/screens/CameraTestScreen.tsx | 81 ++++ example/src/screens/HomeScreen.tsx | 86 ++-- example/src/screens/MarkersScreen.tsx | 2 +- example/src/screens/SnaptshotTestScreen.tsx | 152 +++++++ example/src/screens/StressTestScreen.tsx | 2 +- example/src/types/navigation.ts | 2 + ios/GoogleMapViewImpl.swift | 392 ++++++++++++------ ios/RNGoogleMapsPlusView.swift | 99 +++-- ios/extensions/RNLatLngBounds+Extension.swift | 16 + ios/extensions/RNSize+Extension.swift | 7 + .../RNSnapshotFormat+Extension.swift | 28 ++ .../RNSnapshotResultType+Extension.swift | 12 + src/RNGoogleMapsPlusView.nitro.ts | 17 +- src/types.ts | 33 +- 25 files changed, 993 insertions(+), 288 deletions(-) create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt create mode 100644 example/src/screens/CameraTestScreen.tsx create mode 100644 example/src/screens/SnaptshotTestScreen.tsx create mode 100644 ios/extensions/RNLatLngBounds+Extension.swift create mode 100644 ios/extensions/RNSize+Extension.swift create mode 100644 ios/extensions/RNSnapshotFormat+Extension.swift create mode 100644 ios/extensions/RNSnapshotResultType+Extension.swift diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index bdf02dd..46ab2ec 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -1,8 +1,12 @@ package com.rngooglemapsplus import android.annotation.SuppressLint +import android.graphics.Bitmap import android.location.Location +import android.util.Base64 +import android.util.Size import android.widget.FrameLayout +import androidx.core.graphics.scale import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.PixelUtil.dpToPx @@ -29,11 +33,15 @@ import com.google.android.gms.maps.model.PolylineOptions import com.google.android.gms.maps.model.TileOverlay import com.google.android.gms.maps.model.TileOverlayOptions import com.google.maps.android.data.kml.KmlLayer +import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.toGooglePriority import com.rngooglemapsplus.extensions.toLocationErrorCode import com.rngooglemapsplus.extensions.toRNIndoorBuilding import com.rngooglemapsplus.extensions.toRNIndoorLevel import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream import java.nio.charset.StandardCharsets class GoogleMapsViewImpl( @@ -188,6 +196,8 @@ class GoogleMapsViewImpl( if (cameraPosition == lastSubmittedCameraPosition) { return } + lastSubmittedCameraPosition = cameraPosition + val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason val latDelta = bounds.northeast.latitude - bounds.southwest.latitude @@ -207,7 +217,6 @@ class GoogleMapsViewImpl( ), isGesture, ) - lastSubmittedCameraPosition = cameraPosition } override fun onCameraIdle() { @@ -503,7 +512,7 @@ class GoogleMapsViewImpl( fun setCamera( cameraPosition: CameraPosition, animated: Boolean, - durationMS: Int, + durationMs: Int, ) { onUi { val current = googleMap?.cameraPosition @@ -514,7 +523,7 @@ class GoogleMapsViewImpl( val update = CameraUpdateFactory.newCameraPosition(cameraPosition) if (animated) { - googleMap?.animateCamera(update, durationMS, null) + googleMap?.animateCamera(update, durationMs, null) } else { googleMap?.moveCamera(update) } @@ -525,7 +534,7 @@ class GoogleMapsViewImpl( coordinates: Array, padding: RNMapPadding, animated: Boolean, - durationMS: Int, + durationMs: Int, ) { if (coordinates.isEmpty()) { return @@ -583,13 +592,85 @@ class GoogleMapsViewImpl( 0, ) if (animated) { - googleMap?.animateCamera(update, durationMS, null) + googleMap?.animateCamera(update, durationMs, null) } else { googleMap?.moveCamera(update) } } } + fun setCameraBounds(bounds: LatLngBounds?) { + onUi { + googleMap?.setLatLngBoundsForCameraTarget(bounds) + } + } + + fun animateToBounds( + bounds: LatLngBounds, + padding: Int, + durationMs: Int, + lockBounds: Boolean, + ) { + onUi { + if (lockBounds) { + googleMap?.setLatLngBoundsForCameraTarget(bounds) + } + val update = + CameraUpdateFactory.newLatLngBounds( + bounds, + padding, + ) + googleMap?.animateCamera(update, durationMs, null) + } + } + + fun snapshot( + size: Size?, + format: String, + compressFormat: Bitmap.CompressFormat, + quality: Double, + resultIsFile: Boolean, + ): Promise { + val promise = Promise() + onUi { + googleMap?.snapshot { bitmap -> + try { + if (bitmap == null) { + promise.resolve(null) + return@snapshot + } + + val scaledBitmap = + size?.let { + bitmap.scale(it.width, it.height) + } ?: bitmap + + val output = ByteArrayOutputStream() + scaledBitmap.compress(compressFormat, (quality * 100).toInt().coerceIn(0, 100), output) + val bytes = output.toByteArray() + + if (resultIsFile) { + val file = File(context.cacheDir, "map_snapshot_${System.currentTimeMillis()}.$format") + FileOutputStream(file).use { it.write(bytes) } + promise.resolve(file.absolutePath) + } else { + val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP) + promise.resolve("data:image/$format;base64,$base64") + } + + if (scaledBitmap != bitmap) { + scaledBitmap.recycle() + } + bitmap.recycle() + } catch (e: Exception) { + promise.resolve(null) + } + } + } + + return promise + } + fun addMarker( id: String, opts: MarkerOptions, diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index 42313db..c03c8b5 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -131,7 +131,6 @@ class LocationHandler( private fun restartLocationUpdates() { stop() - // 4) Google Play Services checken – früh zurückmelden val playServicesStatus = GoogleApiAvailability .getInstance() diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 7bbf97b..845421f 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -6,11 +6,16 @@ import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.model.MapStyleOptions import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.circleEquals +import com.rngooglemapsplus.extensions.isFileResult import com.rngooglemapsplus.extensions.markerEquals import com.rngooglemapsplus.extensions.polygonEquals import com.rngooglemapsplus.extensions.polylineEquals import com.rngooglemapsplus.extensions.toCameraPosition +import com.rngooglemapsplus.extensions.toCompressFormat +import com.rngooglemapsplus.extensions.toFileExtension +import com.rngooglemapsplus.extensions.toLatLngBounds import com.rngooglemapsplus.extensions.toMapColorScheme +import com.rngooglemapsplus.extensions.toSize @DoNotStrip class RNGoogleMapsPlusView( @@ -343,25 +348,54 @@ class RNGoogleMapsPlusView( override fun setCamera( camera: RNCamera, animated: Boolean?, - durationMS: Double?, + durationMs: Double?, ) { - view.setCamera(camera.toCameraPosition(), animated == true, durationMS?.toInt() ?: 3000) + view.setCamera(camera.toCameraPosition(), animated == true, durationMs?.toInt() ?: 3000) } override fun setCameraToCoordinates( coordinates: Array, padding: RNMapPadding?, animated: Boolean?, - durationMS: Double?, + durationMs: Double?, ) { view.setCameraToCoordinates( coordinates, padding = padding ?: RNMapPadding(0.0, 0.0, 0.0, 0.0), animated == true, - durationMS?.toInt() ?: 3000, + durationMs?.toInt() ?: 3000, ) } + override fun setCameraBounds(bounds: RNLatLngBounds?) { + view.setCameraBounds( + bounds?.toLatLngBounds(), + ) + } + + override fun animateToBounds( + bounds: RNLatLngBounds, + padding: Double?, + durationMs: Double?, + lockBounds: Boolean?, + ) { + view.animateToBounds( + bounds.toLatLngBounds(), + padding = padding?.toInt() ?: 0, + durationMs?.toInt() ?: 3000, + lockBounds = false, + ) + } + + override fun snapshot(options: RNSnapshotOptions): Promise = + view.snapshot( + size = options.size.toSize(), + format = options.format.toFileExtension(), + compressFormat = options.format.toCompressFormat(), + quality = options.quality, + resultIsFile = options.resultType.isFileResult(), + ) + override fun showLocationDialog() { locationHandler.showLocationDialog() } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt new file mode 100644 index 0000000..b073d0f --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt @@ -0,0 +1,17 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.rngooglemapsplus.RNLatLngBounds + +fun RNLatLngBounds.toLatLngBounds(): LatLngBounds = + LatLngBounds( + LatLng( + southWest.latitude, + southWest.longitude, + ), + LatLng( + northEast.latitude, + northEast.longitude, + ), + ) diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt new file mode 100644 index 0000000..132fd8a --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt @@ -0,0 +1,7 @@ +package com.rngooglemapsplus.extensions + +import android.util.Size +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.rngooglemapsplus.RNSize + +fun RNSize?.toSize(): Size? = this?.let { Size(width.dpToPx().toInt(), height.dpToPx().toInt()) } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt new file mode 100644 index 0000000..da71976 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt @@ -0,0 +1,16 @@ +package com.rngooglemapsplus.extensions + +import android.graphics.Bitmap +import com.rngooglemapsplus.RNSnapshotFormat + +fun RNSnapshotFormat?.toCompressFormat(): Bitmap.CompressFormat = + when (this) { + RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> Bitmap.CompressFormat.JPEG + RNSnapshotFormat.PNG, null -> Bitmap.CompressFormat.PNG + } + +fun RNSnapshotFormat?.toFileExtension(): String = + when (this) { + RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> "jpg" + RNSnapshotFormat.PNG, null -> "png" + } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt new file mode 100644 index 0000000..112d333 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt @@ -0,0 +1,9 @@ +package com.rngooglemapsplus.extensions + +import com.rngooglemapsplus.RNSnapshotResultType + +fun RNSnapshotResultType?.isFileResult(): Boolean = + when (this) { + RNSnapshotResultType.FILE -> true + RNSnapshotResultType.BASE64, null -> false + } diff --git a/example/index.js b/example/index.js index 117ddca..470a3ba 100644 --- a/example/index.js +++ b/example/index.js @@ -1,5 +1,8 @@ import { AppRegistry } from 'react-native'; import App from './src/App'; import { name as appName } from './app.json'; +import { LogBox } from 'react-native'; + +LogBox.ignoreLogs(['InteractionManager has been deprecated']); AppRegistry.registerComponent(appName, () => App); diff --git a/example/src/App.tsx b/example/src/App.tsx index fe469a5..05f1756 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -21,7 +21,9 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useColorScheme } from 'react-native'; import BlankScreen from './screens/BlankScreen'; import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen'; +import CameraTestScreen from './screens/CameraTestScreen'; import type { RootStackParamList } from './types/navigation'; +import SnapshotTestScreen from './screens/SnaptshotTestScreen'; const Stack = createStackNavigator(); @@ -100,10 +102,20 @@ export default function App() { component={IndoorLevelMapScreen} options={{ title: 'Indoor level map' }} /> + + diff --git a/example/src/components/ControlPanel.tsx b/example/src/components/ControlPanel.tsx index 88bb30c..1df87fa 100644 --- a/example/src/components/ControlPanel.tsx +++ b/example/src/components/ControlPanel.tsx @@ -29,6 +29,7 @@ export default function ControlPanel({ mapRef, buttons }: Props) { const theme = useAppTheme(); const navigation = useNavigation(); const progress = useSharedValue(0); + const styles = getThemedStyles(theme); const toggle = () => { progress.value = withTiming(progress.value === 1 ? 0 : 1, { @@ -90,22 +91,16 @@ export default function ControlPanel({ mapRef, buttons }: Props) { return ( - - Controls - - - ▼ - + Controls + @@ -113,16 +108,11 @@ export default function ControlPanel({ mapRef, buttons }: Props) { {finalButtons.map((btn, i) => ( - - {btn.title} - + {btn.title} ))} @@ -131,53 +121,61 @@ export default function ControlPanel({ mapRef, buttons }: Props) { ); } -const styles = StyleSheet.create({ - scrollView: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - paddingHorizontal: 12, - paddingTop: 12, - }, - scrollContent: { - paddingBottom: 40, - }, - header: { - borderRadius: 10, - paddingVertical: 12, - alignItems: 'center', - marginBottom: 10, - flexDirection: 'row', - justifyContent: 'center', - }, - headerText: { - fontWeight: '600', - fontSize: 16, - marginRight: 6, - }, - arrow: { - fontSize: 14, - fontWeight: '600', - }, - animatedContainer: { - overflow: 'hidden', - }, - buttonList: { - gap: 8, - }, - button: { - paddingVertical: 12, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 1, - }, - buttonText: { - fontWeight: '600', - fontSize: 15, - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + scrollView: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + paddingHorizontal: 12, + paddingTop: 12, + backgroundColor: theme.bgPrimary, + }, + scrollContent: { + paddingBottom: 40, + }, + header: { + borderRadius: 10, + paddingVertical: 12, + alignItems: 'center', + marginBottom: 10, + flexDirection: 'row', + justifyContent: 'center', + backgroundColor: theme.bgHeader, + }, + headerText: { + fontWeight: '600', + fontSize: 16, + marginRight: 6, + color: theme.textPrimary, + }, + arrow: { + fontSize: 14, + fontWeight: '600', + color: theme.textPrimary, + }, + animatedContainer: { + overflow: 'hidden', + }, + buttonList: { + gap: 8, + }, + button: { + backgroundColor: theme.bgAccent, + paddingVertical: 12, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + shadowColor: theme.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 1, + }, + buttonText: { + fontWeight: '600', + fontSize: 15, + color: theme.textOnAccent, + }, + }); diff --git a/example/src/components/MapWrapper.tsx b/example/src/components/MapWrapper.tsx index 3fa12d0..a9cfb80 100644 --- a/example/src/components/MapWrapper.tsx +++ b/example/src/components/MapWrapper.tsx @@ -4,6 +4,8 @@ import { GoogleMapsView, type RNIndoorBuilding, type RNIndoorLevel, + RNLocationErrorCode, + RNMapErrorCode, } from 'react-native-google-maps-plus'; import type { GoogleMapsViewRef, @@ -58,10 +60,11 @@ export default function MapWrapper(props: Props) { [] ); - const mapPadding = useMemo( - () => ({ top: 20, left: 20, bottom: layout.bottom + 80, right: 20 }), - [layout.bottom] - ); + const mapPadding = useMemo(() => { + return props.children + ? { top: 20, left: 20, bottom: layout.bottom + 80, right: 20 } + : undefined; + }, [layout.bottom, props.children]); const mapZoomConfig = useMemo(() => ({ min: 0, max: 20 }), []); @@ -93,7 +96,7 @@ export default function MapWrapper(props: Props) { uiSettings={props.uiSettings ?? uiSettings} style={[styles.map, props.style]} userInterfaceStyle={ - (props.userInterfaceStyle ?? scheme === 'dark') ? 'dark' : 'light' + props.userInterfaceStyle ?? (scheme === 'dark' ? 'dark' : 'light') } mapType={props.mapType ?? 'normal'} mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} @@ -104,6 +107,11 @@ export default function MapWrapper(props: Props) { f: (ready: boolean) => console.log('Map is ready! ' + ready), } )} + onMapError={callback( + props.onMapError ?? { + f: (error: RNMapErrorCode) => console.log('Map error:', error), + } + )} onMapPress={callback( props.onMapPress ?? { f: (c: RNLatLng) => console.log('Map press:', c), @@ -184,7 +192,7 @@ export default function MapWrapper(props: Props) { )} onLocationError={callback( props.onLocationError ?? { - f: (e: any) => console.log('Location error', e), + f: (e: RNLocationErrorCode) => console.log('Location error', e), } )} /> diff --git a/example/src/screens/CameraTestScreen.tsx b/example/src/screens/CameraTestScreen.tsx new file mode 100644 index 0000000..3610685 --- /dev/null +++ b/example/src/screens/CameraTestScreen.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useRef, useState } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import ControlPanel from '../components/ControlPanel'; +import type { + GoogleMapsViewRef, + RNLatLngBounds, + RNCamera, + RNLatLng, +} from 'react-native-google-maps-plus'; + +export default function CameraTestScreen() { + const mapRef = useRef(null); + const [boundsActive, setBoundsActive] = useState(false); + + const coordinates = useMemo( + () => [ + { latitude: 37.7749, longitude: -122.4194 }, + { latitude: 37.7849, longitude: -122.4094 }, + { latitude: 37.7649, longitude: -122.4294 }, + ], + [] + ); + + const bounds = useMemo( + () => ({ + southWest: { latitude: 37.703, longitude: -122.527 }, + northEast: { latitude: 37.833, longitude: -122.356 }, + }), + [] + ); + + const buttons = useMemo( + () => [ + { + title: 'Set Camera to SF', + onPress: () => { + const camera: RNCamera = { + center: { latitude: 37.7749, longitude: -122.4194 }, + zoom: 12, + bearing: 0, + tilt: 0, + }; + mapRef.current?.setCamera(camera, true, 1000); + }, + }, + { + title: 'Fit Coordinates', + onPress: () => + mapRef.current?.setCameraToCoordinates( + coordinates, + { top: 50, bottom: 50, left: 50, right: 50 }, + true, + 1000 + ), + }, + { + title: boundsActive ? 'Clear Camera Bounds' : 'Set Camera Bounds', + onPress: () => { + if (boundsActive) { + mapRef.current?.setCameraBounds(undefined); + setBoundsActive(false); + } else { + mapRef.current?.setCameraBounds(bounds); + setBoundsActive(true); + } + }, + }, + { + title: 'Animate To Bounds', + onPress: () => mapRef.current?.animateToBounds(bounds, 50, 1200), + }, + ], + [bounds, boundsActive, coordinates] + ); + + return ( + + + + ); +} diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 66333cc..5c4455a 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -14,62 +14,68 @@ const screens = [ { name: 'KmlLayer', title: 'KML Layer' }, { name: 'Location', title: 'Location & Permissions' }, { name: 'CustomStyle', title: 'Custom Map Style' }, - { name: 'IndoorLevelMap', title: 'Indoor level map' }, - { name: 'StressTest', title: 'Stress Test' }, + { name: 'IndoorLevelMap', title: 'Indoor Level Map' }, + { name: 'Camera', title: 'Camera Test' }, + { name: 'Snapshot', title: 'Snapshot Test' }, + { name: 'Stress', title: 'Stress Test' }, ]; export default function HomeScreen() { const navigation = useNavigation>(); const theme = useAppTheme(); + const styles = getThemedStyles(theme); return ( - - - React Native Google Maps Plus Examples - + + React Native Google Maps Plus Examples {screens.map((s) => ( navigation.navigate(s.name)} activeOpacity={0.85} > - - {s.title} - + {s.title} ))} ); } -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 40, - }, - title: { - fontSize: 18, - fontWeight: '600', - marginBottom: 20, - }, - button: { - paddingVertical: 14, - paddingHorizontal: 24, - borderRadius: 10, - marginVertical: 6, - width: '80%', - alignItems: 'center', - }, - buttonText: { - fontSize: 16, - fontWeight: '600', - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + container: { + flexGrow: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + backgroundColor: theme.bgPrimary, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 24, + color: theme.textPrimary, + textAlign: 'center', + }, + button: { + backgroundColor: theme.bgAccent, + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 10, + marginVertical: 6, + width: '80%', + alignItems: 'center', + justifyContent: 'center', + shadowColor: theme.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 2, + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + color: theme.textOnAccent, + }, + }); diff --git a/example/src/screens/MarkersScreen.tsx b/example/src/screens/MarkersScreen.tsx index bccaf57..6e5c303 100644 --- a/example/src/screens/MarkersScreen.tsx +++ b/example/src/screens/MarkersScreen.tsx @@ -29,7 +29,7 @@ export default function MarkersScreen() { const coords = markers.map((m) => m.coordinate); mapRef.current?.setCameraToCoordinates( coords, - { top: 0, left: 0, bottom: 0, right: 0 }, + { top: 50, left: 50, bottom: 50, right: 50 }, true, 300 ); diff --git a/example/src/screens/SnaptshotTestScreen.tsx b/example/src/screens/SnaptshotTestScreen.tsx new file mode 100644 index 0000000..c38f220 --- /dev/null +++ b/example/src/screens/SnaptshotTestScreen.tsx @@ -0,0 +1,152 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { Image, Modal, Pressable, StyleSheet, Text, View } from 'react-native'; +import MapWrapper from '../components/MapWrapper'; +import ControlPanel from '../components/ControlPanel'; +import { useAppTheme } from '../theme'; +import type { GoogleMapsViewRef } from 'react-native-google-maps-plus'; +import { + RNSnapshotFormat, + RNSnapshotResultType, +} from 'react-native-google-maps-plus'; + +export default function SnapshotTestScreen() { + const mapRef = useRef(null); + const [snapshotUri, setSnapshotUri] = useState(null); + const [visible, setVisible] = useState(false); + + const theme = useAppTheme(); + + const buttons = useMemo( + () => [ + { + title: 'Take Snapshot (Base64)', + onPress: async () => { + try { + const result = await mapRef.current?.snapshot({ + format: RNSnapshotFormat.JPG, + quality: 0.9, + resultType: RNSnapshotResultType.BASE64, + }); + if (result) { + setSnapshotUri(result); + setVisible(true); + } + } catch (e) { + console.warn('Snapshot failed:', e); + } + }, + }, + { + title: 'Take Snapshot (File)', + onPress: async () => { + try { + const result = await mapRef.current?.snapshot({ + format: RNSnapshotFormat.JPG, + quality: 0.9, + resultType: RNSnapshotResultType.FILE, + }); + if (result) { + const uri = result.startsWith('file://') + ? result + : `file://${result}`; + setSnapshotUri(uri); + setVisible(true); + } + } catch (e) { + console.warn('Snapshot failed:', e); + } + }, + }, + ], + [] + ); + + const styles = getThemedStyles(theme); + + return ( + + + + + + + Map Snapshot + + {snapshotUri ? ( + + ) : ( + No image + )} + + setVisible(false)} + style={styles.closeButton} + > + Close + + + + + + ); +} + +const getThemedStyles = (theme: any) => + StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.6)', + alignItems: 'center', + justifyContent: 'center', + }, + dialog: { + backgroundColor: theme.bgPrimary, + borderColor: theme.border, + padding: 20, + borderRadius: 14, + alignItems: 'center', + width: '80%', + shadowColor: theme.shadow, + shadowOpacity: 0.2, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 }, + elevation: 3, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 14, + color: theme.textPrimary, + }, + image: { + width: 260, + height: 260, + borderRadius: 10, + marginBottom: 20, + backgroundColor: theme.bgHeader, + }, + noImage: { + color: theme.textSecondary, + marginBottom: 20, + }, + closeButton: { + backgroundColor: theme.bgAccent, + borderRadius: 10, + paddingVertical: 10, + paddingHorizontal: 20, + shadowColor: theme.shadow, + shadowOpacity: 0.25, + shadowRadius: 6, + shadowOffset: { width: 0, height: 3 }, + elevation: 2, + }, + closeText: { + color: theme.textOnAccent, + fontWeight: '600', + fontSize: 15, + }, + }); diff --git a/example/src/screens/StressTestScreen.tsx b/example/src/screens/StressTestScreen.tsx index f33ecc6..27ea360 100644 --- a/example/src/screens/StressTestScreen.tsx +++ b/example/src/screens/StressTestScreen.tsx @@ -38,7 +38,7 @@ export default function StressTestScreen() { if (coords.length) mapRef.current?.setCameraToCoordinates( coords, - { top: 0, left: 0, bottom: 0, right: 0 }, + { top: 50, left: 50, bottom: 50, right: 50 }, true, 300 ); diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 32d67ee..0cdaa46 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -11,6 +11,8 @@ export type RootStackParamList = { Location: undefined; CustomStyle: undefined; IndoorLevelMap: undefined; + Camera: undefined; + Snapshot: undefined; StressTest: undefined; }; diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 500be6b..11b4f29 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -41,6 +41,7 @@ GMSIndoorDisplayDelegate { setupAppLifecycleObservers() } + @MainActor private func setupAppLifecycleObservers() { NotificationCenter.default.addObserver( self, @@ -82,6 +83,7 @@ GMSIndoorDisplayDelegate { mapReady = true } + @MainActor private func initLocationCallbacks() { locationHandler.onUpdate = { [weak self] loc in guard let self = self else { return } @@ -191,6 +193,7 @@ GMSIndoorDisplayDelegate { } } + @MainActor var currentCamera: GMSCameraPosition? { mapView?.camera } @@ -268,7 +271,8 @@ GMSIndoorDisplayDelegate { } } - @MainActor var mapPadding: RNMapPadding? { + @MainActor + var mapPadding: RNMapPadding? { didSet { mapView?.padding = mapPadding.map { @@ -282,13 +286,15 @@ GMSIndoorDisplayDelegate { } } - @MainActor var mapType: GMSMapViewType? { + @MainActor + var mapType: GMSMapViewType? { didSet { mapView?.mapType = mapType ?? .normal } } - @MainActor var locationConfig: RNLocationConfig? { + @MainActor + var locationConfig: RNLocationConfig? { didSet { locationHandler.desiredAccuracy = locationConfig?.ios?.desiredAccuracy?.toCLLocationAccuracy @@ -315,11 +321,12 @@ GMSIndoorDisplayDelegate { var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? - func setCamera(camera: GMSCameraPosition, animated: Bool, durationMS: Double) { + @MainActor + func setCamera(camera: GMSCameraPosition, animated: Bool, durationMs: Double) { if animated { withCATransaction( disableActions: false, - duration: durationMS / 1000.0 + duration: durationMs / 1000.0 ) { mapView?.animate(to: camera) } @@ -329,11 +336,12 @@ GMSIndoorDisplayDelegate { } } + @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding, animated: Bool, - durationMS: Double + durationMs: Double ) { if coordinates.isEmpty { return @@ -369,7 +377,7 @@ GMSIndoorDisplayDelegate { if animated { withCATransaction( disableActions: false, - duration: durationMS / 1000.0 + duration: durationMs / 1000.0 ) { mapView?.animate(with: update) } @@ -378,6 +386,90 @@ GMSIndoorDisplayDelegate { } } + @MainActor + func setCameraBounds(_ bounds: GMSCoordinateBounds?) { + mapView?.cameraTargetBounds = bounds + } + + @MainActor + func animateToBounds( + _ bounds: GMSCoordinateBounds, + padding: Double, + durationMs: Double, + lockBounds: Bool + ) { + if lockBounds { + mapView?.cameraTargetBounds = bounds + } + + let update = GMSCameraUpdate.fit(bounds, withPadding: CGFloat(padding)) + mapView?.animate(with: update) + } + + @MainActor + func snapshot( + size: CGSize?, + format: String, + imageFormat: ImageFormat, + quality: CGFloat, + resultIsFile: Bool + ) -> NitroModules.Promise { + let promise = Promise() + + DispatchQueue.main.async { + guard let mapView = self.mapView else { + promise.resolve(withResult: nil) + return + } + + let renderer = UIGraphicsImageRenderer(bounds: mapView.bounds) + let image = renderer.image { ctx in + mapView.layer.render(in: ctx.cgContext) + } + + var finalImage = image + + size.map { + UIGraphicsBeginImageContextWithOptions($0, false, 0.0) + image.draw(in: CGRect(origin: .zero, size: $0)) + finalImage = UIGraphicsGetImageFromCurrentImageContext() ?? image + UIGraphicsEndImageContext() + } + + let data: Data? + switch imageFormat { + case .jpeg: + data = finalImage.jpegData(compressionQuality: quality) + case .png: + data = finalImage.pngData() + } + + guard let imageData = data else { + promise.resolve(withResult: nil) + return + } + + // Rückgabe + if resultIsFile { + let filename = + "map_snapshot_\(Int(Date().timeIntervalSince1970)).\(format)" + let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(filename) + do { + try imageData.write(to: fileURL) + promise.resolve(withResult: fileURL.path) + } catch { + promise.resolve(withResult: nil) + } + } else { + let base64 = imageData.base64EncodedString() + promise.resolve(withResult: "data:image/\(format);base64,\(base64)") + } + } + + return promise + } + @MainActor func addMarker(id: String, marker: GMSMarker) { if mapView == nil { @@ -619,177 +711,215 @@ GMSIndoorDisplayDelegate { } func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + onMain { + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) + + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - cameraMoveReasonIsGesture = gesture + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.cameraMoveReasonIsGesture = gesture - onCameraChangeStart?(region, cam, gesture) + self.onCameraChangeStart?(region, cam, gesture) + } } func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - if let last = lastSubmittedCameraPosition, - last.target.latitude == position.target.latitude, - last.target.longitude == position.target.longitude, - last.zoom == position.zoom, - last.bearing == position.bearing, - last.viewingAngle == position.viewingAngle { - return - } - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) + onMain { + if let last = self.lastSubmittedCameraPosition, + last.target.latitude == position.target.latitude, + last.target.longitude == position.target.longitude, + last.zoom == position.zoom, + last.bearing == position.bearing, + last.viewingAngle == position.viewingAngle { + return + } - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + self.lastSubmittedCameraPosition = position + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - onCameraChange?(region, cam, cameraMoveReasonIsGesture) - lastSubmittedCameraPosition = position + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.onCameraChange?(region, cam, self.cameraMoveReasonIsGesture) + } } func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) { - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + onMain { + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) + + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - onCameraChangeComplete?(region, cam, cameraMoveReasonIsGesture) + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.onCameraChangeComplete?(region, cam, self.cameraMoveReasonIsGesture) + } } func mapView( _ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D ) { - onMapPress?( - RNLatLng( - latitude: coordinate.latitude, - longitude: coordinate.longitude + onMain { + self.onMapPress?( + RNLatLng( + latitude: coordinate.latitude, + longitude: coordinate.longitude + ) ) - ) + } } func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { - mapView.selectedMarker = marker - onMarkerPress?(marker.userData as? String, ) + onMain { + mapView.selectedMarker = marker + self.onMarkerPress?(marker.userData as? String, ) + } return true } func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { - switch overlay { - case let circle as GMSCircle: - onCirclePress?(circle.userData as? String, ) + onMain { + switch overlay { + case let circle as GMSCircle: + self.onCirclePress?(circle.userData as? String, ) - case let polygon as GMSPolygon: - onPolygonPress?(polygon.userData as? String, ) + case let polygon as GMSPolygon: + self.onPolygonPress?(polygon.userData as? String, ) - case let polyline as GMSPolyline: - onPolylinePress?(polyline.userData as? String, ) + case let polyline as GMSPolyline: + self.onPolylinePress?(polyline.userData as? String, ) - default: - break + default: + break + } } } func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { - onMarkerDragStart?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDragStart?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { - onMarkerDrag?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDrag?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { - onMarkerDragEnd?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDragEnd?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func didChangeActiveBuilding(_ building: GMSIndoorBuilding?) { - guard let display = mapView?.indoorDisplay, let building else { return } - onIndoorBuildingFocused?(building.toRNIndoorBuilding(from: display)) + onMain { + guard let display = self.mapView?.indoorDisplay, let building else { + return + } + self.onIndoorBuildingFocused?(building.toRNIndoorBuilding(from: display)) + } } func didChangeActiveLevel(_ level: GMSIndoorLevel?) { - guard - let display = mapView?.indoorDisplay, - let building = display.activeBuilding, - let level, - let index = building.levels.firstIndex(where: { - $0.name == level.name && $0.shortName == level.shortName - }) - else { return } - - onIndoorLevelActivated?(level.toRNIndoorLevel(index: index, active: true)) + onMain { + guard + let display = self.mapView?.indoorDisplay, + let building = display.activeBuilding, + let level, + let index = building.levels.firstIndex(where: { + $0.name == level.name && $0.shortName == level.shortName + }) + else { return } + + self.onIndoorLevelActivated?( + level.toRNIndoorLevel(index: index, active: true) + ) + } } +} +@inline(__always) +func onMain(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { block() } + } } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 1cb8c16..cf7c0db 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -277,115 +277,160 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var locationConfig: RNLocationConfig? { + @MainActor + var locationConfig: RNLocationConfig? { didSet { impl.locationConfig = locationConfig } } + @MainActor var onMapError: ((RNMapErrorCode) -> Void)? { didSet { impl.onMapError = onMapError } } + @MainActor var onMapReady: ((Bool) -> Void)? { didSet { impl.onMapReady = onMapReady } } + @MainActor var onLocationUpdate: ((RNLocation) -> Void)? { didSet { impl.onLocationUpdate = onLocationUpdate } } + @MainActor var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? { didSet { impl.onLocationError = onLocationError } } + @MainActor var onMapPress: ((RNLatLng) -> Void)? { didSet { impl.onMapPress = onMapPress } } + @MainActor var onMarkerPress: ((String?) -> Void)? { didSet { impl.onMarkerPress = onMarkerPress } } + @MainActor var onPolylinePress: ((String?) -> Void)? { didSet { impl.onPolylinePress = onPolylinePress } } + @MainActor var onPolygonPress: ((String?) -> Void)? { didSet { impl.onPolygonPress = onPolygonPress } } + @MainActor var onCirclePress: ((String?) -> Void)? { didSet { impl.onCirclePress = onCirclePress } } + @MainActor var onMarkerDragStart: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragStart = onMarkerDragStart } } + @MainActor var onMarkerDrag: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDrag = onMarkerDrag } } + @MainActor var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } + @MainActor var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? { didSet { impl.onIndoorBuildingFocused = onIndoorBuildingFocused } } + @MainActor var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? { didSet { impl.onIndoorLevelActivated = onIndoorLevelActivated } } + @MainActor var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } + @MainActor var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChange = onCameraChange } } + @MainActor var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeComplete = onCameraChangeComplete } } - func setCamera(camera: RNCamera, animated: Bool?, durationMS: Double?) { + @MainActor + func setCamera(camera: RNCamera, animated: Bool?, durationMs: Double?) { let cam = camera.toGMSCameraPosition(current: impl.currentCamera) - onMain { - self.impl.setCamera( - camera: cam, - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) - } + impl.setCamera( + camera: cam, + animated: animated ?? true, + durationMs: durationMs ?? 3000 + ) } + @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding?, animated: Bool?, - durationMS: Double? + durationMs: Double? ) { - onMain { - self.impl.setCameraToCoordinates( - coordinates: coordinates, - padding: padding ?? RNMapPadding(0, 0, 0, 0), - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) - } + impl.setCameraToCoordinates( + coordinates: coordinates, + padding: padding ?? RNMapPadding(0, 0, 0, 0), + animated: animated ?? true, + durationMs: durationMs ?? 3000 + ) + } + + @MainActor + func setCameraBounds(bounds: RNLatLngBounds?) { + impl.setCameraBounds(bounds?.toCoordinateBounds()) } + @MainActor + func animateToBounds( + bounds: RNLatLngBounds, + padding: Double?, + durationMs: Double?, + lockBounds: Bool? + ) { + impl.animateToBounds( + bounds.toCoordinateBounds(), + padding: padding ?? 0, + durationMs: durationMs ?? 3000, + lockBounds: false + ) + } + + @MainActor + func snapshot( + options: RNSnapshotOptions, + ) -> NitroModules.Promise { + return impl.snapshot( + size: options.size?.toCGSize(), + format: options.format.toFileExtension(), + imageFormat: options.format.toImageFormat(), + quality: CGFloat(options.quality), + resultIsFile: options.resultType.isFileResult() + ) + + } + + @MainActor func showLocationDialog() { locationHandler.showLocationDialog() } + @MainActor func openLocationSettings() { locationHandler.openLocationSettings() } + @MainActor func requestLocationPermission() -> NitroModules.Promise { return permissionHandler.requestLocationPermission() } + @MainActor func isGooglePlayServicesAvailable() -> Bool { /// not supported return true } } - -@inline(__always) -func onMain(_ block: @escaping () -> Void) { - if Thread.isMainThread { - block() - } else { - DispatchQueue.main.async { block() } - } -} diff --git a/ios/extensions/RNLatLngBounds+Extension.swift b/ios/extensions/RNLatLngBounds+Extension.swift new file mode 100644 index 0000000..2d5ca16 --- /dev/null +++ b/ios/extensions/RNLatLngBounds+Extension.swift @@ -0,0 +1,16 @@ +import GoogleMaps + +extension RNLatLngBounds { + func toCoordinateBounds() -> GMSCoordinateBounds { + return GMSCoordinateBounds( + coordinate: CLLocationCoordinate2D( + latitude: southWest.latitude, + longitude: southWest.longitude + ), + coordinate: CLLocationCoordinate2D( + latitude: northEast.latitude, + longitude: northEast.longitude + ) + ) + } +} diff --git a/ios/extensions/RNSize+Extension.swift b/ios/extensions/RNSize+Extension.swift new file mode 100644 index 0000000..723509e --- /dev/null +++ b/ios/extensions/RNSize+Extension.swift @@ -0,0 +1,7 @@ +import UIKit + +extension RNSize { + func toCGSize() -> CGSize? { + CGSize(width: width, height: height) + } +} diff --git a/ios/extensions/RNSnapshotFormat+Extension.swift b/ios/extensions/RNSnapshotFormat+Extension.swift new file mode 100644 index 0000000..a6afda7 --- /dev/null +++ b/ios/extensions/RNSnapshotFormat+Extension.swift @@ -0,0 +1,28 @@ +enum ImageFormat { + case png + case jpeg +} + +extension RNSnapshotFormat { + func toImageFormat() -> ImageFormat { + switch self { + case .jpg, .jpeg: + return .jpeg + case .png: + return .png + @unknown default: + return .png + } + } + + func toFileExtension() -> String { + switch self { + case .jpg, .jpeg: + return "jpg" + case .png: + return "png" + @unknown default: + return "png" + } + } +} diff --git a/ios/extensions/RNSnapshotResultType+Extension.swift b/ios/extensions/RNSnapshotResultType+Extension.swift new file mode 100644 index 0000000..61a3e88 --- /dev/null +++ b/ios/extensions/RNSnapshotResultType+Extension.swift @@ -0,0 +1,12 @@ +extension RNSnapshotResultType { + func isFileResult() -> Bool { + switch self { + case .file: + return true + case .base64: + return false + @unknown default: + return false + } + } +} diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index fbc41e4..24e6cda 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -26,6 +26,8 @@ import type { RNKMLayer, RNIndoorBuilding, RNIndoorLevel, + RNLatLngBounds, + RNSnapshotOptions, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -79,15 +81,26 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { } export interface RNGoogleMapsPlusViewMethods extends HybridViewMethods { - setCamera(camera: RNCamera, animated?: boolean, durationMS?: number): void; + setCamera(camera: RNCamera, animated?: boolean, durationMs?: number): void; setCameraToCoordinates( coordinates: RNLatLng[], padding?: RNMapPadding, animated?: boolean, - durationMS?: number + durationMs?: number ): void; + setCameraBounds(bounds?: RNLatLngBounds): void; + + animateToBounds( + bounds: RNLatLngBounds, + padding?: number, + durationMs?: number, + lockBounds?: boolean + ): void; + + snapshot(options: RNSnapshotOptions): Promise; + showLocationDialog(): void; openLocationSettings(): void; diff --git a/src/types.ts b/src/types.ts index ab856ad..8fc9660 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,9 +23,38 @@ export type RNMapUiSettings = { zoomGesturesEnabled?: boolean; }; -export type RNLatLng = { latitude: number; longitude: number }; +export type RNLatLng = { + latitude: number; + longitude: number; +}; + +export type RNLatLngBounds = { + northEast: RNLatLng; + southWest: RNLatLng; +}; + +export type RNSnapshotOptions = { + size?: RNSize; + format: RNSnapshotFormat; + quality: number; + resultType: RNSnapshotResultType; +}; -export type RNBoundingBox = { northEast: RNLatLng; southWest: RNLatLng }; +export type RNSize = { + width: number; + height: number; +}; + +export enum RNSnapshotFormat { + PNG = 0, + JPG = 1, + JPEG = 2, +} + +export enum RNSnapshotResultType { + BASE64 = 0, + FILE = 1, +} export type RNMapPadding = { top: number; From 652519534a7c2a0d9dc16acce3d803f55575f96d Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 22:09:20 +0700 Subject: [PATCH 10/15] feat: snapshot feature --- example/src/screens/SnaptshotTestScreen.tsx | 12 ++++-------- src/types.ts | 11 ++--------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/example/src/screens/SnaptshotTestScreen.tsx b/example/src/screens/SnaptshotTestScreen.tsx index c38f220..142baf6 100644 --- a/example/src/screens/SnaptshotTestScreen.tsx +++ b/example/src/screens/SnaptshotTestScreen.tsx @@ -4,10 +4,6 @@ import MapWrapper from '../components/MapWrapper'; import ControlPanel from '../components/ControlPanel'; import { useAppTheme } from '../theme'; import type { GoogleMapsViewRef } from 'react-native-google-maps-plus'; -import { - RNSnapshotFormat, - RNSnapshotResultType, -} from 'react-native-google-maps-plus'; export default function SnapshotTestScreen() { const mapRef = useRef(null); @@ -23,9 +19,9 @@ export default function SnapshotTestScreen() { onPress: async () => { try { const result = await mapRef.current?.snapshot({ - format: RNSnapshotFormat.JPG, + format: 'jpg', quality: 0.9, - resultType: RNSnapshotResultType.BASE64, + resultType: 'base64', }); if (result) { setSnapshotUri(result); @@ -41,9 +37,9 @@ export default function SnapshotTestScreen() { onPress: async () => { try { const result = await mapRef.current?.snapshot({ - format: RNSnapshotFormat.JPG, + format: 'jpg', quality: 0.9, - resultType: RNSnapshotResultType.FILE, + resultType: 'file', }); if (result) { const uri = result.startsWith('file://') diff --git a/src/types.ts b/src/types.ts index 8fc9660..3746c6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,16 +45,9 @@ export type RNSize = { height: number; }; -export enum RNSnapshotFormat { - PNG = 0, - JPG = 1, - JPEG = 2, -} +export type RNSnapshotFormat = 'png' | 'jpg' | 'jpeg'; -export enum RNSnapshotResultType { - BASE64 = 0, - FILE = 1, -} +export type RNSnapshotResultType = 'base64' | 'file'; export type RNMapPadding = { top: number; From 60f1ee577da1fb7b25038a5ce7b91ab18f98ba19 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 22:59:32 +0700 Subject: [PATCH 11/15] ci: removed pod cache --- .github/workflows/pull_request.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2eecd11..311e216 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -105,17 +105,7 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} - - name: Cache Pods - id: pods-cache - uses: actions/cache@v4.2.4 - with: - path: example/ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile', 'example/ios/Podfile.lock', 'example/package.json') }} - restore-keys: | - ${{ runner.os }}-pods- - - name: Install cocoapods - if: steps.pods-cache.outputs.cache-hit != 'true' working-directory: example run: yarn ios:pods From f1110e728cbf89bd3fac74b0ab83409776925943 Mon Sep 17 00:00:00 2001 From: pinpong Date: Sat, 11 Oct 2025 10:47:19 +0700 Subject: [PATCH 12/15] fix: mapview onDestroy --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 3 +- ios/GoogleMapViewImpl.swift | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 46ab2ec..8fd0113 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -975,6 +975,7 @@ class GoogleMapsViewImpl( fun destroyInternal() { onUi { + locationHandler.stop() markerBuilder.cancelAllJobs() clearMarkers() clearPolylines() @@ -982,7 +983,6 @@ class GoogleMapsViewImpl( clearCircles() clearHeatmaps() clearKmlLayer() - locationHandler.stop() googleMap?.apply { setOnCameraMoveStartedListener(null) setOnCameraMoveListener(null) @@ -1003,6 +1003,7 @@ class GoogleMapsViewImpl( } super.removeAllViews() reactContext.removeLifecycleEventListener(this) + initialized = false } } diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 11b4f29..da91336 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -629,7 +629,10 @@ GMSIndoorDisplayDelegate { @MainActor func clearHeatmaps() { - heatmapsById.values.forEach { $0.map = nil } + heatmapsById.values.forEach { + $0.clearTileCache() + $0.map = nil + } heatmapsById.removeAll() pendingHeatmaps.removeAll() } @@ -671,16 +674,21 @@ GMSIndoorDisplayDelegate { } func deinitInternal() { - markerBuilder.cancelAllIconTasks() - clearMarkers() - clearPolylines() - clearPolygons() - clearCircles() - clearHeatmaps() - locationHandler.stop() - mapView?.clear() - mapView?.delegate = nil - mapView = nil + onMain { + self.locationHandler.stop() + self.markerBuilder.cancelAllIconTasks() + self.clearMarkers() + self.clearPolylines() + self.clearPolygons() + self.clearCircles() + self.clearHeatmaps() + self.clearKmlLayers() + self.mapView?.clear() + self.mapView?.indoorDisplay.delegate = nil + self.mapView?.delegate = nil + self.mapView = nil + self.initialized = false + } } @objc private func appDidBecomeActive() { From 46813b8d7aec265264038f0dd45d4cc242f42d57 Mon Sep 17 00:00:00 2001 From: pinpong Date: Sat, 11 Oct 2025 12:14:43 +0700 Subject: [PATCH 13/15] fix: disable ios view recycling --- scripts/nitrogen-patch.js | 68 +++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/scripts/nitrogen-patch.js b/scripts/nitrogen-patch.js index ce7832f..c4d5353 100644 --- a/scripts/nitrogen-patch.js +++ b/scripts/nitrogen-patch.js @@ -1,24 +1,44 @@ /** - * Recursively patches all generated Android files: + * Recursively patches all generated Nitro files (Android & iOS): + * + * ANDROID * - Replaces 'com.margelo.nitro.rngooglemapsplus' -> 'com.rngooglemapsplus' * - Replaces 'com/margelo/nitro/rngooglemapsplus' -> 'com/rngooglemapsplus' * - Removes 'margelo/nitro/' in RNGoogleMapsPlusOnLoad.cpp - * - Inserts `prepareToRecycleView()` under `onDropViewInstance()` if missing + * - Inserts `prepareToRecycleView()` + * nitrogen/generated/android/kotlin/com/margelo/nitro/rngooglemapsplus/views/HybridRNGoogleMapsPlusViewManager.kt + * + * iOS + * - Inserts `+ (BOOL)shouldBeRecycled` + * nitrogen/generated/ios/c++/views/HybridRNGoogleMapsPlusViewComponent.mm */ import { fileURLToPath } from 'url'; import { basename } from 'path'; import path from 'node:path'; import { readdir, readFile, writeFile } from 'node:fs/promises'; -const ROOT_DIR = path.join(process.cwd(), 'nitrogen', 'generated', 'android'); -console.log(ROOT_DIR); -const ANDROID_ONLOAD_FILE = path.join(ROOT_DIR, 'RNGoogleMapsPlusOnLoad.cpp'); +const ROOT_ANDROID = path.join( + process.cwd(), + 'nitrogen', + 'generated', + 'android' +); +const ROOT_IOS = path.join(process.cwd(), 'nitrogen', 'generated', 'ios'); +const ANDROID_ONLOAD_FILE = path.join( + ROOT_ANDROID, + 'RNGoogleMapsPlusOnLoad.cpp' +); const HYBRID_VIEW_MANAGER = path.join( - ROOT_DIR, + ROOT_ANDROID, 'kotlin/com/margelo/nitro/rngooglemapsplus/views/HybridRNGoogleMapsPlusViewManager.kt' ); +const HYBRID_VIEW_COMPONENT_IOS = path.join( + ROOT_IOS, + 'c++/views/HybridRNGoogleMapsPlusViewComponent.mm' +); + const REPLACEMENTS = [ { regex: /com\.margelo\.nitro\.rngooglemapsplus/g, @@ -33,14 +53,21 @@ const REPLACEMENTS = [ const __filename = fileURLToPath(import.meta.url); const filename = basename(__filename); -const RECYCLE_METHOD = ` +const RECYCLE_METHOD_ANDROID = ` /// added by ${filename} override fun prepareToRecycleView(reactContext: ThemedReactContext, view: View): View? { return null } `; -// Patch-Routine +const RECYCLE_METHOD_IOS = ` +/// added by ${filename} ++ (BOOL)shouldBeRecycled +{ + return NO; +} +`; + async function processFile(filePath) { let content = await readFile(filePath, 'utf8'); let updated = content; @@ -53,16 +80,30 @@ async function processFile(filePath) { updated = updated.replace(/margelo\/nitro\//g, ''); } - console.log(filePath); if (path.resolve(filePath) === path.resolve(HYBRID_VIEW_MANAGER)) { if (!/override fun prepareToRecycleView/.test(updated)) { const pattern = /(override fun onDropViewInstance\(view: View\)\s*\{[^}]+\}\s*)/m; if (pattern.test(updated)) { - updated = updated.replace(pattern, `$1${RECYCLE_METHOD}\n`); + updated = updated.replace(pattern, `$1${RECYCLE_METHOD_ANDROID}\n`); + } else { + throw new Error( + `Pattern for "onDropViewInstance" not found in ${filePath}` + ); + } + } + } + + if (path.resolve(filePath) === path.resolve(HYBRID_VIEW_COMPONENT_IOS)) { + if (!/\+\s*\(BOOL\)\s*shouldBeRecycled/.test(updated)) { + const pattern = + /(- \(instancetype\)\s*init\s*\{(?:[^{}]|\{[^{}]*\})*\})/m; + + if (pattern.test(updated)) { + updated = updated.replace(pattern, `$1\n${RECYCLE_METHOD_IOS}`); } else { - updated = updated.replace(/}\s*$/m, `${RECYCLE_METHOD}\n}\n`); + throw new Error(`Pattern for "init" not found in ${filePath}`); } } } @@ -87,8 +128,9 @@ async function start(dir) { (async () => { try { - await start(ROOT_DIR); - console.log('All occurrences patched successfully.'); + await start(ROOT_ANDROID); + await start(ROOT_IOS); + console.log('All Nitrogen files patched successfully.'); } catch (err) { console.error('Error while processing files:', err); process.exit(1); From 0370ddfc113f4d414c212f28fcfc744fa38c9130 Mon Sep 17 00:00:00 2001 From: pinpong Date: Sat, 11 Oct 2025 12:46:42 +0700 Subject: [PATCH 14/15] fix: map initialization --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 4 +- .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 16 ++++-- example/src/components/MapWrapper.tsx | 51 ++++++++++++++----- ios/.swiftlint.yml | 1 - ios/GoogleMapViewImpl.swift | 3 -- ios/RNGoogleMapsPlusView.swift | 20 +++----- 6 files changed, 58 insertions(+), 37 deletions(-) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 8fd0113..87eac47 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -153,9 +153,9 @@ class GoogleMapsViewImpl( } initLocationCallbacks() applyPending() + mapReady = true + onMapReady?.invoke(true) } - mapReady = true - onMapReady?.invoke(true) } override fun onCameraMoveStarted(reason: Int) { diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 845421f..d77ef1d 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -16,6 +16,9 @@ import com.rngooglemapsplus.extensions.toFileExtension import com.rngooglemapsplus.extensions.toLatLngBounds import com.rngooglemapsplus.extensions.toMapColorScheme import com.rngooglemapsplus.extensions.toSize +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @DoNotStrip class RNGoogleMapsPlusView( @@ -39,11 +42,14 @@ class RNGoogleMapsPlusView( set(value) { if (field == value) return field = value - view.initMapView( - value?.mapId, - value?.liteMode, - value?.camera?.toCameraPosition(), - ) + MainScope().launch { + delay(500) + view.initMapView( + value?.mapId, + value?.liteMode, + value?.camera?.toCameraPosition(), + ) + } } override var uiSettings: RNMapUiSettings? = null diff --git a/example/src/components/MapWrapper.tsx b/example/src/components/MapWrapper.tsx index a9cfb80..5ccb035 100644 --- a/example/src/components/MapWrapper.tsx +++ b/example/src/components/MapWrapper.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { StyleSheet, useColorScheme, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { GoogleMapsView, type RNIndoorBuilding, @@ -22,6 +22,7 @@ import { import type { ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { callback } from 'react-native-nitro-modules'; +import { useTheme } from '@react-navigation/native'; type Props = ViewProps & RNGoogleMapsPlusViewProps & { @@ -31,8 +32,11 @@ type Props = ViewProps & export default function MapWrapper(props: Props) { const { children, ...rest } = props; - const scheme = useColorScheme(); + const theme = useTheme(); + const styles = getThemedStyles(theme); const layout = useSafeAreaInsets(); + + const [mapReady, setMapReady] = React.useState(false); const initialProps = useMemo( () => ({ camera: { @@ -96,7 +100,7 @@ export default function MapWrapper(props: Props) { uiSettings={props.uiSettings ?? uiSettings} style={[styles.map, props.style]} userInterfaceStyle={ - props.userInterfaceStyle ?? (scheme === 'dark' ? 'dark' : 'light') + props.userInterfaceStyle ?? (theme.dark ? 'dark' : 'light') } mapType={props.mapType ?? 'normal'} mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} @@ -104,7 +108,10 @@ export default function MapWrapper(props: Props) { locationConfig={props.locationConfig ?? locationConfig} onMapReady={callback( props.onMapReady ?? { - f: (ready: boolean) => console.log('Map is ready! ' + ready), + f: (ready: boolean) => { + console.log('Map is ready! ' + ready); + setMapReady(true); + }, } )} onMapError={callback( @@ -197,17 +204,33 @@ export default function MapWrapper(props: Props) { )} /> {children} + {!mapReady && ( + + + + )} ); } -const styles = StyleSheet.create({ - container: { flex: 1 }, - map: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + map: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + loadingOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + backgroundColor: theme.dark ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.7)', + }, + }); diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 8d80b0f..a84aed4 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -4,7 +4,6 @@ disabled_rules: - cyclomatic_complexity - function_body_length - closure_parameter_position - - todo identifier_name: min_length: diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index da91336..a27f121 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -704,9 +704,6 @@ GMSIndoorDisplayDelegate { override func didMoveToWindow() { super.didMoveToWindow() if window != nil { - if mapView != nil && mapReady { - onMapReady?(true) - } locationHandler.start() } else { locationHandler.stop() diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index cf7c0db..9294709 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -29,21 +29,17 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - /* - /// TODO: prepareForRecycle - override func prepareForRecycle() { - impl.clearAll() - } - */ - @MainActor var initialProps: RNInitialProps? { didSet { - impl.initMapView( - mapId: initialProps?.mapId, - liteMode: initialProps?.liteMode, - camera: initialProps?.camera?.toGMSCameraPosition(current: nil) - ) + Task { @MainActor in + try? await Task.sleep(nanoseconds: 500_000_000) + impl.initMapView( + mapId: initialProps?.mapId, + liteMode: initialProps?.liteMode, + camera: initialProps?.camera?.toGMSCameraPosition(current: nil) + ) + } } } From 150a52640f9ec79a108c9c4aa69bdecd5831dece Mon Sep 17 00:00:00 2001 From: pinpong Date: Sat, 11 Oct 2025 13:15:02 +0700 Subject: [PATCH 15/15] refactor: map initialization --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 2 ++ .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 25 +++++++++++-------- ios/GoogleMapViewImpl.swift | 5 ++++ ios/RNGoogleMapsPlusView.swift | 21 ++++++++++------ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 87eac47..ad55a07 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -372,6 +372,8 @@ class GoogleMapsViewImpl( } } + var initialProps: RNInitialProps? = null + var uiSettings: RNMapUiSettings? = null set(value) { field = value diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index d77ef1d..d31d24e 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -16,14 +16,12 @@ import com.rngooglemapsplus.extensions.toFileExtension import com.rngooglemapsplus.extensions.toLatLngBounds import com.rngooglemapsplus.extensions.toMapColorScheme import com.rngooglemapsplus.extensions.toSize -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @DoNotStrip class RNGoogleMapsPlusView( val context: ThemedReactContext, ) : HybridRNGoogleMapsPlusViewSpec() { + private var propsInitialized = false private var currentCustomMapStyle: String? = null private var permissionHandler = PermissionHandler(context) private var locationHandler = LocationHandler(context) @@ -38,18 +36,23 @@ class RNGoogleMapsPlusView( override val view = GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerBuilder) + override fun afterUpdate() { + super.afterUpdate() + if (!propsInitialized) { + propsInitialized = true + view.initMapView( + initialProps?.mapId, + initialProps?.liteMode, + initialProps?.camera?.toCameraPosition(), + ) + } + } + override var initialProps: RNInitialProps? = null set(value) { if (field == value) return field = value - MainScope().launch { - delay(500) - view.initMapView( - value?.mapId, - value?.liteMode, - value?.camera?.toCameraPosition(), - ) - } + view.initialProps = value } override var uiSettings: RNMapUiSettings? = null diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index a27f121..4fcedc2 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -198,6 +198,11 @@ GMSIndoorDisplayDelegate { mapView?.camera } + @MainActor + var initialProps: RNInitialProps? { + didSet {} + } + @MainActor var uiSettings: RNMapUiSettings? { didSet { diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 9294709..13b7e87 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -8,6 +8,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { private let permissionHandler: PermissionHandler private let locationHandler: LocationHandler + private var propsInitialized = false private let markerBuilder = MapMarkerBuilder() private let polylineBuilder = MapPolylineBuilder() private let polygonBuilder = MapPolygonBuilder() @@ -29,20 +30,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - @MainActor - var initialProps: RNInitialProps? { - didSet { + func afterUpdate() { + if !propsInitialized { + propsInitialized = true Task { @MainActor in - try? await Task.sleep(nanoseconds: 500_000_000) impl.initMapView( - mapId: initialProps?.mapId, - liteMode: initialProps?.liteMode, - camera: initialProps?.camera?.toGMSCameraPosition(current: nil) + mapId: self.initialProps?.mapId, + liteMode: self.initialProps?.liteMode, + camera: self.initialProps?.camera?.toGMSCameraPosition(current: nil) ) } } } + @MainActor + var initialProps: RNInitialProps? { + didSet { + impl.initialProps = initialProps + } + } + @MainActor var uiSettings: RNMapUiSettings? { didSet { impl.uiSettings = uiSettings }