Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
82ddacf
fix(ios): callout content not visible on Apple Maps
lodev09 Mar 13, 2026
2b0b9d0
feat: add Callout component
lodev09 Mar 13, 2026
c07c43f
feat(example): add callout press to event status
lodev09 Mar 16, 2026
030956a
refactor: move callout to Marker props with bubbled support
lodev09 Mar 16, 2026
24d4af3
chore(android): upgrade play-services-maps to 20.0.0
lodev09 Mar 16, 2026
923ac08
fix(android): non-bubbled callout position and flash on show
lodev09 Mar 16, 2026
d685ade
fix(android): non-bubbled callout not intercepting touches
lodev09 Mar 16, 2026
fd8bec9
fix(android): dismiss open info windows when showing non-bubbled callout
lodev09 Mar 16, 2026
c5abf0d
feat: add calloutAnchor prop with content size change repositioning
lodev09 Mar 16, 2026
5a5a0e9
fix(android): replace AdvancedMarker.iconView with manual view positi…
lodev09 Mar 17, 2026
bf7e2a7
fix(ios): reset callout anchor to default when prop is removed
lodev09 Mar 17, 2026
60f6e31
perf(ios): parent non-bubbled callout to annotation view on Apple Maps
lodev09 Mar 17, 2026
7f9d7be
fix(ios): prevent hiding callout when pressed
lodev09 Mar 17, 2026
a30db8b
fix: ios google non-bubbled callouts
lodev09 Mar 17, 2026
7ad0c69
refactor: remove callout press event
lodev09 Mar 17, 2026
7b4bb17
fix: remove empty iOS Google marker snippet
lodev09 Mar 17, 2026
cb926bb
refactor: use callout options struct
lodev09 Mar 17, 2026
86ebad3
fix(web): callout infowindow
lodev09 Mar 17, 2026
608e2e6
fix(web): auto-close other callouts when opening a new one
lodev09 Mar 17, 2026
bac50bf
feat(web): callout improvements and typed GeoJSON properties
lodev09 Mar 17, 2026
c05bc48
fix: callout crash, hit-test dedup, and cleanup improvements
lodev09 Mar 18, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ import { MapView, Marker, Polyline, Polygon } from '@lugg/maps';
## Components

- [MapView](docs/MAPVIEW.md) - Main map component
- [Marker](docs/MARKER.md) - Map markers
- [Marker](docs/MARKER.md) - Map markers with callout support
- [Polyline](docs/POLYLINE.md) - Draw lines on the map
- [Polygon](docs/POLYGON.md) - Draw filled shapes on the map
- [GeoJson](docs/GEOJSON.md) - Render GeoJSON data on the map
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.google.android.gms:play-services-maps:19.2.0"
implementation "com.google.android.gms:play-services-maps:20.0.0"
}
74 changes: 74 additions & 0 deletions android/src/main/java/com/luggmaps/LuggCalloutView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.luggmaps

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.View
import androidx.core.graphics.createBitmap
import androidx.core.view.isNotEmpty
import com.facebook.react.views.view.ReactViewGroup

class LuggCalloutView(context: Context) : ReactViewGroup(context) {
val contentView: ReactViewGroup = ReactViewGroup(context)
var bubbled: Boolean = true
var anchorX: Float = 0.5f
set(value) {
field = value
onUpdate?.invoke()
}
var anchorY: Float = 1.0f
set(value) {
field = value
onUpdate?.invoke()
}
var onUpdate: (() -> Unit)? = null

val hasCustomContent: Boolean
get() = contentView.isNotEmpty()

init {
visibility = GONE
}

override fun addView(child: View, index: Int) {
contentView.addView(child, index)
child.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
onUpdate?.invoke()
}
}

override fun removeView(child: View) {
contentView.removeView(child)
}

override fun removeViewAt(index: Int) {
contentView.removeViewAt(index)
}

override fun getChildCount(): Int = contentView.childCount

override fun getChildAt(index: Int): View? = contentView.getChildAt(index)

