Skip to content

Commit b6c1281

Browse files
authored
feat: add snapshot function
## Pull request ### Before submitting - [x] This PR targets the `dev` branch (not `main`) - [x] Commit messages follow the semantic-release format - [x] No debug logs or sensitive data included --- ### Summary Add snapshot feature ### Type of change - [x] Feature - [ ] Fix - [ ] Refactor - [ ] Internal / CI - [ ] Documentation --- ### Scope - [x] Android - [x] iOS - [ ] Core - [x] Example App - [ ] Docs
2 parents cd25244 + 60f1ee5 commit b6c1281

26 files changed

+982
-298
lines changed

.github/workflows/pull_request.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,7 @@ jobs:
105105
with:
106106
xcode-version: ${{ env.XCODE_VERSION }}
107107

108-
- name: Cache Pods
109-
id: pods-cache
110-
uses: actions/[email protected]
111-
with:
112-
path: example/ios/Pods
113-
key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile', 'example/ios/Podfile.lock', 'example/package.json') }}
114-
restore-keys: |
115-
${{ runner.os }}-pods-
116-
117108
- name: Install cocoapods
118-
if: steps.pods-cache.outputs.cache-hit != 'true'
119109
working-directory: example
120110
run: yarn ios:pods
121111

android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.rngooglemapsplus
22

33
import android.annotation.SuppressLint
4+
import android.graphics.Bitmap
45
import android.location.Location
6+
import android.util.Base64
7+
import android.util.Size
58
import android.widget.FrameLayout
9+
import androidx.core.graphics.scale
610
import com.facebook.react.bridge.LifecycleEventListener
711
import com.facebook.react.bridge.UiThreadUtil
812
import com.facebook.react.uimanager.PixelUtil.dpToPx
@@ -29,11 +33,15 @@ import com.google.android.gms.maps.model.PolylineOptions
2933
import com.google.android.gms.maps.model.TileOverlay
3034
import com.google.android.gms.maps.model.TileOverlayOptions
3135
import com.google.maps.android.data.kml.KmlLayer
36+
import com.margelo.nitro.core.Promise
3237
import com.rngooglemapsplus.extensions.toGooglePriority
3338
import com.rngooglemapsplus.extensions.toLocationErrorCode
3439
import com.rngooglemapsplus.extensions.toRNIndoorBuilding
3540
import com.rngooglemapsplus.extensions.toRNIndoorLevel
3641
import java.io.ByteArrayInputStream
42+
import java.io.ByteArrayOutputStream
43+
import java.io.File
44+
import java.io.FileOutputStream
3745
import java.nio.charset.StandardCharsets
3846

3947
class GoogleMapsViewImpl(
@@ -188,6 +196,8 @@ class GoogleMapsViewImpl(
188196
if (cameraPosition == lastSubmittedCameraPosition) {
189197
return
190198
}
199+
lastSubmittedCameraPosition = cameraPosition
200+
191201
val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason
192202

193203
val latDelta = bounds.northeast.latitude - bounds.southwest.latitude
@@ -207,7 +217,6 @@ class GoogleMapsViewImpl(
207217
),
208218
isGesture,
209219
)
210-
lastSubmittedCameraPosition = cameraPosition
211220
}
212221

