expo-gaode-map 2.2.37 → 2.2.39-next.0

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 (92) hide show
  1. package/README.md +4 -3
  2. package/android/build.gradle +11 -32
  3. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +35 -12
  4. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapViewModule.kt +2 -0
  5. package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +8 -0
  6. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +13 -5
  7. package/build/ExpoGaodeMapOfflineModule.js +1 -1
  8. package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
  9. package/build/ExpoGaodeMapView.js +2 -8
  10. package/build/ExpoGaodeMapView.js.map +1 -1
  11. package/build/components/AreaMaskOverlay.d.ts +1 -2
  12. package/build/components/AreaMaskOverlay.d.ts.map +1 -1
  13. package/build/components/AreaMaskOverlay.js +2 -1
  14. package/build/components/AreaMaskOverlay.js.map +1 -1
  15. package/build/components/FoldableMapView.d.ts +3 -2
  16. package/build/components/FoldableMapView.d.ts.map +1 -1
  17. package/build/components/FoldableMapView.js +2 -1
  18. package/build/components/FoldableMapView.js.map +1 -1
  19. package/build/components/MapUI.js +2 -1
  20. package/build/components/MapUI.js.map +1 -1
  21. package/build/components/RouteOverlay.d.ts +1 -2
  22. package/build/components/RouteOverlay.d.ts.map +1 -1
  23. package/build/components/RouteOverlay.js +5 -5
  24. package/build/components/RouteOverlay.js.map +1 -1
  25. package/build/components/overlays/Circle.d.ts +1 -1
  26. package/build/components/overlays/Circle.d.ts.map +1 -1
  27. package/build/components/overlays/Circle.js +2 -1
  28. package/build/components/overlays/Circle.js.map +1 -1
  29. package/build/components/overlays/Cluster.d.ts +1 -1
  30. package/build/components/overlays/Cluster.d.ts.map +1 -1
  31. package/build/components/overlays/Cluster.js +2 -1
  32. package/build/components/overlays/Cluster.js.map +1 -1
  33. package/build/components/overlays/HeatMap.d.ts +1 -1
  34. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  35. package/build/components/overlays/HeatMap.js +2 -1
  36. package/build/components/overlays/HeatMap.js.map +1 -1
  37. package/build/components/overlays/Marker.d.ts +1 -1
  38. package/build/components/overlays/Marker.d.ts.map +1 -1
  39. package/build/components/overlays/Marker.js +8 -7
  40. package/build/components/overlays/Marker.js.map +1 -1
  41. package/build/components/overlays/MultiPoint.d.ts +1 -2
  42. package/build/components/overlays/MultiPoint.d.ts.map +1 -1
  43. package/build/components/overlays/MultiPoint.js +2 -1
  44. package/build/components/overlays/MultiPoint.js.map +1 -1
  45. package/build/components/overlays/Polygon.d.ts +1 -1
  46. package/build/components/overlays/Polygon.d.ts.map +1 -1
  47. package/build/components/overlays/Polygon.js +2 -1
  48. package/build/components/overlays/Polygon.js.map +1 -1
  49. package/build/components/overlays/Polyline.d.ts +1 -1
  50. package/build/components/overlays/Polyline.d.ts.map +1 -1
  51. package/build/components/overlays/Polyline.js +2 -1
  52. package/build/components/overlays/Polyline.js.map +1 -1
  53. package/build/module/geometry.d.ts.map +1 -1
  54. package/build/module/geometry.js +2 -1
  55. package/build/module/geometry.js.map +1 -1
  56. package/build/module/sdk.d.ts.map +1 -1
  57. package/build/module/sdk.js +10 -3
  58. package/build/module/sdk.js.map +1 -1
  59. package/build/search/ExpoGaodeMapSearchModule.d.ts +1 -1
  60. package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -1
  61. package/build/search/ExpoGaodeMapSearchModule.js.map +1 -1
  62. package/build/types/common.types.d.ts +1 -1
  63. package/build/types/common.types.d.ts.map +1 -1
  64. package/build/types/common.types.js.map +1 -1
  65. package/build/types/index.d.ts +1 -1
  66. package/build/types/index.d.ts.map +1 -1
  67. package/build/types/index.js.map +1 -1
  68. package/build/types/map-view.types.d.ts +11 -0
  69. package/build/types/map-view.types.d.ts.map +1 -1
  70. package/build/types/map-view.types.js.map +1 -1
  71. package/build/utils/ErrorHandler.d.ts +4 -0
  72. package/build/utils/ErrorHandler.d.ts.map +1 -1
  73. package/build/utils/ErrorHandler.js +8 -0
  74. package/build/utils/ErrorHandler.js.map +1 -1
  75. package/build/utils/GeoUtils.d.ts +2 -2
  76. package/build/utils/GeoUtils.d.ts.map +1 -1
  77. package/build/utils/GeoUtils.js +3 -3
  78. package/build/utils/GeoUtils.js.map +1 -1
  79. package/build/utils/RouteUtils.d.ts.map +1 -1
  80. package/build/utils/RouteUtils.js +5 -2
  81. package/build/utils/RouteUtils.js.map +1 -1
  82. package/ios/ExpoGaodeMapView.swift +106 -10
  83. package/ios/ExpoGaodeMapViewModule.swift +5 -0
  84. package/ios/overlays/MarkerView.swift +104 -32
  85. package/package.json +14 -8
  86. package/plugin/build/withGaodeMap.js +3 -2
  87. package/shared/cpp/ClusterEngine.cpp +4 -14
  88. package/shared/cpp/ClusterEngine.hpp +1 -12
  89. package/shared/cpp/ClusterTypes.hpp +18 -0
  90. package/shared/cpp/ColorParser.cpp +22 -3
  91. package/shared/cpp/GeometryEngine.cpp +1 -1
  92. package/shared/cpp/QuadTree.hpp +1 -1