fun createContentBitmap(): Bitmap? {
var maxWidth = 0
var maxHeight = 0
for (i in 0 until contentView.childCount) {
val child = contentView.getChildAt(i)
val childRight = child.left + child.width
val childBottom = child.top + child.height
if (childRight > maxWidth) maxWidth = childRight
if (childBottom > maxHeight) maxHeight = childBottom
}

if (maxWidth <= 0 || maxHeight <= 0) return null

val bitmap = createBitmap(maxWidth, maxHeight)
val canvas = Canvas(bitmap)
contentView.draw(canvas)
return bitmap
}

fun onDropViewInstance() {
contentView.removeAllViews()
}
}
38 changes: 38 additions & 0 deletions android/src/main/java/com/luggmaps/LuggCalloutViewManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.luggmaps

import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.LuggCalloutViewManagerDelegate
import com.facebook.react.viewmanagers.LuggCalloutViewManagerInterface

@ReactModule(name = LuggCalloutViewManager.NAME)
class LuggCalloutViewManager :
ViewGroupManager<LuggCalloutView>(),
LuggCalloutViewManagerInterface<LuggCalloutView> {
private val delegate: ViewManagerDelegate<LuggCalloutView> = LuggCalloutViewManagerDelegate(this)

override fun getDelegate(): ViewManagerDelegate<LuggCalloutView> = delegate
override fun getName(): String = NAME
override fun createViewInstance(context: ThemedReactContext): LuggCalloutView = LuggCalloutView(context)

override fun setBubbled(view: LuggCalloutView, value: Boolean) {
view.bubbled = value
}

override fun setAnchor(view: LuggCalloutView, value: ReadableMap?) {
view.anchorX = value?.getDouble("x")?.toFloat() ?: 0.5f
view.anchorY = value?.getDouble("y")?.toFloat() ?: 1.0f
}

override fun onDropViewInstance(view: LuggCalloutView) {
super.onDropViewInstance(view)
view.onDropViewInstance()
}

companion object {
const val NAME = "LuggCalloutView"
}
}
11 changes: 0 additions & 11 deletions android/src/main/java/com/luggmaps/LuggMapWrapperView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
return super.dispatchTouchEvent(event)
}

override fun requestLayout() {
super.requestLayout()
getChildAt(0)?.let {
it.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
it.layout(0, 0, width, height)
}
}

