@lugg/maps 1.0.0-beta.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +74 -0
  4. package/android/src/main/java/com/luggmaps/LuggCalloutViewManager.kt +38 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +0 -11
  6. package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +73 -51
  7. package/android/src/main/java/com/luggmaps/LuggPackage.kt +8 -1
  8. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +181 -5
  9. package/ios/LuggCalloutView.h +23 -0
  10. package/ios/LuggCalloutView.mm +145 -0
  11. package/ios/LuggMarkerView.h +2 -0
  12. package/ios/LuggMarkerView.mm +17 -2
  13. package/ios/core/AppleMapProvider.mm +211 -24
  14. package/ios/core/GoogleMapProvider.mm +125 -5
  15. package/ios/core/LuggAnnotationView.h +12 -0
  16. package/ios/core/LuggAnnotationView.mm +55 -0
  17. package/lib/module/MapProvider.web.js +3 -1
  18. package/lib/module/MapProvider.web.js.map +1 -1
  19. package/lib/module/MapView.web.js +15 -2
  20. package/lib/module/MapView.web.js.map +1 -1
  21. package/lib/module/components/Marker.js +21 -4
  22. package/lib/module/components/Marker.js.map +1 -1
  23. package/lib/module/components/Marker.web.js +86 -22
  24. package/lib/module/components/Marker.web.js.map +1 -1
  25. package/lib/module/components/index.js.map +1 -1
  26. package/lib/module/fabric/LuggCalloutViewNativeComponent.ts +20 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/typescript/src/MapProvider.web.d.ts +6 -0
  29. package/lib/typescript/src/MapProvider.web.d.ts.map +1 -1
  30. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  31. package/lib/typescript/src/components/GeoJson.types.d.ts +11 -1
  32. package/lib/typescript/src/components/GeoJson.types.d.ts.map +1 -1
  33. package/lib/typescript/src/components/Marker.d.ts +1 -1
  34. package/lib/typescript/src/components/Marker.d.ts.map +1 -1
  35. package/lib/typescript/src/components/Marker.types.d.ts +27 -1
  36. package/lib/typescript/src/components/Marker.types.d.ts.map +1 -1
  37. package/lib/typescript/src/components/Marker.web.d.ts +1 -1
  38. package/lib/typescript/src/components/Marker.web.d.ts.map +1 -1
  39. package/lib/typescript/src/components/index.d.ts +1 -1
  40. package/lib/typescript/src/components/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/index.web.d.ts +1 -1
  42. package/lib/typescript/src/components/index.web.d.ts.map +1 -1
  43. package/lib/typescript/src/fabric/LuggCalloutViewNativeComponent.d.ts +13 -0
  44. package/lib/typescript/src/fabric/LuggCalloutViewNativeComponent.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +2 -2
  46. package/lib/typescript/src/index.d.ts.map +1 -1
  47. package/lib/typescript/src/index.web.d.ts +2 -2
  48. package/lib/typescript/src/index.web.d.ts.map +1 -1
  49. package/package.json +2 -1
  50. package/src/MapProvider.web.tsx +11 -1
  51. package/src/MapView.web.tsx +26 -2
  52. package/src/components/GeoJson.types.ts +12 -1
  53. package/src/components/Marker.tsx +29 -1
  54. package/src/components/Marker.types.ts +28 -1
  55. package/src/components/Marker.web.tsx +113 -23
  56. package/src/components/index.ts +1 -0
  57. package/src/components/index.web.ts +1 -1
  58. package/src/fabric/LuggCalloutViewNativeComponent.ts +20 -0
  59. package/src/index.ts +2 -0
  60. package/src/index.web.ts +2 -1
package/README.md CHANGED
@@ -113,7 +113,7 @@ import { MapView, Marker, Polyline, Polygon } from '@lugg/maps';
113
113
  ## Components
114
114
 
115
115
  - [MapView](docs/MAPVIEW.md) - Main map component
116
- - [Marker](docs/MARKER.md) - Map markers
116
+ - [Marker](docs/MARKER.md) - Map markers with callout support
117
117
  - [Polyline](docs/POLYLINE.md) - Draw lines on the map