@@ -28,6 +28,12 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
28
28
  var showsScale: Bool = true
29
29
  /// 是否启用缩放手势
30
30
  var isZoomEnabled: Bool = true
31
+ /// 缩放手势锚点:gesture 跟随手势中心点;center 固定地图中心点
32
+ var zoomGestureAnchor: String = "gesture" {
33
+ didSet {
34
+ applyZoomGestureSettings()
35
+ }
36
+ }
31
37
  /// 是否启用滚动手势
32
38
  var isScrollEnabled: Bool = true
33
39
  /// 是否启用旋转手势
@@ -113,9 +119,15 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
113
119
  private var pendingCameraMoveData: [String: Any]?
114
120
  private var cameraMoveDispatchWorkItem: DispatchWorkItem?
115
121
  private var lastCameraMoveDispatchTime: CFTimeInterval = 0
122
+ /// iOS 缩放惯性/连续 setZoom 可能多次触发 regionDidChange,合并后再派发 idle。
123
+ private var pendingCameraIdleData: [String: Any]?
124
+ private var cameraIdleDispatchWorkItem: DispatchWorkItem?
125
+ private let cameraIdleDebounceMs = 160
116
126
 
117
127
  /// 缩放手势识别器(用于模拟惯性)
118
128
  private var pinchGesture: UIPinchGestureRecognizer!
129
+ private var centerAnchoredPinchStartZoom: CGFloat?
130
+ private var centerAnchoredZoomCoordinate: CLLocationCoordinate2D?
119
131
 
120
132
  // 惯性动画相关属性
121
133
  private var displayLink: CADisplayLink?
@@ -361,12 +373,11 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
361
373
  uiManager.setMapType(1)
362
374
  uiManager.setShowsScale(showsScale)
363
375
  uiManager.setShowsCompass(showsCompass)
364
- uiManager.setZoomEnabled(isZoomEnabled)
376
+ applyZoomGestureSettings()
365
377
  uiManager.setScrollEnabled(isScrollEnabled)
366
378
  uiManager.setRotateEnabled(isRotateEnabled)
367
379
  uiManager.setTiltEnabled(isTiltEnabled)
368
380
  uiManager.setShowsUserLocation(showsUserLocation, followUser: followUserLocation)
369
- updatePinchGestureState()
370
381
  }
371
382
 
