expo-gaode-map 2.2.15 → 2.2.16

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 (33) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +40 -1
  3. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +427 -61
  4. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterViewModule.kt +16 -0
  5. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +160 -25
  6. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +13 -1
  7. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +165 -13
  8. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointViewModule.kt +9 -1
  9. package/android/src/main/java/expo/modules/gaodemap/utils/BitmapDescriptorCache.kt +20 -0
  10. package/build/components/overlays/Cluster.d.ts.map +1 -1
  11. package/build/components/overlays/Cluster.js +6 -2
  12. package/build/components/overlays/Cluster.js.map +1 -1
  13. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  14. package/build/components/overlays/HeatMap.js +12 -1
  15. package/build/components/overlays/HeatMap.js.map +1 -1
  16. package/build/index.d.ts +0 -1
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js.map +1 -1
  19. package/build/types/overlays.types.d.ts +69 -14
  20. package/build/types/overlays.types.d.ts.map +1 -1
  21. package/build/types/overlays.types.js.map +1 -1
  22. package/build/utils/ModuleLoader.js +1 -1
  23. package/build/utils/ModuleLoader.js.map +1 -1
  24. package/ios/ExpoGaodeMapView.swift +44 -0
  25. package/ios/overlays/ClusterAnnotation.swift +32 -0
  26. package/ios/overlays/ClusterView.swift +251 -45
  27. package/ios/overlays/ClusterViewModule.swift +14 -0
  28. package/ios/overlays/CoordinateQuadTree.swift +291 -0
  29. package/ios/overlays/HeatMapView.swift +119 -5
  30. package/ios/overlays/HeatMapViewModule.swift +13 -1
  31. package/ios/overlays/MultiPointView.swift +160 -2
  32. package/ios/overlays/MultiPointViewModule.swift +22 -0
  33. package/package.json +1 -1
@@ -45,4 +45,4 @@ android {
45
45
  dependencies {
46
46
  // 高德地图 3D SDK
47
47
  implementation ('com.amap.api:3dmap:latest.integration')
48
- }
48
+ }
@@ -145,6 +145,19 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
145
145
  }
146
146
  }
147
147
 
148
+ // 辅助监听器列表
149
+ private val cameraChangeListeners = mutableListOf<AMap.OnCameraChangeListener>()
150
+
151
+ fun addCameraChangeListener(listener: AMap.OnCameraChangeListener) {
152
+ if (!cameraChangeListeners.contains(listener)) {
153
+ cameraChangeListeners.add(listener)
154
+ }
155
+ }
156
+
157
+ fun removeCameraChangeListener(listener: AMap.OnCameraChangeListener) {
158
+ cameraChangeListeners.remove(listener)
159
+ }
160
+
148
161
  /**
149
162
  * 设置地图事件监听
150
163
  */
@@ -152,6 +165,9 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
152
165
  // 设置相机移动监听器
