Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e5240d6
fix(android): proguard-rules.pro
pinpong Oct 21, 2025
cc31a7b
chore(example): add svg marker examples
pinpong Oct 22, 2025
25517f1
fix(android): update location config
pinpong Oct 22, 2025
bdbed0a
chore(example): enable hardwareAccelerated
pinpong Oct 22, 2025
844c652
chore: updated dependencies
pinpong Oct 23, 2025
e43bb64
chore: downgrade react version
pinpong Oct 23, 2025
4bd8e47
feat: add onMapLoaded callback
pinpong Oct 23, 2025
b9aaa20
feat: add onMapLongPress
pinpong Oct 23, 2025
f4badbc
feat: add url tile overlay
pinpong Oct 24, 2025
35544ff
feat(android): add marker icon remote image support
pinpong Oct 25, 2025
875e50c
feat: add onPoiPress
pinpong Oct 25, 2025
d2e0909
feat: add onInfoWindowPress
pinpong Oct 25, 2025
092993f
refactor: align RNRegion with native SDK behavior
pinpong Oct 25, 2025
7019a86
fix: threading issues
pinpong Oct 26, 2025
584dcea
fix(ios): heatmap gradient colorMapSize
pinpong Oct 26, 2025
b7c259a
fix(android): clear tile cache
pinpong Oct 26, 2025
fdd27c2
fix(example): types
pinpong Oct 26, 2025
bcc81c7
fix(heatmap): colorMapSize
pinpong Oct 26, 2025
538c2c8
chore: updated package.json
pinpong Oct 26, 2025
962ac52
fix(example): config dialog
pinpong Oct 26, 2025
7e0a4f1
feat: emit region and camera from onMapLoaded event
pinpong Oct 26, 2025
3c274a1
feat: add consumeOnMarkerPress and consumeOnMyLocationButtonPress to …
pinpong Oct 26, 2025
66fb740
feat: add custom info window support
pinpong Oct 27, 2025
0e1ef8b
fix(example): import
pinpong Oct 27, 2025
541a4e5
refactor: cleanup
pinpong Oct 27, 2025
302d359
chore: sync code parity between Android and iOS
pinpong Oct 27, 2025
9fab0f8
Merge branch 'main' into dev
pinpong Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
923 changes: 444 additions & 479 deletions android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions android/src/main/java/com/rngooglemapsplus/LocationHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class LocationHandler(
private var priority: Int = PRIORITY_DEFAULT
private var interval: Long = INTERVAL_DEFAULT
private var minUpdateInterval: Long = MIN_UPDATE_INTERVAL
private var lastSubmittedLocation: Location? = null
private var isActive = false

var onUpdate: ((Location) -> Unit)? = null
Expand All @@ -57,6 +56,8 @@ class LocationHandler(
this.interval = interval ?: INTERVAL_DEFAULT
this.minUpdateInterval = minUpdateInterval ?: MIN_UPDATE_INTERVAL
buildLocationRequest(this.priority, this.interval, this.minUpdateInterval)
stop()
start()
}

fun showLocationDialog() {
Expand Down Expand Up @@ -143,9 +144,8 @@ class LocationHandler(
fusedLocationClientProviderClient.lastLocation
.addOnSuccessListener(
OnSuccessListener { location ->
if (location != null && location != lastSubmittedLocation) {
if (location != null) {
onUpdate?.invoke(location)
lastSubmittedLocation = location
}
},
).addOnFailureListener { e ->
Expand All @@ -157,11 +157,8 @@ class LocationHandler(
override fun onLocationResult(locationResult: LocationResult) {
val location = locationResult.lastLocation
if (location != null) {
if (location != lastSubmittedLocation) {
lastSubmittedLocation = location
listener?.onLocationChanged(location)
onUpdate?.invoke(location)
}
listener?.onLocationChanged(location)
onUpdate?.invoke(location)
} else {
onError?.invoke(RNLocationErrorCode.POSITION_UNAVAILABLE)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.graphics.Color
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.google.android.gms.maps.model.Circle
import com.google.android.gms.maps.model.CircleOptions
import com.rngooglemapsplus.extensions.onUi
import com.rngooglemapsplus.extensions.toColor
import com.rngooglemapsplus.extensions.toLatLng

Expand All @@ -23,7 +24,7 @@ class MapCircleBuilder {
prev: RNCircle,
next: RNCircle,
circle: Circle,
) {
) = onUi {
if (prev.center.latitude != next.center.latitude ||
prev.center.longitude != next.center.longitude
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class MapHeatmapBuilder {
heatmap.gradient?.let {
val colors = it.colors.map { c -> c.toColor() }.toIntArray()
val startPoints = it.startPoints.map { p -> p.toFloat() }.toFloatArray()
gradient(Gradient(colors, startPoints))
gradient(Gradient(colors, startPoints, it.colorMapSize.toInt()))
}
}.build()

Expand Down
22 changes: 22 additions & 0 deletions android/src/main/java/com/rngooglemapsplus/MapHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rngooglemapsplus.extensions

import com.facebook.react.bridge.UiThreadUtil
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking

inline fun onUi(crossinline block: () -> Unit) {
if (UiThreadUtil.isOnUiThread()) {
block()
} else {
UiThreadUtil.runOnUiThread { block() }
}
}

inline fun <T> onUiSync(crossinline block: () -> T): T {
if (UiThreadUtil.isOnUiThread()) return block()
val result = CompletableDeferred<T>()
UiThreadUtil.runOnUiThread {
runCatching(block).onSuccess(result::complete).onFailure(result::completeExceptionally)
}
return runBlocking { result.await() }
}
144 changes: 142 additions & 2 deletions android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package com.rngooglemapsplus

import MarkerTag
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Typeface
import android.graphics.drawable.PictureDrawable
import android.util.Base64
import android.util.LruCache
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.graphics.createBitmap
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGExternalFileResolver
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.ThemedReactContext
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.rngooglemapsplus.extensions.markerStyleEquals
import com.rngooglemapsplus.extensions.onUi
import com.rngooglemapsplus.extensions.styleHash
import com.rngooglemapsplus.extensions.toLatLng
import kotlinx.coroutines.CoroutineScope
Expand All @@ -20,9 +30,14 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLDecoder
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.coroutineContext

class MapMarkerBuilder(
val context: ThemedReactContext,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
) {
private val iconCache =
Expand All @@ -33,7 +48,103 @@ class MapMarkerBuilder(
): Int = 1
}

private val jobsById = mutableMapOf<String, Job>()
private val jobsById = ConcurrentHashMap<String, Job>()

init {
// / TODO: refactor with androidsvg 1.5 release
SVG.registerExternalFileResolver(
object : SVGExternalFileResolver() {
override fun resolveImage(filename: String?): Bitmap? {
if (filename.isNullOrBlank()) return null

return runCatching {
when {
filename.startsWith("data:image/svg+xml") -> {
val svgContent =
if ("base64," in filename) {
val base64 = filename.substringAfter("base64,")
String(Base64.decode(base64, Base64.DEFAULT), Charsets.UTF_8)
} else {
URLDecoder.decode(filename.substringAfter(","), "UTF-8")
}

val svg = SVG.getFromString(svgContent)
val width = (svg.documentWidth.takeIf { it > 0 } ?: 128f).toInt()
val height = (svg.documentHeight.takeIf { it > 0 } ?: 128f).toInt()

createBitmap(width, height).apply {
Canvas(this).also(svg::renderToCanvas)
}
}

filename.startsWith("http://") || filename.startsWith("https://") -> {
val conn =
(URL(filename).openConnection() as HttpURLConnection).apply {
connectTimeout = 5000
readTimeout = 5000
requestMethod = "GET"
instanceFollowRedirects = true
}
conn.connect()

val contentType = conn.contentType ?: ""
val result =
if (contentType.contains("svg") || filename.endsWith(".svg")) {
val svgText = conn.inputStream.bufferedReader().use { it.readText() }
val innerSvg = SVG.getFromString(svgText)
val w = innerSvg.documentWidth.takeIf { it > 0 } ?: 128f
val h = innerSvg.documentHeight.takeIf { it > 0 } ?: 128f
val bmp = createBitmap(w.toInt(), h.toInt())
val canvas = Canvas(bmp)
innerSvg.renderToCanvas(canvas)
bmp
} else {
conn.inputStream.use { BitmapFactory.decodeStream(it) }
}

conn.disconnect()
result
}

else -> null
}
}.getOrNull()
}

override fun resolveFont(
fontFamily: String?,
fontWeight: Int,
fontStyle: String?,
): Typeface? {
if (fontFamily.isNullOrBlank()) return null

return runCatching {
val assetManager = context.assets

val candidates =
listOf(
"fonts/$fontFamily.ttf",
"fonts/$fontFamily.otf",
)

for (path in candidates) {
try {
return Typeface.createFromAsset(assetManager, path)
} catch (_: Throwable) {
// / ignore
}
}

Typeface.create(fontFamily, Typeface.NORMAL)
}.getOrElse {
Typeface.create(fontFamily, fontWeight)
}
}

override fun isFormatSupported(mimeType: String?): Boolean = mimeType?.startsWith("image/") == true
},
)
}

fun build(
m: RNMarker,
Expand All @@ -57,7 +168,7 @@ class MapMarkerBuilder(
prev: RNMarker,
next: RNMarker,
marker: Marker,
) {
) = onUi {
if (prev.coordinate.latitude != next.coordinate.latitude ||
prev.coordinate.longitude != next.coordinate.longitude
) {
Expand Down Expand Up @@ -132,6 +243,10 @@ class MapMarkerBuilder(
if (prev.zIndex != next.zIndex) {
marker.zIndex = next.zIndex?.toFloat() ?: 0f
}

if (prev.infoWindowIconSvg != next.infoWindowIconSvg) {
marker.tag = MarkerTag(id = next.id, iconSvg = next.infoWindowIconSvg)
}
}

fun buildIconAsync(
Expand Down Expand Up @@ -189,6 +304,31 @@ class MapMarkerBuilder(
iconCache.evictAll()
}

fun buildInfoWindow(iconSvg: RNMarkerSvg?): ImageView? {
val iconSvg = iconSvg ?: return null

val svgView =
ImageView(context).apply {
layoutParams =
LinearLayout.LayoutParams(
iconSvg.width.dpToPx().toInt(),
iconSvg.height.dpToPx().toInt(),
)
}

try {
val svg = SVG.getFromString(iconSvg.svgString)
svg.setDocumentWidth(iconSvg.width.dpToPx())
svg.setDocumentHeight(iconSvg.height.dpToPx())
val drawable = PictureDrawable(svg.renderToPicture())
svgView.setImageDrawable(drawable)
} catch (e: Exception) {
return null
}

return svgView
}

private suspend fun renderBitmap(m: RNMarker): Bitmap? {
m.iconSvg ?: return null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.graphics.Color
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.google.android.gms.maps.model.Polygon
import com.google.android.gms.maps.model.PolygonOptions
import com.rngooglemapsplus.extensions.onUi
import com.rngooglemapsplus.extensions.toColor
import com.rngooglemapsplus.extensions.toLatLng

Expand All @@ -30,7 +31,7 @@ class MapPolygonBuilder {
prev: RNPolygon,
next: RNPolygon,
poly: Polygon,
) {
) = onUi {
val coordsChanged =
prev.coordinates.size != next.coordinates.size ||
!prev.coordinates.zip(next.coordinates).all { (a, b) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.google.android.gms.maps.model.Polyline
import com.google.android.gms.maps.model.PolylineOptions
import com.google.android.gms.maps.model.RoundCap
import com.google.android.gms.maps.model.SquareCap
import com.rngooglemapsplus.extensions.onUi
import com.rngooglemapsplus.extensions.toColor
import com.rngooglemapsplus.extensions.toLatLng

Expand All @@ -34,7 +35,7 @@ class MapPolylineBuilder {
prev: RNPolyline,
next: RNPolyline,
polyline: Polyline,
) {
) = onUi {
val coordsChanged =
prev.coordinates.size != next.coordinates.size ||
!prev.coordinates.zip(next.coordinates).all { (a, b) ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.rngooglemapsplus

import com.google.android.gms.maps.model.TileOverlayOptions
import com.google.android.gms.maps.model.UrlTileProvider
import java.net.URL

class MapUrlTileOverlayBuilder {
fun build(t: RNUrlTileOverlay): TileOverlayOptions {
val provider =
object : UrlTileProvider(
t.tileSize.toInt(),
t.tileSize.toInt(),
) {
override fun getTileUrl(
x: Int,
y: Int,
zoom: Int,
): URL? {
val url =
t.url
.replace("{x}", x.toString())
.replace("{y}", y.toString())
.replace("{z}", zoom.toString())

return try {
URL(url)
} catch (e: Exception) {
null
}
}
}

val opts = TileOverlayOptions().tileProvider(provider)

t.fadeIn?.let { opts.fadeIn(it) }
t.zIndex?.let { opts.zIndex(it.toFloat()) }
t.opacity?.let { opts.transparency(1f - it.toFloat()) }
return opts
}
}
Loading
Loading