override fun onLayout(
changed: Boolean,
left: Int,
Expand Down
124 changes: 73 additions & 51 deletions android/src/main/java/com/luggmaps/LuggMarkerView.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.luggmaps

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.createBitmap
import androidx.core.view.isNotEmpty
import com.facebook.react.views.view.ReactViewGroup
Expand Down Expand Up @@ -68,15 +66,20 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
private set

val hasCustomView: Boolean
get() = iconView.isNotEmpty()
get() = contentView.isNotEmpty()

val iconView: ReactViewGroup = ReactViewGroup(context)
val contentView: ReactViewGroup = ReactViewGroup(context)

private fun measureIconViewBounds(): Pair<Int, Int> {
var onUpdate: (() -> Unit)? = null

var calloutView: LuggCalloutView? = null
private set

private fun measureContentBounds(): Pair<Int, Int> {
var maxWidth = 0
var maxHeight = 0
for (i in 0 until iconView.childCount) {
val child = iconView.getChildAt(i)
for (i in 0 until contentView.childCount) {
val child = contentView.getChildAt(i)
val childRight = child.left + child.width
val childBottom = child.top + child.height
if (childRight > maxWidth) maxWidth = childRight
Expand All @@ -85,8 +88,8 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
return Pair(maxWidth, maxHeight)
}

private fun createIconBitmap(): BitmapDescriptor? {
val (width, height) = measureIconViewBounds()
private fun createContentBitmap(): BitmapDescriptor? {
val (width, height) = measureContentBounds()
if (width <= 0 || height <= 0) return null

val scaledWidth = (width * scale).toInt()
Expand All @@ -95,41 +98,31 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
val bitmap = createBitmap(scaledWidth, scaledHeight)
val canvas = Canvas(bitmap)
canvas.scale(scale, scale)
iconView.draw(canvas)
contentView.draw(canvas)
return BitmapDescriptorFactory.fromBitmap(bitmap)
}

private fun createIconViewWrapper(): View {
val (width, height) = measureIconViewBounds()
fun layoutContentView() {
val (width, height) = measureContentBounds()
val scaledWidth = (width * scale).toInt()
val scaledHeight = (height * scale).toInt()

(iconView.parent as? ViewGroup)?.removeView(iconView)
iconView.scaleX = scale
iconView.scaleY = scale
iconView.pivotX = 0f
iconView.pivotY = 0f
contentView.scaleX = scale
contentView.scaleY = scale
contentView.pivotX = 0f
contentView.pivotY = 0f

return object : ReactViewGroup(context) {
init {
addView(iconView)
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(scaledWidth, scaledHeight)
}
}
contentView.measure(
View.MeasureSpec.makeMeasureSpec(scaledWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(scaledHeight, View.MeasureSpec.EXACTLY)
)
contentView.layout(0, 0, scaledWidth, scaledHeight)
}

fun applyIconToMarker() {
val m = marker ?: return
if (!hasCustomView) return

if (rasterize) {
createIconBitmap()?.let { m.setIcon(it) }
} else {
m.iconView = createIconViewWrapper()
}
createContentBitmap()?.let { m.setIcon(it) }
}

fun applyScaleToMarker() {
Expand All @@ -139,28 +132,26 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
scaleUpdateRunnable?.let { removeCallbacks(it) }
scaleUpdateRunnable = Runnable {
if (rasterize) {
createIconBitmap()?.let { m.setIcon(it) }
createContentBitmap()?.let { m.setIcon(it) }
} else {
m.iconView = createIconViewWrapper()
layoutContentView()
onUpdate?.invoke()
}
}
post(scaleUpdateRunnable)
}

fun updateIcon(onAddMarker: () -> Unit) {
if (!hasCustomView) return
if (rasterize) {
post {
if (marker == null) {
onAddMarker()
} else {
applyIconToMarker()
}
post {
if (marker == null) {
onAddMarker()
} else if (rasterize) {
applyIconToMarker()
} else {
layoutContentView()
onUpdate?.invoke()
}
} else {
marker?.remove()
marker = null
post { onAddMarker() }
}
}

Expand All @@ -169,23 +160,52 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
}

override fun addView(child: View, index: Int) {
iconView.addView(child, index)
if (child is LuggCalloutView) {
calloutView = child
} else {
contentView.addView(child, index)
}
didLayout = false
}

override fun removeView(child: View) {
iconView.removeView(child)
if (child is LuggCalloutView) {
calloutView = null
} else {
contentView.removeView(child)
}
didLayout = false
}

override fun removeViewAt(index: Int) {
iconView.removeViewAt(index)
val child = getChildAt(index)
if (child is LuggCalloutView) {
calloutView = null
} else {
contentView.removeViewAt(index)
}
didLayout = false
}

override fun getChildCount(): Int = iconView.childCount
override fun removeViews(start: Int, count: Int) {
for (i in (start until start + count).reversed()) {
val child = getChildAt(i)
if (child is LuggCalloutView) {
calloutView = null
} else if (i < contentView.childCount) {
contentView.removeViewAt(i)
}
}
didLayout = false
}

override fun getChildCount(): Int = contentView.childCount + if (calloutView != null) 1 else 0

override fun getChildAt(index: Int): View? = iconView.getChildAt(index)
override fun getChildAt(index: Int): View? {
if (index < contentView.childCount) return contentView.getChildAt(index)
if (index == contentView.childCount && calloutView != null) return calloutView
return null
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
Expand Down Expand Up @@ -277,8 +297,10 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
fun onDropViewInstance() {
scaleUpdateRunnable?.let { removeCallbacks(it) }
scaleUpdateRunnable = null
onUpdate = null
didLayout = false
calloutView = null
delegate = null
iconView.removeAllViews()
contentView.removeAllViews()
}
}
9 changes: 8 additions & 1 deletion android/src/main/java/com/luggmaps/LuggPackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@ import com.facebook.react.uimanager.ViewManager

class LuggPackage : ReactPackage {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
listOf(LuggMapViewManager(), LuggMarkerViewManager(), LuggMapWrapperViewManager(), LuggPolylineViewManager(), LuggPolygonViewManager())
listOf(
LuggMapViewManager(),
LuggMarkerViewManager(),
LuggCalloutViewManager(),
LuggMapWrapperViewManager(),
LuggPolylineViewManager(),
LuggPolygonViewManager()
)
}
Loading
Loading