153
166
  aMap.setOnCameraChangeListener(object : AMap.OnCameraChangeListener {
154
167
  override fun onCameraChange(cameraPosition: com.amap.api.maps.model.CameraPosition?) {
168
+ // 通知辅助监听器
169
+ cameraChangeListeners.forEach { it.onCameraChange(cameraPosition) }
170
+
155
171
  // 相机移动中 - 应用节流优化
156
172
  cameraPosition?.let {
157
173
  val currentTime = System.currentTimeMillis()
@@ -199,6 +215,9 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
199
215
  }
200
216
 
201
217
  override fun onCameraChangeFinish(cameraPosition: com.amap.api.maps.model.CameraPosition?) {
218
+ // 通知辅助监听器
219
+ cameraChangeListeners.forEach { it.onCameraChangeFinish(cameraPosition) }
220
+
202
221
  // 相机移动完成
203
222
  cameraPosition?.let {
204
223
  val visibleRegion = aMap.projection.visibleRegion
@@ -229,7 +248,13 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
229
248
 
230
249
  // 设置全局 Marker 点击监听器
231
250
  aMap.setOnMarkerClickListener { marker ->
232
- MarkerView.handleMarkerClick(marker)
251
+ if (MarkerView.handleMarkerClick(marker)) {
252
+ return@setOnMarkerClickListener true
253
+ }
254
+ if (ClusterView.handleMarkerClick(marker)) {
255
+ return@setOnMarkerClickListener true
256
+ }
257
+ false
233
258
  }
234
259
 
235
260
  // 设置全局 Marker 拖拽监听器
@@ -247,6 +272,19 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
247
272
  }
248
273
  })
249
274
 
275
+ // 设置全局 MultiPoint 点击监听器
276
+ aMap.setOnMultiPointClickListener { item ->
277
+ for (i in 0 until childCount) {
278
+ val child = getChildAt(i)
279
+ if (child is MultiPointView) {
280
+ if (child.handleMultiPointClick(item)) {
281
+ return@setOnMultiPointClickListener true
282
+ }
283
+ }
284
+ }
285
+ return@setOnMultiPointClickListener false
286
+ }
287
+
250
288
  aMap.setOnMapClickListener { latLng ->
251
289
  // 检查声明式 PolylineView
252
290
  if (checkDeclarativePolylinePress(latLng)) {
@@ -465,6 +503,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
465
503
  aMap.setOnCameraChangeListener(null)
466
504
  aMap.setOnMarkerClickListener(null)
467
505
  aMap.setOnMarkerDragListener(null)
506
+ aMap.setOnMultiPointClickListener(null)
468
507
 
469
508
  // 清除所有覆盖物
470
509
  aMap.clear()
@@ -1,53 +1,132 @@
1
1
  package expo.modules.gaodemap.overlays
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
5
+ import android.util.Log
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.Canvas
9
+ import java.net.URL
10
+ import expo.modules.gaodemap.utils.BitmapDescriptorCache
11
+
12
+ import android.graphics.Paint
13
+
14
+ import android.graphics.Typeface
15
+ import android.os.Handler
16
+ import android.os.Looper
17
+
4
18
  import com.amap.api.maps.AMap
19
+ import com.amap.api.maps.AMapUtils
20
+ import com.amap.api.maps.model.BitmapDescriptor
5
21
  import com.amap.api.maps.model.BitmapDescriptorFactory
22
+ import com.amap.api.maps.model.CameraPosition
6
23
  import com.amap.api.maps.model.LatLng
7
24
  import com.amap.api.maps.model.Marker
8
25
  import com.amap.api.maps.model.MarkerOptions
26
+ import expo.modules.gaodemap.ExpoGaodeMapView
27
+ import expo.modules.gaodemap.utils.ColorParser
9
28
  import expo.modules.kotlin.AppContext
10
29
  import expo.modules.kotlin.viewevent.EventDispatcher
11
30
  import expo.modules.kotlin.views.ExpoView
31
+ import kotlinx.coroutines.*
32
+ import java.util.concurrent.ConcurrentHashMap
33
+
12
34
 
13
35
  /**
14
36
  * 点聚合视图
15
- * 注意:高德 Android SDK 的点聚合功能需要额外依赖,这里提供基础实现
37
+ * 实现真正的点聚合逻辑,支持自定义样式和点击事件
16
38
  */
17
- class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
39
+ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext), AMap.OnCameraChangeListener {
18
40
 
19
41
  private val onPress by EventDispatcher()
20
42
  @Suppress("unused")
21
43
  private val onClusterPress by EventDispatcher()
22
44
 
23
45
  private var aMap: AMap? = null
24
- private var markers: MutableList<Marker> = mutableListOf()
25
- private var points: List<Map<String, Any>> = emptyList()
26
- @Suppress("unused")
27
- private var radius: Int = 60
28
- @Suppress("unused")
29
- private var minClusterSize: Int = 2
30
46
 
47
+ // 聚合点数据
48
+ data class ClusterItem(
49
+ val latLng: LatLng,
50
+ val data: Map<String, Any>
51
+ )
52
+
53
+ // 聚合对象
54
+ class Cluster(val center: LatLng) {
55
+ val items = mutableListOf<ClusterItem>()
56
+
57
+ fun add(item: ClusterItem) {
58
+ items.add(item)
59
+ }
60
+
61
+ val size: Int get() = items.size
62
+ val position: LatLng get() = center // 简单处理,使用中心点作为聚合点位置,也可以计算平均位置
63
+ }
64
+
65
+ private var rawPoints: List<Map<String, Any>> = emptyList()
66
+ private var clusterItems: List<ClusterItem> = emptyList()
67
+ private var clusters: List<Cluster> = emptyList()
68
+
69
+ // 当前显示的 Markers
70
+ private val currentMarkers = mutableListOf<Marker>()
71
+
72
+ // 配置属性
73
+ private var radius: Int = 60 // dp
74
+ private var minClusterSize: Int = 1
75
+
76
+ // 样式属性
77
+ private var clusterStyle: Map<String, Any>? = null
78
+ private var clusterBuckets: List<Map<String, Any>>? = null
79
+ private var clusterTextStyle: Map<String, Any>? = null
80
+
81
+ private val mainHandler = Handler(Looper.getMainLooper())
82
+
83
+ // 协程作用域
84
+ private var scope = CoroutineScope(Dispatchers.Main + Job())
85
+ private var calculationJob: Job? = null
86
+
87
+ // 缓存 BitmapDescriptor
88
+ private val bitmapCache = ConcurrentHashMap<Int, BitmapDescriptor>()
89
+ private var currentIconDescriptor: BitmapDescriptor? = null
90
+ private var pendingIconUri: String? = null
91
+
31
92
  /**
32
93
  * 设置地图实例
33
94
  */
34
95
  @Suppress("unused")
35
96
  fun setMap(map: AMap) {
36
97
  aMap = map
37
- createOrUpdateCluster()
98
+ // 注册相机监听
99
+ // 注意:addView 时会自动调用 setMap,此时 parent 已设置
100
+ (parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
101
+
102
+ // 如果有待处理的 icon,加载它
103
+ pendingIconUri?.let {
104
+ loadAndSetIcon(it)
105
+ }
106
+
107
+ updateClusters()
38
108
  }
39
109
 
40
110
  /**
41
111
  * 设置聚合点数据
42
112
  */
43
113
  fun setPoints(pointsList: List<Map<String, Any>>) {
44
- // 过滤无效坐标
45
- points = pointsList.filter { point ->
46
- val lat = (point["latitude"] as? Number)?.toDouble()
47
- val lng = (point["longitude"] as? Number)?.toDouble()
48
- lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180
114
+ rawPoints = pointsList
115
+ // 预处理数据
116
+ scope.launch(Dispatchers.Default) {
117
+ clusterItems = pointsList.mapNotNull { point: Map<String, Any> ->
118
+ val lat = (point["latitude"] as? Number)?.toDouble()
119
+ val lng = (point["longitude"] as? Number)?.toDouble()
120
+ if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
121
+ ClusterItem(LatLng(lat, lng), point)
122
+ } else {
123
+ null
124
+ }
125
+ }
126
+ withContext(Dispatchers.Main) {
127
+ updateClusters()
128
+ }
49
129
  }
50
- createOrUpdateCluster()
51
130
  }
52
131
 
53
132
  /**
@@ -55,84 +134,371 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
55
134
  */
56
135
  fun setRadius(radiusValue: Int) {
57
136
  radius = radiusValue
58
- createOrUpdateCluster()
137
+ updateClusters()
59
138
  }
60
139
 
61
140
  /**
62
141
  * 设置最小聚合数量
63
142
  */
64
143
  fun setMinClusterSize(size: Int) {
144
+ Log.d("ClusterView", "setMinClusterSize: $size")
65
145
  minClusterSize = size
66
- createOrUpdateCluster()
146
+ updateClusters()
67
147
  }
68
148
 
69
149
  /**
70
- * 设置图标
150
+ * 设置聚合样式
151
+ */
152
+ fun setClusterStyle(style: Map<String, Any>) {
153
+ clusterStyle = style
154
+ bitmapCache.clear() // 样式改变,清除缓存
155
+ updateClusters()
156
+ }
157
+
158
+ /**
159
+ * 设置聚合文字样式
160
+ */
161
+ fun setClusterTextStyle(style: Map<String, Any>) {
162
+ clusterTextStyle = style
163
+ bitmapCache.clear() // 样式改变,清除缓存
164
+ updateClusters()
165
+ }
166
+
167
+ fun setClusterBuckets(buckets: List<Map<String, Any>>) {
168
+ clusterBuckets = buckets
169
+ bitmapCache.clear()
170
+ updateClusters()
171
+ }
172
+
173
+ /**
174
+ * 设置图标 (保留接口,目前主要使用 clusterStyle)
71
175
  */
72
176
  @Suppress("UNUSED_PARAMETER")
73
177
  fun setIcon(iconUri: String?) {
74
- // 简化处理,实际需要实现图片加载
75
- createOrUpdateCluster()
178
+ pendingIconUri = iconUri
179
+ if (iconUri != null) {
180
+ loadAndSetIcon(iconUri)
181
+ } else {
182
+ currentIconDescriptor = null
183
+ updateClusters()
184
+ }
185
+ }
186
+
187
+ private fun loadAndSetIcon(iconUri: String) {
188
+ // 尝试从缓存获取
189
+ val cacheKey = "cluster|$iconUri"
190
+ BitmapDescriptorCache.get(cacheKey)?.let {
191
+ currentIconDescriptor = it
192
+ updateClusters()
193
+ return
194
+ }
195
+
196
+ scope.launch(Dispatchers.IO) {
197
+ try {
198
+ val descriptor = when {
199
+ iconUri.startsWith("http") -> {
200
+ val url = URL(iconUri)
201
+ val bitmap = BitmapFactory.decodeStream(url.openStream())
202
+ BitmapDescriptorFactory.fromBitmap(bitmap)
203
+ }
204
+ iconUri.startsWith("file://") -> {
205
+ val path = iconUri.substring(7)
206
+ BitmapDescriptorFactory.fromPath(path)
207
+ }
208
+ else -> {
209
+ // 尝试作为资源名称加载
210
+ val resId = context.resources.getIdentifier(iconUri, "drawable", context.packageName)
211
+ if (resId != 0) {
212
+ BitmapDescriptorFactory.fromResource(resId)
213
+ } else {
214
+ // 尝试作为普通文件路径
215
+ BitmapDescriptorFactory.fromPath(iconUri)
216
+ }
217
+ }
218
+ }
219
+
220
+ if (descriptor != null) {
221
+ BitmapDescriptorCache.put(cacheKey, descriptor)
222
+ currentIconDescriptor = descriptor
223
+ withContext(Dispatchers.Main) {
224
+ updateClusters()
225
+ }
226
+ }
227
+ } catch (e: Exception) {
228
+ e.printStackTrace()
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 相机移动回调
235
+ */
236
+ override fun onCameraChange(cameraPosition: CameraPosition?) {
237
+ // 移动过程中不实时重新计算,以免性能问题
238
+ }
239
+
240
+ override fun onCameraChangeFinish(cameraPosition: CameraPosition?) {
241
+ updateClusters()
242
+ }
243
+
244
+ override fun onAttachedToWindow() {
245
+ super.onAttachedToWindow()
246
+ // 重新创建协程作用域(如果已被取消)
247
+ if (!scope.isActive) {
248
+ scope = CoroutineScope(Dispatchers.Main + Job())
249
+ }
250
+ // 重新注册监听器(防止因 detach 导致监听器丢失)
251
+ (parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
252
+ updateClusters()
253
+ }
254
+
255
+ override fun onDetachedFromWindow() {
256
+ super.onDetachedFromWindow()
257
+ scope.cancel() // 取消协程
258
+ (parent as? ExpoGaodeMapView)?.removeCameraChangeListener(this)
259
+ currentMarkers.forEach {
260
+ it.remove()
261
+ unregisterMarker(it)
262
+ }
263
+ currentMarkers.clear()
264
+ bitmapCache.clear()
76
265
  }
77
266
 
78
267
  /**
79
- * 创建或更新聚合
80
- * 注意:这是简化实现,完整的点聚合需要使用专门的聚合库
268
+ * 更新聚合
269
+ * 使用协程在后台计算
81
270
  */
82
- private fun createOrUpdateCluster() {
83
- aMap?.let { map ->
84
- // 清除旧的标记
85
- markers.forEach { it.remove() }
86
- markers.clear()
271
+ private fun updateClusters() {
272
+ val map = aMap ?: return
273
+
274
+ // 取消上一次计算
275
+ calculationJob?.cancel()
276
+
277
+ // 确保 scope 处于活跃状态
278
+ if (!scope.isActive) {
279
+ scope = CoroutineScope(Dispatchers.Main + Job())
280
+ }
281
+
282
+ calculationJob = scope.launch(Dispatchers.Default) {
283
+ if (clusterItems.isEmpty()) return@launch
87
284
 
88
- // 简化实现:直接添加所有点作为标记
89
- // 实际应用中应该使用点聚合算法
90
- points.forEach { point ->
91
- val lat = (point["latitude"] as? Number)?.toDouble()
92
- val lng = (point["longitude"] as? Number)?.toDouble()
285
+ // 获取当前比例尺 (米/像素)
286
+ val scalePerPixel = withContext(Dispatchers.Main) {
287
+ // 增加安全性检查
288
+ if (map.mapType != 0) { // 简单检查 map 是否存活
289
+ map.scalePerPixel
290
+ } else {
291
+ 0f
292
+ }
293
+ }
294
+
295
+ if (scalePerPixel <= 0) {
296
+ // 比例尺无效,稍后重试
297
+ withContext(Dispatchers.Main) {
298
+ android.util.Log.w("ClusterView", "Invalid scalePerPixel: $scalePerPixel, retrying...")
299
+ mainHandler.postDelayed({ updateClusters() }, 500)
300
+ }
301
+ return@launch
302
+ }
303
+
304
+ // 计算聚合距离 (米)
305
+ // radius 是 dp,需要转 px,再转米
306
+ val density = context.resources.displayMetrics.density
307
+ val radiusPx = radius * density
308
+ val radiusMeters = radiusPx * scalePerPixel
309
+
310
+ val newClusters = mutableListOf<Cluster>()
311
+ val visited = BooleanArray(clusterItems.size)
312
+
313
+ // 简单的距离聚合算法
314
+ for (i in clusterItems.indices) {
315
+ if (visited[i]) continue
316
+
317
+ val item = clusterItems[i]
318
+ val cluster = Cluster(item.latLng)
319
+ cluster.add(item)
320
+ visited[i] = true
93
321
 
94
- if (lat != null && lng != null) {
95
- val markerOptions = MarkerOptions()
96
- .position(LatLng(lat, lng))
97
- .icon(BitmapDescriptorFactory.defaultMarker())
322
+ for (j in i + 1 until clusterItems.size) {
323
+ if (visited[j]) continue
324
+
325
+ val other = clusterItems[j]
326
+ val distance = AMapUtils.calculateLineDistance(item.latLng, other.latLng)
98
327
 
99
- val marker = map.addMarker(markerOptions)
100
- marker?.let { markers.add(it) }
328
+ if (distance < radiusMeters) {
329
+ cluster.add(other)
330
+ visited[j] = true
331
+ }
101
332
  }
333
+
334
+ newClusters.add(cluster)
102
335
  }
103
336
 
104
- // 设置点击监听
105
- map.setOnMarkerClickListener { clickedMarker ->
106
- if (markers.contains(clickedMarker)) {
107
- onPress(mapOf(
108
- "latitude" to clickedMarker.position.latitude,
109
- "longitude" to clickedMarker.position.longitude
110
- ))
111
- true
112
- } else {
113
- false
114
- }
337
+ // 更新 UI
338
+ withContext(Dispatchers.Main) {
339
+ renderClusters(newClusters)
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * 渲染聚合点
346
+ */
347
+ private fun renderClusters(newClusters: List<Cluster>) {
348
+ Log.d("ClusterView", "renderClusters: count=${newClusters.size}, minClusterSize=$minClusterSize")
349
+ val map = aMap ?: return
350
+ clusters = newClusters
351
+
352
+ // 清除旧 Markers
353
+ currentMarkers.forEach {
354
+ it.remove()
355
+ unregisterMarker(it)
356
+ }
357
+ currentMarkers.clear()
358
+
359
+ // 添加新 Markers
360
+ newClusters.forEach { cluster ->
361
+ val markerOptions = MarkerOptions()
362
+ .position(cluster.center)
363
+
364
+ if (cluster.size >= minClusterSize) {
365
+ // 聚合点
366
+ markerOptions.icon(generateIcon(cluster.size))
367
+ markerOptions.zIndex(2.0f) // 聚合点层级高一点
368
+ } else {
369
+ // 单个点
370
+ markerOptions.icon(currentIconDescriptor ?: BitmapDescriptorFactory.defaultMarker())
371
+ markerOptions.zIndex(1.0f)
372
+ }
373
+
374
+ val marker = map.addMarker(markerOptions)
375
+ if (marker != null) {
376
+ currentMarkers.add(marker)
377
+ registerMarker(marker, this)
378
+ // 存储关联的 cluster 数据,以便点击时获取
379
+ marker.setObject(cluster)
115
380
  }
116
381
  }
117
382
  }
118
383
 
119
384
  /**
120
- * 移除聚合
385
+ * 生成聚合图标
386
+ */
387
+ @SuppressLint("UseKtx")
388
+ private fun generateIcon(count: Int): BitmapDescriptor {
389
+ // 检查缓存
390
+ // 简单的缓存策略:只根据数量缓存。如果样式变化,会清空缓存。
391
+ bitmapCache[count]?.let { return it }
392
+
393
+ val density = context.resources.displayMetrics.density
394
+
395
+ // 获取样式配置
396
+ var activeStyle = clusterStyle ?: emptyMap()
397
+
398
+ clusterBuckets?.let { buckets ->
399
+ val bestBucket = buckets
400
+ .filter { (it["minPoints"] as? Number)?.toInt() ?: 0 <= count }
401
+ .maxByOrNull { (it["minPoints"] as? Number)?.toInt() ?: 0 }
402
+
403
+ if (bestBucket != null) {
404
+ activeStyle = activeStyle + bestBucket
405
+ }
406
+ }
407
+
408
+ val bgColorVal = activeStyle["backgroundColor"]
409
+ val borderColorVal = activeStyle["borderColor"]
410
+ val borderWidthVal = (activeStyle["borderWidth"] as? Number)?.toFloat() ?: 2f
411
+ val textSizeVal = (clusterTextStyle?.get("fontSize") as? Number)?.toFloat() ?: 14f
412
+ val textColorVal = clusterTextStyle?.get("color")
413
+ val fontWeightVal = clusterTextStyle?.get("fontWeight") as? String
414
+
415
+ // 解析颜色
416
+ val bgColor = ColorParser.parseColor(bgColorVal ?: "#F54531") // 默认红色
417
+ val borderColor = ColorParser.parseColor(borderColorVal ?: "#FFFFFF") // 默认白色
418
+ val textColor = ColorParser.parseColor(textColorVal ?: "#FFFFFF") // 默认白色
419
+
420
+ // 计算尺寸 (根据 iOS 逻辑:size = 30 + (count.toString().length - 1) * 5)
421
+ // 这里简单处理,或者根据 count 动态调整
422
+ // 基础大小 36dp
423
+ val baseSize = 36
424
+ val extraSize = (count.toString().length - 1) * 6
425
+ val sizeDp = baseSize + extraSize
426
+ val sizePx = (sizeDp * density).toInt()
427
+
428
+ val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
429
+ val canvas = Canvas(bitmap)
430
+
431
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
432
+ val radius = sizePx / 2f
433
+
434
+ // 绘制边框
435
+ paint.color = borderColor
436
+ paint.style = Paint.Style.FILL
437
+ canvas.drawCircle(radius, radius, radius, paint)
438
+
439
+ // 绘制背景
440
+ paint.color = bgColor
441
+ val borderWidthPx = borderWidthVal * density
442
+ canvas.drawCircle(radius, radius, radius - borderWidthPx, paint)
443
+
444
+ // 绘制文字
445
+ paint.color = textColor
446
+ paint.textSize = textSizeVal * density
447
+ paint.textAlign = Paint.Align.CENTER
448
+
449
+ // 字体粗细
450
+ if (fontWeightVal == "bold") {
451
+ paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
452
+ }
453
+
454
+ // 文字垂直居中
455
+ val fontMetrics = paint.fontMetrics
456
+ val baseline = radius - (fontMetrics.bottom + fontMetrics.top) / 2
457
+
458
+ canvas.drawText(count.toString(), radius, baseline, paint)
459
+
460
+ val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
461
+ bitmapCache[count] = descriptor
462
+ return descriptor
463
+ }
464
+
465
+ /**
466
+ * 处理 Marker 点击
121
467
  */
122
- fun removeCluster() {
123
- markers.forEach { it.remove() }
124
- markers.clear()
125
- points = emptyList()
468
+ fun onMarkerClick(marker: Marker) {
469
+ val cluster = marker.getObject() as? Cluster
470
+ if (cluster != null) {
471
+ // 无论聚合数量多少,统一触发 onClusterPress
472
+ // 这样保证用户在 React Native 端监听 onClusterPress 时总能收到事件
473
+ // 如果是单点,count 为 1,pois 包含单个点数据
474
+ val pointsData = cluster.items.map { it.data }
475
+ onClusterPress(mapOf(
476
+ "count" to cluster.size,
477
+ "latitude" to cluster.center.latitude,
478
+ "longitude" to cluster.center.longitude,
479
+ "pois" to pointsData,
480
+ "points" to pointsData // 兼容 iOS 或用户习惯
481
+ ))
482
+ }
126
483
  }
127
484
 
128
- override fun onDetachedFromWindow() {
129
- super.onDetachedFromWindow()
130
- // 🔑 关键修复:使用 post 延迟检查
131
- post {
132
- if (parent == null) {
133
- removeCluster()
134
- aMap = null
485
+ companion object {
486
+ private val markerMap = ConcurrentHashMap<Marker, ClusterView>()
487
+
488
+ fun registerMarker(marker: Marker, view: ClusterView) {
489
+ markerMap[marker] = view
490
+ }
491
+
492
+ fun unregisterMarker(marker: Marker) {
493
+ markerMap.remove(marker)
494
+ }
495
+
496
+ fun handleMarkerClick(marker: Marker): Boolean {
497
+ markerMap[marker]?.let { view ->
498
+ view.onMarkerClick(marker)
499
+ return true
135
500
  }
501
+ return false
136
502
  }
137
503
  }
138
504
  }
@@ -24,6 +24,22 @@ class ClusterViewModule : Module() {
24
24
  Prop<Int>("minClusterSize") { view: ClusterView, size ->
25
25
  view.setMinClusterSize(size)
26
26
  }
27
+
28
+ Prop<Map<String, Any>>("clusterStyle") { view: ClusterView, style ->
29
+ view.setClusterStyle(style)
30
+ }
31
+
32
+ Prop<List<Map<String, Any>>>("clusterBuckets") { view: ClusterView, buckets ->
33
+ view.setClusterBuckets(buckets)
34
+ }
35
+
36
+ Prop<Map<String, Any>>("clusterTextStyle") { view: ClusterView, style ->
37
+ view.setClusterTextStyle(style)
38
+ }
39
+
40
+ Prop<String>("icon") { view: ClusterView, icon ->
41
+ view.setIcon(icon)
42
+ }
27
43
  }
28
44
  }
29
45
  }