372
383
  /**
@@ -390,7 +401,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
390
401
 
391
402
  uiManager.setShowsScale(showsScale)
392
403
  uiManager.setShowsCompass(showsCompass)
393
- uiManager.setZoomEnabled(isZoomEnabled)
404
+ applyZoomGestureSettings()
394
405
  uiManager.setScrollEnabled(isScrollEnabled)
395
406
  uiManager.setRotateEnabled(isRotateEnabled)
396
407
  uiManager.setTiltEnabled(isTiltEnabled)
@@ -398,13 +409,12 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
398
409
  uiManager.setShowsTraffic(showsTraffic)
399
410
  uiManager.setShowsBuildings(showsBuildings)
400
411
  uiManager.setShowsIndoorMap(showsIndoorMap)
401
- updatePinchGestureState()
402
412
  mapView.distanceFilter = distanceFilter
403
413
  mapView.headingFilter = headingFilter
404
414
  if let customMapStyleData {
405
415
  uiManager.setCustomMapStyle(customMapStyleData)
406
416
  }
407
-
417
+
408
418
  // 更新苹果地图样式
409
419
  updateAppleMapStyle()
410
420
 
@@ -439,6 +449,13 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
439
449
  // MARK: - 手势处理
440
450
 
441
451
  @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
452
+ guard isZoomEnabled else { return }
453
+
454
+ if zoomGestureAnchor == "center" {
455
+ handleCenterAnchoredPinch(gesture)
456
+ return
457
+ }
458
+
442
459
  if gesture.state == .began {
443
460
  // 手势开始,立即停止之前的惯性动画,避免冲突
444
461
  stopInertiaAnimation()
@@ -455,6 +472,44 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
455
472
  }
456
473
  }
457
474
  }
475
+
476
+ private func handleCenterAnchoredPinch(_ gesture: UIPinchGestureRecognizer) {
477
+ switch gesture.state {
478
+ case .began:
479
+ stopInertiaAnimation()
480
+ centerAnchoredPinchStartZoom = mapView.zoomLevel
481
+ centerAnchoredZoomCoordinate = mapView.centerCoordinate
482
+ case .changed:
483
+ let startZoom = centerAnchoredPinchStartZoom ?? mapView.zoomLevel
484
+ let center = centerAnchoredZoomCoordinate ?? mapView.centerCoordinate
485
+ let zoomDelta = log2(Double(max(gesture.scale, 0.0001)))
486
+ applyCenterAnchoredZoom(CGFloat(Double(startZoom) + zoomDelta), center: center)
487
+ case .ended, .cancelled, .failed:
488
+ centerAnchoredPinchStartZoom = nil
489
+ let velocity = gesture.velocity
490
+ if abs(velocity) > 0.1 {
491
+ zoomVelocity = Double(velocity) * 0.02
492
+ if centerAnchoredZoomCoordinate == nil {
493
+ centerAnchoredZoomCoordinate = mapView.centerCoordinate
494
+ }
495
+ startInertiaAnimation()
496
+ } else {
497
+ centerAnchoredZoomCoordinate = nil
498
+ }
499
+ default:
500
+ break
501
+ }
502
+ }
503
+
504
+ private func applyCenterAnchoredZoom(_ zoom: CGFloat, center: CLLocationCoordinate2D) {
505
+ let validZoom = max(mapView.minZoomLevel, min(mapView.maxZoomLevel, zoom))
506
+ let status = MAMapStatus()
507
+ status.centerCoordinate = center
508
+ status.zoomLevel = validZoom
509
+ status.rotationDegree = mapView.rotationDegree
510
+ status.cameraDegree = mapView.cameraDegree
511
+ mapView.setMapStatus(status, animated: false, duration: 0)
512
+ }
458
513
 
459
514
  private func startInertiaAnimation() {
460
515
  stopInertiaAnimation()
@@ -476,12 +531,21 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
476
531
  // 碰到边界,停止动画
477
532
  newZoom = max(mapView.minZoomLevel, min(mapView.maxZoomLevel, newZoom))
478
533
  stopInertiaAnimation()
479
- mapView.setZoomLevel(newZoom, animated: false)
534
+ if zoomGestureAnchor == "center" {
535
+ applyCenterAnchoredZoom(newZoom, center: centerAnchoredZoomCoordinate ?? mapView.centerCoordinate)
536
+ centerAnchoredZoomCoordinate = nil
537
+ } else {
538
+ mapView.setZoomLevel(newZoom, animated: false)
539
+ }
480
540
  return
481
541
  }
482
542
 
483
543
  // 更新地图缩放级别(animated: false 以保证逐帧控制的流畅性)
484
- mapView.setZoomLevel(newZoom, animated: false)
544
+ if zoomGestureAnchor == "center" {
545
+ applyCenterAnchoredZoom(newZoom, center: centerAnchoredZoomCoordinate ?? mapView.centerCoordinate)
546
+ } else {
547
+ mapView.setZoomLevel(newZoom, animated: false)
548
+ }
485
549
 
486
550
  // 减速(应用摩擦力)
487
551
  zoomVelocity *= friction
@@ -489,6 +553,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
489
553
  // 停止条件
490
554
  if abs(zoomVelocity) < velocityThreshold {
491
555
  stopInertiaAnimation()
556
+ centerAnchoredZoomCoordinate = nil
492
557
  }
493
558
  }
494
559
 
@@ -806,6 +871,9 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
806
871
  pendingCameraMoveData = nil
807
872
  cameraMoveDispatchWorkItem?.cancel()
808
873
  cameraMoveDispatchWorkItem = nil
874
+ pendingCameraIdleData = nil
875
+ cameraIdleDispatchWorkItem?.cancel()
876
+ cameraIdleDispatchWorkItem = nil
809
877
  if let privacyObserver {
810
878
  NotificationCenter.default.removeObserver(privacyObserver)
811
879
  }
@@ -879,13 +947,20 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
879
947
  pinchGesture.delegate = self
880
948
  resolvedMapView.addGestureRecognizer(pinchGesture)
881
949
  self.pinchGesture = pinchGesture
882
- updatePinchGestureState()
950
+ applyZoomGestureSettings()
883
951
  }
884
952
 
885
- private func updatePinchGestureState() {
953
+ private func applyZoomGestureSettings() {
954
+ uiManager?.setZoomEnabled(isZoomEnabled && zoomGestureAnchor != "center")
886
955
  pinchGesture?.isEnabled = isZoomEnabled
956
+ if zoomGestureAnchor != "center" {
957
+ centerAnchoredPinchStartZoom = nil
958
+ centerAnchoredZoomCoordinate = nil
959
+ }
887
960
  if !isZoomEnabled {
888
961
  stopInertiaAnimation()
962
+ centerAnchoredPinchStartZoom = nil
963
+ centerAnchoredZoomCoordinate = nil
889
964
  }
890
965
  }
891
966
  }
@@ -958,7 +1033,7 @@ extension ExpoGaodeMapView {
958
1033
  */