213222
override fun onCameraIdle() {
@@ -503,7 +512,7 @@ class GoogleMapsViewImpl(
503512
fun setCamera(
504513
cameraPosition: CameraPosition,
505514
animated: Boolean,
506-
durationMS: Int,
515+
durationMs: Int,
507516
) {
508517
onUi {
509518
val current = googleMap?.cameraPosition
@@ -514,7 +523,7 @@ class GoogleMapsViewImpl(
514523
val update = CameraUpdateFactory.newCameraPosition(cameraPosition)
515524

516525
if (animated) {
517-
googleMap?.animateCamera(update, durationMS, null)
526+
googleMap?.animateCamera(update, durationMs, null)
518527
} else {
519528
googleMap?.moveCamera(update)
520529
}
@@ -525,7 +534,7 @@ class GoogleMapsViewImpl(
525534
coordinates: Array<RNLatLng>,
526535
padding: RNMapPadding,
527536
animated: Boolean,
528-
durationMS: Int,
537+
durationMs: Int,
529538
) {
530539
if (coordinates.isEmpty()) {
531540
return
@@ -583,13 +592,85 @@ class GoogleMapsViewImpl(
583592
0,
584593
)
585594
if (animated) {
586-
googleMap?.animateCamera(update, durationMS, null)
595+
googleMap?.animateCamera(update, durationMs, null)
587596
} else {
588597
googleMap?.moveCamera(update)
589598
}
590599
}
591600
}
592601

602+
fun setCameraBounds(bounds: LatLngBounds?) {
603+
onUi {
604+
googleMap?.setLatLngBoundsForCameraTarget(bounds)
605+
}
606+
}
607+
608+
fun animateToBounds(
609+
bounds: LatLngBounds,
610+
padding: Int,
611+
durationMs: Int,
612+
lockBounds: Boolean,
613+
) {
614+
onUi {
615+
if (lockBounds) {
616+
googleMap?.setLatLngBoundsForCameraTarget(bounds)
617+
}
618+
val update =
619+
CameraUpdateFactory.newLatLngBounds(
620+
bounds,
621+
padding,
622+
)
623+
googleMap?.animateCamera(update, durationMs, null)
624+
}
625+
}
626+
627+
fun snapshot(
628+
size: Size?,
629+
format: String,
630+
compressFormat: Bitmap.CompressFormat,
631+
quality: Double,
632+
resultIsFile: Boolean,
633+
): Promise<String?> {
634+
val promise = Promise<String?>()
635+
onUi {
636+
googleMap?.snapshot { bitmap ->
637+
try {
638+
if (bitmap == null) {
639+
promise.resolve(null)
640+
return@snapshot
641+
}
642+
643+
val scaledBitmap =
644+
size?.let {
645+
bitmap.scale(it.width, it.height)
646+
} ?: bitmap
647+
648+
val output = ByteArrayOutputStream()
649+
scaledBitmap.compress(compressFormat, (quality * 100).toInt().coerceIn(0, 100), output)
650+
val bytes = output.toByteArray()
651+
652+
if (resultIsFile) {
653+
val file = File(context.cacheDir, "map_snapshot_${System.currentTimeMillis()}.$format")
654+
FileOutputStream(file).use { it.write(bytes) }
655+
promise.resolve(file.absolutePath)
656+
} else {
657+
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
658+
promise.resolve("data:image/$format;base64,$base64")
659+
}
660+
661+
if (scaledBitmap != bitmap) {
662+
scaledBitmap.recycle()
663+
}
664+
bitmap.recycle()
665+
} catch (e: Exception) {
666+
promise.resolve(null)
667+
}
668+
}
669+
}
670+
671+
return promise
672+
}
673+
593674
fun addMarker(
594675
id: String,
595676
opts: MarkerOptions,

android/src/main/java/com/rngooglemapsplus/LocationHandler.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ class LocationHandler(
131131

132132
private fun restartLocationUpdates() {
133133
stop()
134-
// 4) Google Play Services checken – früh zurückmelden
135134
val playServicesStatus =
136135
GoogleApiAvailability
137136
.getInstance()

android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import com.facebook.react.uimanager.ThemedReactContext
66
import com.google.android.gms.maps.model.MapStyleOptions
77
import com.margelo.nitro.core.Promise
88
import com.rngooglemapsplus.extensions.circleEquals
9+
import com.rngooglemapsplus.extensions.isFileResult
910
import com.rngooglemapsplus.extensions.markerEquals
1011
import com.rngooglemapsplus.extensions.polygonEquals
1112
import com.rngooglemapsplus.extensions.polylineEquals
1213
import com.rngooglemapsplus.extensions.toCameraPosition
14+
import com.rngooglemapsplus.extensions.toCompressFormat
15+
import com.rngooglemapsplus.extensions.toFileExtension
16+
import com.rngooglemapsplus.extensions.toLatLngBounds
1317
import com.rngooglemapsplus.extensions.toMapColorScheme
18+
import com.rngooglemapsplus.extensions.toSize
1419

1520
@DoNotStrip
1621
class RNGoogleMapsPlusView(
@@ -343,25 +348,54 @@ class RNGoogleMapsPlusView(
343348
override fun setCamera(
344349
camera: RNCamera,
345350
animated: Boolean?,
346-
durationMS: Double?,
351+
durationMs: Double?,
347352
) {
348-
view.setCamera(camera.toCameraPosition(), animated == true, durationMS?.toInt() ?: 3000)
353+
view.setCamera(camera.toCameraPosition(), animated == true, durationMs?.toInt() ?: 3000)
349354
}
350355

351356
override fun setCameraToCoordinates(
352357
coordinates: Array<RNLatLng>,
353358
padding: RNMapPadding?,
354359
animated: Boolean?,
355-
durationMS: Double?,
360+
durationMs: Double?,
356361
) {
357362
view.setCameraToCoordinates(
358363
coordinates,
359364
padding = padding ?: RNMapPadding(0.0, 0.0, 0.0, 0.0),
360365
animated == true,
361-
durationMS?.toInt() ?: 3000,
366+
durationMs?.toInt() ?: 3000,
362367
)
363368
}
364369

370+
override fun setCameraBounds(bounds: RNLatLngBounds?) {
371+
view.setCameraBounds(
372+
bounds?.toLatLngBounds(),
373+
)
374+
}
375+
376+
override fun animateToBounds(
377+
bounds: RNLatLngBounds,
378+
padding: Double?,
379+
durationMs: Double?,
380+
lockBounds: Boolean?,
381+
) {
382+
view.animateToBounds(
383+
bounds.toLatLngBounds(),
384+
padding = padding?.toInt() ?: 0,
385+
durationMs?.toInt() ?: 3000,
386+
lockBounds = false,
387+
)
388+
}
389+
390+
override fun snapshot(options: RNSnapshotOptions): Promise<String?> =
391+
view.snapshot(
392+
size = options.size.toSize(),
393+
format = options.format.toFileExtension(),
394+
compressFormat = options.format.toCompressFormat(),
395+
quality = options.quality,
396+
resultIsFile = options.resultType.isFileResult(),
397+
)
398+
365399
override fun showLocationDialog() {
366400
locationHandler.showLocationDialog()
367401
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.rngooglemapsplus.extensions
2+
3+
import com.google.android.gms.maps.model.LatLng
4+
import com.google.android.gms.maps.model.LatLngBounds
5+
import com.rngooglemapsplus.RNLatLngBounds
6+
7+
fun RNLatLngBounds.toLatLngBounds(): LatLngBounds =
8+
LatLngBounds(
9+
LatLng(
10+
southWest.latitude,
11+
southWest.longitude,
12+
),
13+
LatLng(
14+
northEast.latitude,
15+
northEast.longitude,
16+
),
17+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.rngooglemapsplus.extensions
2+
3+
import android.util.Size
4+
import com.facebook.react.uimanager.PixelUtil.dpToPx
5+
import com.rngooglemapsplus.RNSize
6+
7+
fun RNSize?.toSize(): Size? = this?.let { Size(width.dpToPx().toInt(), height.dpToPx().toInt()) }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.rngooglemapsplus.extensions
2+
3+
import android.graphics.Bitmap
4+
import com.rngooglemapsplus.RNSnapshotFormat
5+
6+
fun RNSnapshotFormat?.toCompressFormat(): Bitmap.CompressFormat =
7+
when (this) {
8+
RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> Bitmap.CompressFormat.JPEG
9+
RNSnapshotFormat.PNG, null -> Bitmap.CompressFormat.PNG
10+
}
11+
12+
fun RNSnapshotFormat?.toFileExtension(): String =
13+
when (this) {
14+
RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> "jpg"
15+
RNSnapshotFormat.PNG, null -> "png"
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.rngooglemapsplus.extensions
2+
3+
import com.rngooglemapsplus.RNSnapshotResultType
4+
5+
fun RNSnapshotResultType?.isFileResult(): Boolean =
6+
when (this) {
7+
RNSnapshotResultType.FILE -> true
8+
RNSnapshotResultType.BASE64, null -> false
9+
}

example/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { AppRegistry } from 'react-native';
22
import App from './src/App';
33
import { name as appName } from './app.json';
4+
import { LogBox } from 'react-native';
5+
6+
LogBox.ignoreLogs(['InteractionManager has been deprecated']);
47

58
AppRegistry.registerComponent(appName, () => App);

example/src/App.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
2121
import { useColorScheme } from 'react-native';
2222
import BlankScreen from './screens/BlankScreen';
2323
import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen';
24+
import CameraTestScreen from './screens/CameraTestScreen';
2425
import type { RootStackParamList } from './types/navigation';
26+
import SnapshotTestScreen from './screens/SnaptshotTestScreen';
2527

2628
const Stack = createStackNavigator<RootStackParamList>();
2729

@@ -100,10 +102,20 @@ export default function App() {
100102
component={IndoorLevelMapScreen}
101103
options={{ title: 'Indoor level map' }}
102104
/>
105+
<Stack.Screen
106+
name="Camera"
107+
component={CameraTestScreen}
108+
options={{ title: 'Camera test' }}
109+
/>
110+
<Stack.Screen
111+
name="Snapshot"
112+
component={SnapshotTestScreen}
113+
options={{ title: 'Snapshot test' }}
114+
/>
103115
<Stack.Screen
104116
name="StressTest"
105117
component={StressTestScreen}
106-
options={{ title: 'Stress Test' }}
118+
options={{ title: 'Stress test' }}
107119
/>
108120
</Stack.Navigator>
109121
</NavigationContainer>

0 commit comments

Comments
 (0)