118
118
  - [Polygon](docs/POLYGON.md) - Draw filled shapes on the map
119
119
  - [GeoJson](docs/GEOJSON.md) - Render GeoJSON data on the map
@@ -74,5 +74,5 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
74
74
  dependencies {
75
75
  implementation "com.facebook.react:react-android"
76
76
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
- implementation "com.google.android.gms:play-services-maps:19.2.0"
77
+ implementation "com.google.android.gms:play-services-maps:20.0.0"
78
78
  }
@@ -0,0 +1,74 @@
1
+ package com.luggmaps
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.view.View
7
+ import androidx.core.graphics.createBitmap
8
+ import androidx.core.view.isNotEmpty
9
+ import com.facebook.react.views.view.ReactViewGroup
10
+
11
+ class LuggCalloutView(context: Context) : ReactViewGroup(context) {
12
+ val contentView: ReactViewGroup = ReactViewGroup(context)
13
+ var bubbled: Boolean = true
14
+ var anchorX: Float = 0.5f
15
+ set(value) {
16
+ field = value
17
+ onUpdate?.invoke()
18
+ }
19
+ var anchorY: Float = 1.0f
20
+ set(value) {
21
+ field = value
22
+ onUpdate?.invoke()
23
+ }
24
+ var onUpdate: (() -> Unit)? = null
25
+
26
+ val hasCustomContent: Boolean
27
+ get() = contentView.isNotEmpty()
28
+
29
+ init {
30
+ visibility = GONE
31
+ }
32
+
33
+ override fun addView(child: View, index: Int) {
34
+ contentView.addView(child, index)
35
+ child.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
36
+ onUpdate?.invoke()
37
+ }
38
+ }
39
+
40
+ override fun removeView(child: View) {
41
+ contentView.removeView(child)
42
+ }
43
+
44
+ override fun removeViewAt(index: Int) {
45
+ contentView.removeViewAt(index)
46
+ }
47
+
48
+ override fun getChildCount(): Int = contentView.childCount
49
+
50
+ override fun getChildAt(index: Int): View? = contentView.getChildAt(index)
51
+
52
+ fun createContentBitmap(): Bitmap? {
53
+ var maxWidth = 0
54
+ var maxHeight = 0
55
+ for (i in 0 until contentView.childCount) {
56
+ val child = contentView.getChildAt(i)
57
+ val childRight = child.left + child.width
58
+ val childBottom = child.top + child.height
59
+ if (childRight > maxWidth) maxWidth = childRight
60
+ if (childBottom > maxHeight) maxHeight = childBottom
61
+ }
62
+
63
+ if (maxWidth <= 0 || maxHeight <= 0) return null
64
+
65
+ val bitmap = createBitmap(maxWidth, maxHeight)
66
+ val canvas = Canvas(bitmap)
67
+ contentView.draw(canvas)
68
+ return bitmap
69
+ }
70
+
71
+ fun onDropViewInstance() {
72
+ contentView.removeAllViews()
73
+ }
74
+ }
@@ -0,0 +1,38 @@
1
+ package com.luggmaps
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.uimanager.ViewManagerDelegate
8
+ import com.facebook.react.viewmanagers.LuggCalloutViewManagerDelegate
9
+ import com.facebook.react.viewmanagers.LuggCalloutViewManagerInterface
10
+
11
+ @ReactModule(name = LuggCalloutViewManager.NAME)
12
+ class LuggCalloutViewManager :
13
+ ViewGroupManager<LuggCalloutView>(),
14
+ LuggCalloutViewManagerInterface<LuggCalloutView> {
15
+ private val delegate: ViewManagerDelegate<LuggCalloutView> = LuggCalloutViewManagerDelegate(this)
16
+
17
+ override fun getDelegate(): ViewManagerDelegate<LuggCalloutView> = delegate
18
+ override fun getName(): String = NAME
19
+ override fun createViewInstance(context: ThemedReactContext): LuggCalloutView = LuggCalloutView(context)
20
+
21
+ override fun setBubbled(view: LuggCalloutView, value: Boolean) {
22
+ view.bubbled = value
23
+ }
24
+
25
+ override fun setAnchor(view: LuggCalloutView, value: ReadableMap?) {
26
+ view.anchorX = value?.getDouble("x")?.toFloat() ?: 0.5f
27
+ view.anchorY = value?.getDouble("y")?.toFloat() ?: 1.0f
28
+ }
29
+
30
+ override fun onDropViewInstance(view: LuggCalloutView) {
31
+ super.onDropViewInstance(view)
32
+ view.onDropViewInstance()
33
+ }
34
+
35
+ companion object {
36
+ const val NAME = "LuggCalloutView"
37
+ }
38
+ }
@@ -15,17 +15,6 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
15
15
  return super.dispatchTouchEvent(event)