959
1034
  public func mapView(_ mapView: MAMapView, regionDidChangeAnimated animated: Bool) {
960
1035
  flushPendingCameraMoveEvent()
961
- onCameraIdle(buildCameraEventData(for: mapView))
1036
+ scheduleCameraIdleEvent(buildCameraEventData(for: mapView))
962
1037
 
963
1038
  // 这里的 overlayViews 是 [UIView] 类型,可能包含 ClusterView
964
1039
  for view in overlayViews {
@@ -1038,6 +1113,27 @@ extension ExpoGaodeMapView {
1038
1113
  lastCameraMoveDispatchTime = timestamp
1039
1114
  onCameraMove(eventData)
1040
1115
  }
1116
+
1117
+ private func scheduleCameraIdleEvent(_ eventData: [String: Any]) {
1118
+ pendingCameraIdleData = eventData
1119
+ cameraIdleDispatchWorkItem?.cancel()
1120
+
1121
+ let workItem = DispatchWorkItem { [weak self] in
1122
+ guard let self = self, let latestEventData = self.pendingCameraIdleData else {
1123
+ return
1124
+ }
1125
+
1126
+ self.pendingCameraIdleData = nil
1127
+ self.cameraIdleDispatchWorkItem = nil
1128
+ self.onCameraIdle(latestEventData)
1129
+ }
1130
+
1131
+ cameraIdleDispatchWorkItem = workItem
1132
+ DispatchQueue.main.asyncAfter(
1133
+ deadline: .now() + Double(cameraIdleDebounceMs) / 1000.0,
1134
+ execute: workItem
1135
+ )
1136
+ }
1041
1137
 
1042
1138
  /**
1043
1139
  * 地图单击事件
@@ -43,6 +43,11 @@ public class ExpoGaodeMapViewModule: Module {
43
43
  Prop("zoomGesturesEnabled") { (view: ExpoGaodeMapView, enabled: Bool) in
44
44
  view.isZoomEnabled = enabled
45
45
  }
46
+
47
+ // 缩放手势锚点:gesture 使用手势焦点,center 固定地图中心点
48
+ Prop("zoomGestureAnchor") { (view: ExpoGaodeMapView, anchor: String?) in
49
+ view.zoomGestureAnchor = anchor == "center" ? "center" : "gesture"
50
+ }
46
51
 
47
52
  Prop("scrollGesturesEnabled") { (view: ExpoGaodeMapView, enabled: Bool) in
48
53
  view.isScrollEnabled = enabled
@@ -77,6 +77,8 @@ class MarkerView: ExpoView {
77
77
  private var pendingSubviewRefreshTask: DispatchWorkItem?
78
78
  /// 最近一次应用到 annotationView 的 children 结构签名
79
79
  private var lastRenderedChildrenSignature: String?
80
+ /// 最近一次成功渲染的 children 图片,用于缓存未命中时避免闪烁
81
+ private var lastRenderedChildrenImage: UIImage?
80
82
  /// 上次设置的地图引用(防止重复调用)
81
83
  private weak var lastSetMapView: MAMapView?
82
84
 
@@ -156,7 +158,11 @@ class MarkerView: ExpoView {
156
158
  func setCacheKey(_ key: String?) {
157
159
  guard cacheKey != key else { return }
158
160
  self.cacheKey = key
159
- refreshAnnotationAppearance(invalidateChildrenCache: !subviews.isEmpty)
161
+ if !subviews.isEmpty {
162
+ scheduleChildrenImageRefresh()
163
+ } else {
164
+ refreshAnnotationAppearance()
165
+ }
160
166
  }
161
167
 
162
168
  /**
@@ -233,9 +239,8 @@ class MarkerView: ExpoView {
233
239
  let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
234
240
  let key = childrenCacheKey(for: size)
235
241
  if let cached = IconBitmapCache.shared.image(forKey: key) {
236
- annotationView?.image = cached
237
242
  if let annotationView = annotationView {
238
- applyCenterOffset(to: annotationView, defaultOffset: .zero)
243
+ applyChildrenImage(cached, to: annotationView, signature: childrenRenderSignature())
239
244
  }
240
245
  return annotationView
241
246
  }
@@ -244,9 +249,8 @@ class MarkerView: ExpoView {
244
249
  DispatchQueue.main.async { [weak self, weak annotationView] in
245
250
  guard let self = self, let annotationView = annotationView else { return }
246
251
  guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
247
- if let generated = self.createImageFromSubviews() {
248
- annotationView.image = generated
249
- self.applyCenterOffset(to: annotationView, defaultOffset: .zero)
252
+ if let generated = self.createImageFromSubviews(size: size) {
253
+ self.applyChildrenImage(generated, to: annotationView, signature: self.childrenRenderSignature())
250
254
  }
251
255
  }
252
256
  return annotationView
@@ -307,9 +311,7 @@ class MarkerView: ExpoView {
307
311
 
308
312
  // 🔑 如果有 children,使用自定义视图
309
313
  if self.subviews.count > 0 {
310
- let reuseToken = cacheKey?.replacingOccurrences(of: "|", with: "_")
311
- ?? String(ObjectIdentifier(self).hashValue)
312
- let reuseId = "custom_marker_children_\(reuseToken)" + (growAnimation ? "_grow" : "")
314
+ let reuseId = "custom_marker_children_\(ObjectIdentifier(self).hashValue)" + (growAnimation ? "_grow" : "")
313
315
  var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
314
316
  if annotationView == nil {
315
317
  if growAnimation {
@@ -331,37 +333,44 @@ class MarkerView: ExpoView {
331
333
  // 生成 cacheKey 或 fallback 到 identifier
332
334
  let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 40))
333
335
  let key = childrenCacheKey(for: size)
336
+ let signature = childrenRenderSignature()
334
337
 
335
338
  // 1) 如果缓存命中,直接同步返回图像(fast path)
336
339
  if let cached = IconBitmapCache.shared.image(forKey: key) {
337
- annotationView?.image = cached
338
340
  if let annotationView = annotationView {
339
- applyCenterOffset(to: annotationView, defaultOffset: .zero)
341
+ applyChildrenImage(cached, to: annotationView, signature: signature)
340
342
  }
341
343
  return annotationView
342
344
  }
343
345
 
344
- // 2) 缓存未命中:返回占位(透明),并异步在主线程生成图像然后回填
345
- UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
346
- let transparentImage = UIGraphicsGetImageFromCurrentImageContext()
347
- UIGraphicsEndImageContext()
348
- annotationView?.image = transparentImage
346
+ // 2) 缓存未命中:先尝试同步渲染,失败时保留旧图,避免点击切态闪烁。
347
+ if let generated = createImageFromSubviews(size: size) {
348
+ if let annotationView = annotationView {
349
+ applyChildrenImage(generated, to: annotationView, signature: signature)
350
+ }
351
+ return annotationView
352
+ }
349
353
 
350
- // 🔑 修复:延长延迟时间,给 React Native Image 更多加载时间
354
+ if annotationView?.image == nil, let previousImage = lastRenderedChildrenImage {
355
+ annotationView?.image = previousImage
356
+ if let annotationView = annotationView {
357
+ applyCenterOffset(to: annotationView, defaultOffset: .zero)
358
+ }
359
+ }
360
+
361
+ // 图片内容可能尚未就绪,保留旧图并延迟重试,避免透明闪烁。
351
362
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak annotationView] in
352
363
  guard let self = self, let annotationView = annotationView else { return }
353
364
  guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
354
365
  // 再次检查缓存(避免重复渲染)
355
366
  if let cached = IconBitmapCache.shared.image(forKey: key) {
356
- annotationView.image = cached
357
- self.applyCenterOffset(to: annotationView, defaultOffset: .zero)
367
+ self.applyChildrenImage(cached, to: annotationView, signature: signature)
358
368
  return
359
369
  }
360
370
 
361
371
  // 调用你的原生渲染逻辑(保留空白检测、多次 layout)
362
- if let generated = self.createImageFromSubviews() {
363
- annotationView.image = generated
364
- self.applyCenterOffset(to: annotationView, defaultOffset: .zero)
372
+ if let generated = self.createImageFromSubviews(size: size) {
373
+ self.applyChildrenImage(generated, to: annotationView, signature: signature)
365
374
  } else if self.hasPendingImageContent() {
366
375
  self.scheduleSubviewRefresh(allowFallbackToDefault: false)
367
376
  }
@@ -499,8 +508,8 @@ class MarkerView: ExpoView {
499
508
  /**
500
509
  * 将子视图转换为图片
501
510
  */
502
- private func createImageFromSubviews() -> UIImage? {
503
- let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
511
+ private func createImageFromSubviews(size explicitSize: CGSize? = nil) -> UIImage? {
512
+ let size = explicitSize ?? resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
504
513
  let key = childrenCacheKey(for: size)
505
514
 
506
515
  if let cachedImage = IconBitmapCache.shared.image(forKey: key) {
@@ -549,6 +558,70 @@ class MarkerView: ExpoView {
549
558
  return image
550
559
  }
551
560
 
561
+ private func applyChildrenImage(_ image: UIImage, to annotationView: MAAnnotationView, signature: String? = nil) {
562
+ annotationView.image = image
563
+ applyCenterOffset(to: annotationView, defaultOffset: .zero)
564
+ annotationView.canShowCallout = false
565
+ annotationView.isDraggable = draggable
566
+ lastRenderedChildrenImage = image
567
+ if let resolvedSignature = signature {
568
+ lastRenderedChildrenSignature = resolvedSignature
569
+ }
570
+ }
571
+
572
+ private func refreshChildrenImageInPlace(invalidateChildrenCache: Bool = false) {
573
+ guard !isRemoving else { return }
574
+
575
+ let refresh = { [weak self] in
576
+ guard let self = self, !self.isRemoving, !self.subviews.isEmpty else { return }
577
+
578
+ if invalidateChildrenCache {
579
+ self.lastRenderedChildrenSignature = nil
580
+ self.invalidateCurrentChildrenCache()
581
+ }
582
+
583
+ guard let targetView = self.annotationView ?? self.animatedAnnotationView else {
584
+ self.refreshAnnotationAppearance(invalidateChildrenCache: invalidateChildrenCache)
585
+ return
586
+ }
587
+
588
+ let size = self.resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 40))
589
+ let key = self.childrenCacheKey(for: size)
590
+ let signature = self.childrenRenderSignature()
591
+
592
+ if let cached = IconBitmapCache.shared.image(forKey: key) {
593
+ self.applyChildrenImage(cached, to: targetView, signature: signature)
594
+ return
595
+ }
596
+
597
+ if let generated = self.createImageFromSubviews(size: size) {
598
+ self.applyChildrenImage(generated, to: targetView, signature: signature)
599
+ return
600
+ }
601
+
602
+ if self.hasPendingImageContent() {
603
+ self.scheduleSubviewRefresh(allowFallbackToDefault: false)
604
+ }
605
+ }
606
+
607
+ if Thread.isMainThread {
608
+ refresh()
609
+ } else {
610
+ DispatchQueue.main.async(execute: refresh)
611
+ }
612
+ }
613
+
614
+ private func scheduleChildrenImageRefresh(invalidateChildrenCache: Bool = false) {
615
+ pendingSubviewRefreshTask?.cancel()
616
+
617
+ let task = DispatchWorkItem { [weak self] in
618
+ self?.refreshChildrenImageInPlace(invalidateChildrenCache: invalidateChildrenCache)
619
+ }
620
+
621
+ pendingSubviewRefreshTask = task
622
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.02, execute: task)
623
+ }
624
+
552
625
  private func iconCacheKey(for iconUri: String) -> String {
553
626
  let baseKey = cacheKey ?? "icon|\(iconUri)"
554
627
  return "\(baseKey)|\(Int(iconWidth.rounded()))x\(Int(iconHeight.rounded()))"
@@ -769,6 +842,8 @@ class MarkerView: ExpoView {
769
842
  animatedAnnotationView?.annotation = nil
770
843
  annotationView = nil
771
844
  animatedAnnotationView = nil
845
+ lastRenderedChildrenImage = nil
846
+ lastRenderedChildrenSignature = nil
772
847
  isAnimating = false
773
848
  lastSetMapView = nil
774
849
  self.mapView = nil
@@ -850,12 +925,9 @@ class MarkerView: ExpoView {
850
925
  return
851
926
  }
852
927
 
853
- if let image = createImageFromSubviews() {
854
- annotationView.image = image
855
- applyCenterOffset(to: annotationView, defaultOffset: .zero)
856
- annotationView.canShowCallout = false
857
- annotationView.isDraggable = draggable
858
- lastRenderedChildrenSignature = signature
928
+ let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 40))
929
+ if let image = createImageFromSubviews(size: size) {
930
+ applyChildrenImage(image, to: annotationView, signature: signature)
859
931
  }
860
932
  }
861
933
 
@@ -1023,7 +1095,7 @@ class MarkerView: ExpoView {
1023
1095
  guard contentWidth != width else { return }
1024
1096
  contentWidth = width
1025
1097
  if !subviews.isEmpty {
1026
- refreshAnnotationAppearance(invalidateChildrenCache: true)
1098
+ scheduleChildrenImageRefresh(invalidateChildrenCache: true)
1027
1099
  }
1028
1100
  }
1029
1101
 
@@ -1031,7 +1103,7 @@ class MarkerView: ExpoView {
1031
1103
  guard contentHeight != height else { return }
1032
1104
  contentHeight = height
1033
1105
  if !subviews.isEmpty {
1034
- refreshAnnotationAppearance(invalidateChildrenCache: true)
1106
+ scheduleChildrenImageRefresh(invalidateChildrenCache: true)
1035
1107
  }
1036
1108
  }
1037
1109
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-gaode-map",
3
- "version": "2.2.37",
4
- "description": "A full-featured React Native AMap (Gaode Map) library for Expo, including map display, location, overlays, offline maps, and geometry utilities.",
3
+ "version": "2.2.39-next.0",
4
+ "description": "Expo Modules AMap (Gaode Map) stack for Expo/React Native: Config Plugin, New Architecture support, maps, location, search, offline maps, and react-native-amap3d migration guidance.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "files": [
@@ -18,7 +18,7 @@
18
18
  "build": "expo-module build && yarn build:plugin",
19
19
  "build:plugin": "tsc --project plugin/tsconfig.json",
20
20
  "clean": "expo-module clean && rm -rf plugin/build",
21
- "lint": "expo-module lint",
21
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
22
22
  "test": "npx jest --no-watchman --config jest.config.js --runInBand",
23
23
  "prepare": "expo-module prepare && yarn build:plugin",
24
24
  "postinstall": "node scripts/check-expo-modules.js",
@@ -38,9 +38,13 @@
38
38
  "expo-map",
39
39
  "react-native-amap",
40
40
  "react-native-amap3d",
41
+ "react-native-amap3d-alternative",
41
42
  "react-native-gaode-map",
42
43
  "rn-amap",
43
44
  "rn-gaode-map",
45
+ "expo-gaode",
46
+ "eas-build",
47
+ "fabric",
44
48
  "gaode-map",
45
49
  "gaode",
46
50
  "amap",
@@ -71,13 +75,15 @@
71
75
  "@babel/runtime": "^7.25.7",
72
76
  "@testing-library/react-native": "^13.3.3",
73
77
  "@types/jest": "^29.5.14",
78
+ "@types/node": "^22.18.0",
74
79
  "@types/react": "~19.2.10",
75
- "expo": "^55.0.8",
76
- "expo-module-scripts": "^55.0.2",
80
+ "eslint": "^9.25.1",
81
+ "expo": "^57.0.1",
82
+ "expo-module-scripts": "^56.0.3",
77
83
  "jest": "~29.7.0",
78
- "jest-expo": "~55.0.11",
79
- "react-native": "0.83.2",
80
- "typescript": "^5.9.3"
84
+ "jest-expo": "~57.0.0",
85
+ "react-native": "0.86.0",
86
+ "typescript": "^6.0.3"
81
87
  },
82
88
  "peerDependencies": {
83
89
  "expo": "*",
@@ -146,8 +146,9 @@ const withGaodeMapAndroidManifest = (config, props) => {
146
146
  else {
147
147
  // 更新现有的 API Key
148
148
  const apiKeyIndex = mainApplication['meta-data'].findIndex((item) => item.$?.['android:name'] === 'com.amap.api.v2.apikey');
149
- if (apiKeyIndex !== -1) {
150
- mainApplication['meta-data'][apiKeyIndex].$ = {
149
+ const apiKeyMetaData = mainApplication['meta-data'][apiKeyIndex];
150
+ if (apiKeyMetaData) {
151
+ apiKeyMetaData.$ = {
151
152
  'android:name': 'com.amap.api.v2.apikey',
152
153
  'android:value': props.androidKey,
153
154
  };
@@ -1,4 +1,5 @@
1
1
  #include "ClusterEngine.hpp"
2
+ #include "GeometryEngine.hpp"
2
3
  #include "QuadTree.hpp"
3
4
  #include <cmath>
4
5
  #include <algorithm>
@@ -11,19 +12,8 @@ static inline double cluster_toRadians(double degrees) {
11
12
  return degrees * 0.017453292519943295;
12
13
  }
13
14
 
14
- static double haversineMeters(const ClusterPoint& a, const ClusterPoint& b) {
15
- const double lat1 = cluster_toRadians(a.lat);
16
- const double lat2 = cluster_toRadians(b.lat);
17
- const double dLat = lat2 - lat1;
18
- const double dLon = cluster_toRadians(b.lon - a.lon);
19
-
20
- const double sinHalfLat = std::sin(dLat * 0.5);
21
- const double sinHalfLon = std::sin(dLon * 0.5);
22
-
23
- const double h = sinHalfLat * sinHalfLat + std::cos(lat1) * std::cos(lat2) * sinHalfLon * sinHalfLon;
24
- const double c = 2.0 * std::atan2(std::sqrt(h), std::sqrt(1.0 - h));
25
-
26
- return 6371000.0 * c;
15
+ static double clusterDistanceMeters(const ClusterPoint& a, const ClusterPoint& b) {
16
+ return calculateDistance(a.lat, a.lon, b.lat, b.lon);
27
17
  }
28
18
 
29
19
  std::vector<ClusterOutput> clusterPoints(const std::vector<ClusterPoint>& points, double radiusMeters) {
@@ -95,7 +85,7 @@ std::vector<ClusterOutput> clusterPoints(const std::vector<ClusterPoint>& points
95
85
  if (globalVisited[neighbor.index]) continue;
96
86
 
97
87
  // Precise check
98
- if (haversineMeters(p, neighbor) <= radiusMeters) {
88
+ if (clusterDistanceMeters(p, neighbor) <= radiusMeters) {
99
89
  cluster.indices.push_back(neighbor.index);
100
90
  globalVisited[neighbor.index] = true;
101
91
  }
@@ -1,20 +1,9 @@
1
1
  #pragma once
2
2
 
3
- #include <vector>
3
+ #include "ClusterTypes.hpp"
4
4
 
5
5
  namespace gaodemap {
6
6
 
7
- struct ClusterPoint {
8
- double lat;
9
- double lon;
10
- int index;
11
- };
12
-
13
- struct ClusterOutput {
14
- int centerIndex;
15
- std::vector<int> indices;
16
- };
17
-
18
7
  std::vector<ClusterOutput> clusterPoints(const std::vector<ClusterPoint>& points, double radiusMeters);
19
8
 
20
9
  }
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #include <vector>
4
+
5
+ namespace gaodemap {
6
+
7
+ struct ClusterPoint {
8
+ double lat;
9
+ double lon;
10
+ int index;
11
+ };
12
+
13
+ struct ClusterOutput {
14
+ int centerIndex;
15
+ std::vector<int> indices;
16
+ };
17
+
18
+ }
@@ -1,5 +1,6 @@
1
1
  #include "ColorParser.hpp"
2
2
  #include <algorithm>
3
+ #include <cctype>
3
4
  #include <sstream>
4
5
  #include <vector>
5
6
  #include <map>
@@ -109,13 +110,29 @@ static const std::map<std::string, uint32_t> NAMED_COLORS = {
109
110
  {"transparent", 0x00000000}
110
111
  };
111
112
 
113
+ static bool isSpace(unsigned char ch) {
114
+ return std::isspace(ch) != 0;
115
+ }
116
+
117
+ static char toLower(unsigned char ch) {
118
+ return static_cast<char>(std::tolower(ch));
119
+ }
120
+
121
+ static bool isHexDigit(unsigned char ch) {
122
+ return std::isxdigit(ch) != 0;
123
+ }
124
+
112
125
  uint32_t parseColor(const std::string& colorString) {
113
126
  std::string str = colorString;
114
127
  // Remove whitespace
115
- str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end());
128
+ str.erase(std::remove_if(str.begin(), str.end(), [](unsigned char ch) {
129
+ return isSpace(ch);
130
+ }), str.end());
116
131
  // Lowercase for named colors check
117
132
  std::string lowerStr = str;
118
- std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), ::tolower);
133
+ std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), [](unsigned char ch) {
134
+ return toLower(ch);
135
+ });
119
136
 
120
137
  if (NAMED_COLORS.count(lowerStr)) {
121
138
  return NAMED_COLORS.at(lowerStr);
@@ -125,7 +142,9 @@ uint32_t parseColor(const std::string& colorString) {
125
142
  return parseRgba(str); // pass original str with potential spaces if needed, but we removed them
126
143
  }
127
144
 
128
- if (lowerStr.find("#") == 0 || std::all_of(lowerStr.begin(), lowerStr.end(), ::isxdigit)) {
145
+ if (lowerStr.find("#") == 0 || std::all_of(lowerStr.begin(), lowerStr.end(), [](unsigned char ch) {
146
+ return isHexDigit(ch);
147
+ })) {
129
148
  return parseHex(str);
130
149
  }
131
150