16
16
  }
17
17
 
18
- override fun requestLayout() {
19
- super.requestLayout()
20
- getChildAt(0)?.let {
21
- it.measure(
22
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
23
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
24
- )
25
- it.layout(0, 0, width, height)
26
- }
27
- }
28
-
29
18
  override fun onLayout(
30
19
  changed: Boolean,
31
20
  left: Int,
@@ -1,10 +1,8 @@
1
1
  package com.luggmaps
2
2
 
3
3
  import android.content.Context
4
- import android.graphics.Bitmap
5
4
  import android.graphics.Canvas
6
5
  import android.view.View
7
- import android.view.ViewGroup
8
6
  import androidx.core.graphics.createBitmap
9
7
  import androidx.core.view.isNotEmpty
10
8
  import com.facebook.react.views.view.ReactViewGroup
@@ -68,15 +66,20 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
68
66
  private set
69
67
 
70
68
  val hasCustomView: Boolean
71
- get() = iconView.isNotEmpty()
69
+ get() = contentView.isNotEmpty()
72
70
 
73
- val iconView: ReactViewGroup = ReactViewGroup(context)
71
+ val contentView: ReactViewGroup = ReactViewGroup(context)
74
72
 
75
- private fun measureIconViewBounds(): Pair<Int, Int> {
73
+ var onUpdate: (() -> Unit)? = null
74
+
75
+ var calloutView: LuggCalloutView? = null
76
+ private set
77
+
78
+ private fun measureContentBounds(): Pair<Int, Int> {
76
79
  var maxWidth = 0
77
80
  var maxHeight = 0
78
- for (i in 0 until iconView.childCount) {
79
- val child = iconView.getChildAt(i)
81
+ for (i in 0 until contentView.childCount) {
82
+ val child = contentView.getChildAt(i)
80
83
  val childRight = child.left + child.width
81
84
  val childBottom = child.top + child.height
82
85
  if (childRight > maxWidth) maxWidth = childRight
@@ -85,8 +88,8 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
85
88
  return Pair(maxWidth, maxHeight)
86
89
  }
87
90
 
88
- private fun createIconBitmap(): BitmapDescriptor? {
89
- val (width, height) = measureIconViewBounds()
91
+ private fun createContentBitmap(): BitmapDescriptor? {
92
+ val (width, height) = measureContentBounds()
90
93
  if (width <= 0 || height <= 0) return null
91
94
 
92
95
  val scaledWidth = (width * scale).toInt()
@@ -95,41 +98,31 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
95
98
  val bitmap = createBitmap(scaledWidth, scaledHeight)
96
99
  val canvas = Canvas(bitmap)
97
100
  canvas.scale(scale, scale)
98
- iconView.draw(canvas)
101
+ contentView.draw(canvas)
99
102
  return BitmapDescriptorFactory.fromBitmap(bitmap)
100
103
  }
101
104
 
102
- private fun createIconViewWrapper(): View {
103
- val (width, height) = measureIconViewBounds()
105
+ fun layoutContentView() {
106
+ val (width, height) = measureContentBounds()
104
107
  val scaledWidth = (width * scale).toInt()
105
108
  val scaledHeight = (height * scale).toInt()
106
109
 
107
- (iconView.parent as? ViewGroup)?.removeView(iconView)
108
- iconView.scaleX = scale
109
- iconView.scaleY = scale
110
- iconView.pivotX = 0f
111
- iconView.pivotY = 0f
110
+ contentView.scaleX = scale
111
+ contentView.scaleY = scale
112
+ contentView.pivotX = 0f
113
+ contentView.pivotY = 0f
112
114
 
113
- return object : ReactViewGroup(context) {
114
- init {
115
- addView(iconView)
116
- }
117
-
118
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
119
- setMeasuredDimension(scaledWidth, scaledHeight)
120
- }
121
- }
115
+ contentView.measure(
116
+ View.MeasureSpec.makeMeasureSpec(scaledWidth, View.MeasureSpec.EXACTLY),
117
+ View.MeasureSpec.makeMeasureSpec(scaledHeight, View.MeasureSpec.EXACTLY)
118
+ )
119
+ contentView.layout(0, 0, scaledWidth, scaledHeight)
122
120
  }
123
121
 
124
122
  fun applyIconToMarker() {
125
123
  val m = marker ?: return
126
124
  if (!hasCustomView) return
127
-
128
- if (rasterize) {
129
- createIconBitmap()?.let { m.setIcon(it) }
130
- } else {
131
- m.iconView = createIconViewWrapper()
132
- }
125
+ createContentBitmap()?.let { m.setIcon(it) }
133
126
  }
134
127
 
135
128
  fun applyScaleToMarker() {
@@ -139,9 +132,10 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
139
132
  scaleUpdateRunnable?.let { removeCallbacks(it) }
140
133
  scaleUpdateRunnable = Runnable {
141
134
  if (rasterize) {
142
- createIconBitmap()?.let { m.setIcon(it) }
135
+ createContentBitmap()?.let { m.setIcon(it) }
143
136
  } else {
144
- m.iconView = createIconViewWrapper()
137
+ layoutContentView()
138
+ onUpdate?.invoke()
145
139
  }
146
140
  }
147
141
  post(scaleUpdateRunnable)
@@ -149,18 +143,15 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
149
143
 
150
144
  fun updateIcon(onAddMarker: () -> Unit) {
151
145
  if (!hasCustomView) return
152
- if (rasterize) {
153
- post {
154
- if (marker == null) {
155
- onAddMarker()
156
- } else {
157
- applyIconToMarker()
158
- }
146
+ post {
147
+ if (marker == null) {
148
+ onAddMarker()
149
+ } else if (rasterize) {
150
+ applyIconToMarker()
151
+ } else {
152
+ layoutContentView()
153
+ onUpdate?.invoke()
159
154
  }
160
- } else {
161
- marker?.remove()
162
- marker = null
163
- post { onAddMarker() }
164
155
  }
165
156
  }
166
157
 
@@ -169,23 +160,52 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
169
160
  }
170
161
 
171
162
  override fun addView(child: View, index: Int) {
172
- iconView.addView(child, index)
163
+ if (child is LuggCalloutView) {
164
+ calloutView = child
165
+ } else {
166
+ contentView.addView(child, index)
167
+ }
173
168
  didLayout = false
174
169
  }
175
170
 
176
171
  override fun removeView(child: View) {
177
- iconView.removeView(child)
172
+ if (child is LuggCalloutView) {
173
+ calloutView = null
174
+ } else {
175
+ contentView.removeView(child)
176
+ }
178
177
  didLayout = false
179
178
  }
180
179
 
181
180
  override fun removeViewAt(index: Int) {
182
- iconView.removeViewAt(index)
181
+ val child = getChildAt(index)
182
+ if (child is LuggCalloutView) {
183
+ calloutView = null
184
+ } else {
185
+ contentView.removeViewAt(index)
186
+ }
183
187
  didLayout = false
184
188
  }
185
189
 
186
- override fun getChildCount(): Int = iconView.childCount
190
+ override fun removeViews(start: Int, count: Int) {
191
+ for (i in (start until start + count).reversed()) {
192
+ val child = getChildAt(i)
193
+ if (child is LuggCalloutView) {
194
+ calloutView = null
195
+ } else if (i < contentView.childCount) {
196
+ contentView.removeViewAt(i)
197
+ }
198
+ }
199
+ didLayout = false
200
+ }
201
+
202
+ override fun getChildCount(): Int = contentView.childCount + if (calloutView != null) 1 else 0
187
203
 
188
- override fun getChildAt(index: Int): View? = iconView.getChildAt(index)
204
+ override fun getChildAt(index: Int): View? {
205
+ if (index < contentView.childCount) return contentView.getChildAt(index)
206
+ if (index == contentView.childCount && calloutView != null) return calloutView
207
+ return null
208
+ }
189
209
 
190
210
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
191
211
  super.onSizeChanged(w, h, oldw, oldh)
@@ -277,8 +297,10 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
277
297
  fun onDropViewInstance() {
278
298
  scaleUpdateRunnable?.let { removeCallbacks(it) }
279
299
  scaleUpdateRunnable = null
300
+ onUpdate = null
280
301
  didLayout = false
302
+ calloutView = null
281
303
  delegate = null
282
- iconView.removeAllViews()
304
+ contentView.removeAllViews()
283
305
  }
284
306
  }
@@ -6,5 +6,12 @@ import com.facebook.react.uimanager.ViewManager
6
6
 
7
7
  class LuggPackage : ReactPackage {
8
8
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
9
- listOf(LuggMapViewManager(), LuggMarkerViewManager(), LuggMapWrapperViewManager(), LuggPolylineViewManager(), LuggPolygonViewManager())
9
+ listOf(
10
+ LuggMapViewManager(),
11
+ LuggMarkerViewManager(),
12
+ LuggCalloutViewManager(),
13
+ LuggMapWrapperViewManager(),
14
+ LuggPolylineViewManager(),
15
+ LuggPolygonViewManager()
16
+ )
10
17
  }
@@ -3,6 +3,8 @@ package com.luggmaps.core
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.view.View
6
+ import android.widget.ImageView
7
+ import androidx.core.graphics.createBitmap
6
8
  import com.facebook.react.uimanager.PixelUtil.dpToPx
7
9
  import com.google.android.gms.maps.CameraUpdateFactory
8
10
  import com.google.android.gms.maps.GoogleMap
@@ -11,12 +13,14 @@ import com.google.android.gms.maps.MapView
11
13
  import com.google.android.gms.maps.OnMapReadyCallback
12
14
  import com.google.android.gms.maps.model.AdvancedMarker
13
15
  import com.google.android.gms.maps.model.AdvancedMarkerOptions
16
+ import com.google.android.gms.maps.model.BitmapDescriptorFactory
14
17
  import com.google.android.gms.maps.model.LatLng
15
18
  import com.google.android.gms.maps.model.MapColorScheme
16
19
  import com.google.android.gms.maps.model.Marker
17
20
  import com.google.android.gms.maps.model.Polygon
18
21
  import com.google.android.gms.maps.model.PolygonOptions
19
22
  import com.google.android.gms.maps.model.PolylineOptions
23
+ import com.luggmaps.LuggCalloutView
20
24
  import com.luggmaps.LuggMapWrapperView
21
25
  import com.luggmaps.LuggMarkerView
22
26
  import com.luggmaps.LuggMarkerViewDelegate
@@ -38,7 +42,8 @@ class GoogleMapProvider(private val context: Context) :
38
42
  GoogleMap.OnMapLongClickListener,
39
43
  GoogleMap.OnPolygonClickListener,
40
44
  GoogleMap.OnMarkerClickListener,
41
- GoogleMap.OnMarkerDragListener {
45
+ GoogleMap.OnMarkerDragListener,
46
+ GoogleMap.InfoWindowAdapter {
42
47
 
43
48
  override var delegate: MapProviderDelegate? = null
44
49
  override val isMapReady: Boolean get() = _isMapReady
@@ -56,6 +61,8 @@ class GoogleMapProvider(private val context: Context) :
56
61
  private val polylineAnimators = mutableMapOf<LuggPolylineView, PolylineAnimator>()
57
62
  private val polygonToViewMap = mutableMapOf<Polygon, LuggPolygonView>()
58
63
  private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
64
+ private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
65
+ private var activeNonBubbledMarker: Marker? = null
59
66
  private var tapLocation: LatLng? = null
60
67
 
61
68
  // Initial camera settings
@@ -103,6 +110,11 @@ class GoogleMapProvider(private val context: Context) :
103
110
  }
104
111
 
105
112
  override fun destroy() {
113
+ dismissNonBubbledCallout()
114
+ for (markerView in liveMarkerViews) {
115
+ markerView.onUpdate = null
116
+ }
117
+ liveMarkerViews.clear()
106
118
  pendingMarkerViews.clear()
107
119
  pendingPolylineViews.clear()
108
120
  pendingPolygonViews.clear()
@@ -120,6 +132,7 @@ class GoogleMapProvider(private val context: Context) :
120
132
  googleMap?.setOnPolygonClickListener(null)
121
133
  googleMap?.setOnMarkerClickListener(null)
122
134
  googleMap?.setOnMarkerDragListener(null)
135
+ googleMap?.setInfoWindowAdapter(null)
123
136
  googleMap?.clear()
124
137
  googleMap = null
125
138
  _isMapReady = false
@@ -143,6 +156,7 @@ class GoogleMapProvider(private val context: Context) :
143
156
  map.setOnPolygonClickListener(this)
144
157
  map.setOnMarkerClickListener(this)
145
158
  map.setOnMarkerDragListener(this)
159
+ map.setInfoWindowAdapter(this)
146
160
 
147
161
  wrapperView?.touchEventHandler = { event ->
148
162
  if (event.action == android.view.MotionEvent.ACTION_DOWN) {
@@ -177,6 +191,8 @@ class GoogleMapProvider(private val context: Context) :
177
191
  val map = googleMap ?: return
178
192
  val position = map.cameraPosition
179
193
  delegate?.mapProviderDidMoveCamera(position.target.latitude, position.target.longitude, position.zoom, isDragging)
194
+ positionLiveMarkers()
195
+ positionNonBubbledCallout()
180
196
  }
181
197
 
182
198
  override fun onCameraIdle() {
@@ -190,6 +206,7 @@ class GoogleMapProvider(private val context: Context) :
190
206
  }
191
207
 
192
208
  override fun onMapClick(latLng: LatLng) {
209
+ dismissNonBubbledCallout()
193
210
  val map = googleMap ?: return
194
211
  val point = map.projection.toScreenLocation(latLng)
195
212
  delegate?.mapProviderDidPress(latLng.latitude, latLng.longitude, point.x.toFloat(), point.y.toFloat())
@@ -211,9 +228,18 @@ class GoogleMapProvider(private val context: Context) :
211
228
  }
212
229
 
213
230
  override fun onMarkerClick(marker: Marker): Boolean {
231
+ dismissNonBubbledCallout()
232
+
214
233
  markerToViewMap[marker]?.let { view ->
215
234
  val point = googleMap?.projection?.toScreenLocation(marker.position)
216
235
  view.emitPressEvent(point?.x?.toFloat() ?: 0f, point?.y?.toFloat() ?: 0f)
236
+
237
+ val calloutView = view.calloutView
238
+ if (calloutView != null && !calloutView.bubbled && calloutView.hasCustomContent) {
239
+ googleMap?.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
240
+ showNonBubbledCallout(marker, calloutView)
241
+ return true
242
+ }
217
243
  }
218
244
  return false
219
245
  }
@@ -224,6 +250,7 @@ class GoogleMapProvider(private val context: Context) :
224
250
  view.setCoordinate(marker.position.latitude, marker.position.longitude)
225
251
  val point = googleMap?.projection?.toScreenLocation(marker.position)
226
252
  view.emitDragStartEvent(point?.x?.toFloat() ?: 0f, point?.y?.toFloat() ?: 0f)
253
+ if (!view.rasterize) positionLiveMarker(view)
227
254
  }
228
255
  }
229
256
 
@@ -232,6 +259,7 @@ class GoogleMapProvider(private val context: Context) :
232
259
  view.setCoordinate(marker.position.latitude, marker.position.longitude)
233
260
  val point = googleMap?.projection?.toScreenLocation(marker.position)
234
261
  view.emitDragChangeEvent(point?.x?.toFloat() ?: 0f, point?.y?.toFloat() ?: 0f)
262
+ if (!view.rasterize) positionLiveMarker(view)
235
263
  }
236
264
  }
237
265
 
@@ -241,7 +269,92 @@ class GoogleMapProvider(private val context: Context) :
241
269
  view.setCoordinate(marker.position.latitude, marker.position.longitude)
242
270
  val point = googleMap?.projection?.toScreenLocation(marker.position)
243
271
  view.emitDragEndEvent(point?.x?.toFloat() ?: 0f, point?.y?.toFloat() ?: 0f)
272
+ if (!view.rasterize) positionLiveMarker(view)
273
+ }
274
+ }
275
+
276
+ override fun getInfoWindow(marker: Marker): View? {
277
+ // Non-bubbled callouts are rendered as live views, not info windows
278
+ return null
279
+ }
280
+
281
+ override fun getInfoContents(marker: Marker): View? {
282
+ val markerView = markerToViewMap[marker] ?: return null
283
+ val calloutView = markerView.calloutView ?: return null
284
+ if (!calloutView.hasCustomContent || !calloutView.bubbled) return null
285
+
286
+ val bitmap = calloutView.createContentBitmap() ?: return null
287
+ return ImageView(context).apply { setImageBitmap(bitmap) }
288
+ }
289
+
290
+ private fun showNonBubbledCallout(marker: Marker, calloutView: LuggCalloutView) {
291
+ val wrapper = wrapperView ?: return
292
+ val contentView = calloutView.contentView
293
+
294
+ calloutView.onUpdate = {
295
+ layoutNonBubbledCallout()
296
+ positionNonBubbledCallout()
244
297
  }
298
+
299
+ dismissInfoWindows()
300
+ wrapper.addView(contentView)
301
+ activeNonBubbledMarker = marker
302
+ layoutNonBubbledCallout()
303
+ positionNonBubbledCallout()
304
+ }
305
+
306
+ private fun dismissInfoWindows() {
307
+ for ((marker, _) in markerToViewMap) {
308
+ if (marker.isInfoWindowShown) {
309
+ marker.hideInfoWindow()
310
+ }
311
+ }
312
+ }
313
+
314
+ private fun dismissNonBubbledCallout() {
315
+ val marker = activeNonBubbledMarker ?: return
316
+ val markerView = markerToViewMap[marker] ?: return
317
+ val calloutView = markerView.calloutView ?: return
318
+ val contentView = calloutView.contentView
319
+
320
+ calloutView.onUpdate = null
321
+ (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
322
+ activeNonBubbledMarker = null
323
+ }
324
+
325
+ private fun layoutNonBubbledCallout() {
326
+ val marker = activeNonBubbledMarker ?: return
327
+ val markerView = markerToViewMap[marker] ?: return
328
+ val calloutView = markerView.calloutView ?: return
329
+ val contentView = calloutView.contentView
330
+
331
+ var contentWidth = 0
332
+ var contentHeight = 0
333
+ for (i in 0 until contentView.childCount) {
334
+ val child = contentView.getChildAt(i)
335
+ val childRight = child.left + child.width
336
+ val childBottom = child.top + child.height
337
+ if (childRight > contentWidth) contentWidth = childRight
338
+ if (childBottom > contentHeight) contentHeight = childBottom
339
+ }
340
+
341
+ contentView.measure(
342
+ View.MeasureSpec.makeMeasureSpec(contentWidth, View.MeasureSpec.EXACTLY),
343
+ View.MeasureSpec.makeMeasureSpec(contentHeight, View.MeasureSpec.EXACTLY)
344
+ )
345
+ contentView.layout(0, 0, contentWidth, contentHeight)
346
+ }
347
+
348
+ private fun positionNonBubbledCallout() {
349
+ val marker = activeNonBubbledMarker ?: return
350
+ val markerView = markerToViewMap[marker] ?: return
351
+ val calloutView = markerView.calloutView ?: return
352
+ val contentView = calloutView.contentView
353
+ val map = googleMap ?: return
354
+
355
+ val point = map.projection.toScreenLocation(marker.position)
356
+ contentView.translationX = point.x - contentView.width * calloutView.anchorX
357
+ contentView.translationY = point.y - contentView.height * calloutView.anchorY
245
358
  }
246
359
 
247
360
  // endregion
@@ -382,6 +495,7 @@ class GoogleMapProvider(private val context: Context) :
382
495
  }
383
496
 
384
497
  override fun removeMarkerView(markerView: LuggMarkerView) {
498
+ removeLiveMarker(markerView)
385
499
  markerView.marker?.let { markerToViewMap.remove(it) }
386
500
  markerView.marker?.remove()
387
501
  markerView.marker = null
@@ -411,9 +525,14 @@ class GoogleMapProvider(private val context: Context) :
411
525
  isDraggable = markerView.draggable
412
526
  }
413
527
 
414
- if (markerView.hasCustomView && markerView.scaleChanged) {
415
- markerView.applyScaleToMarker()
416
- markerView.clearScaleChanged()
528
+ if (markerView.hasCustomView) {
529
+ if (markerView.scaleChanged) {
530
+ markerView.applyScaleToMarker()
531
+ markerView.clearScaleChanged()
532
+ }
533
+ if (!markerView.rasterize) {
534
+ positionLiveMarker(markerView)
535
+ }
417
536
  }
418
537
  }
419
538
 
@@ -440,7 +559,64 @@ class GoogleMapProvider(private val context: Context) :
440
559
 
441
560
  markerView.marker = marker
442
561
  markerToViewMap[marker] = markerView
443
- markerView.applyIconToMarker()
562
+
563
+ if (markerView.hasCustomView) {
564
+ if (markerView.rasterize) {
565
+ markerView.applyIconToMarker()
566
+ } else {
567
+ showLiveMarker(markerView)
568
+ }
569
+ }
570
+ }
571
+
572
+ // Workaround: AdvancedMarker.iconView is buggy on Android, so we manually add the custom
573
+ // content view to the wrapper and position it via screen projection instead. The underlying
574
+ // marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
575
+ private fun showLiveMarker(markerView: LuggMarkerView) {
576
+ val wrapper = wrapperView ?: return
577
+
578
+ markerView.onUpdate = {
579
+ updateLiveMarkerHitArea(markerView)
580
+ positionLiveMarker(markerView)
581
+ }
582
+
583
+ val contentView = markerView.contentView
584
+ contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
585
+ (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
586
+ wrapper.addView(contentView)
587
+ liveMarkerViews.add(markerView)
588
+ markerView.layoutContentView()
589
+ updateLiveMarkerHitArea(markerView)
590
+ positionLiveMarker(markerView)
591
+ }
592
+
593
+ private fun updateLiveMarkerHitArea(markerView: LuggMarkerView) {
594
+ val marker = markerView.marker ?: return
595
+ val contentView = markerView.contentView
596
+ val w = contentView.width.coerceAtLeast(1)
597
+ val h = contentView.height.coerceAtLeast(1)
598
+ marker.setIcon(BitmapDescriptorFactory.fromBitmap(createBitmap(w, h)))
599
+ }
600
+
601
+ private fun removeLiveMarker(markerView: LuggMarkerView) {
602
+ markerView.onUpdate = null
603
+ val contentView = markerView.contentView
604
+ (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
605
+ liveMarkerViews.remove(markerView)
606
+ }
607
+
608
+ private fun positionLiveMarkers() {
609
+ for (markerView in liveMarkerViews) {
610
+ positionLiveMarker(markerView)
611
+ }
612
+ }
613
+
614
+ private fun positionLiveMarker(markerView: LuggMarkerView) {
615
+ val map = googleMap ?: return
616
+ val contentView = markerView.contentView
617
+ val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
618
+ contentView.translationX = point.x - contentView.width * markerView.anchorX
619
+ contentView.translationY = point.y - contentView.height * markerView.anchorY
444
620
  }
445
621
 
446
622
  // endregion