expo-gaode-map 2.2.38 → 2.2.39

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 (79) hide show
  1. package/android/build.gradle +6 -31
  2. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +35 -12
  3. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapViewModule.kt +2 -0
  4. package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +8 -0
  5. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +48 -7
  6. package/build/ExpoGaodeMapOfflineModule.js +1 -1
  7. package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
  8. package/build/ExpoGaodeMapView.js +2 -8
  9. package/build/ExpoGaodeMapView.js.map +1 -1
  10. package/build/components/AreaMaskOverlay.d.ts +1 -2
  11. package/build/components/AreaMaskOverlay.d.ts.map +1 -1
  12. package/build/components/AreaMaskOverlay.js +2 -1
  13. package/build/components/AreaMaskOverlay.js.map +1 -1
  14. package/build/components/FoldableMapView.d.ts +3 -2
  15. package/build/components/FoldableMapView.d.ts.map +1 -1
  16. package/build/components/FoldableMapView.js +2 -1
  17. package/build/components/FoldableMapView.js.map +1 -1
  18. package/build/components/MapUI.js +2 -1
  19. package/build/components/MapUI.js.map +1 -1
  20. package/build/components/RouteOverlay.d.ts +1 -2
  21. package/build/components/RouteOverlay.d.ts.map +1 -1
  22. package/build/components/RouteOverlay.js +5 -5
  23. package/build/components/RouteOverlay.js.map +1 -1
  24. package/build/components/overlays/Circle.d.ts +1 -1
  25. package/build/components/overlays/Circle.d.ts.map +1 -1
  26. package/build/components/overlays/Circle.js +2 -1
  27. package/build/components/overlays/Circle.js.map +1 -1
  28. package/build/components/overlays/Cluster.d.ts +1 -1
  29. package/build/components/overlays/Cluster.d.ts.map +1 -1
  30. package/build/components/overlays/Cluster.js +2 -1
  31. package/build/components/overlays/Cluster.js.map +1 -1
  32. package/build/components/overlays/HeatMap.d.ts +1 -1
  33. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  34. package/build/components/overlays/HeatMap.js +2 -1
  35. package/build/components/overlays/HeatMap.js.map +1 -1
  36. package/build/components/overlays/Marker.d.ts +1 -1
  37. package/build/components/overlays/Marker.d.ts.map +1 -1
  38. package/build/components/overlays/Marker.js +8 -7
  39. package/build/components/overlays/Marker.js.map +1 -1
  40. package/build/components/overlays/MultiPoint.d.ts +1 -2
  41. package/build/components/overlays/MultiPoint.d.ts.map +1 -1
  42. package/build/components/overlays/MultiPoint.js +2 -1
  43. package/build/components/overlays/MultiPoint.js.map +1 -1
  44. package/build/components/overlays/Polygon.d.ts +1 -1
  45. package/build/components/overlays/Polygon.d.ts.map +1 -1
  46. package/build/components/overlays/Polygon.js +2 -1
  47. package/build/components/overlays/Polygon.js.map +1 -1
  48. package/build/components/overlays/Polyline.d.ts +1 -1
  49. package/build/components/overlays/Polyline.d.ts.map +1 -1
  50. package/build/components/overlays/Polyline.js +2 -1
  51. package/build/components/overlays/Polyline.js.map +1 -1
  52. package/build/module/geometry.d.ts.map +1 -1
  53. package/build/module/geometry.js +2 -1
  54. package/build/module/geometry.js.map +1 -1
  55. package/build/search/ExpoGaodeMapSearchModule.d.ts +1 -1
  56. package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -1
  57. package/build/search/ExpoGaodeMapSearchModule.js.map +1 -1
  58. package/build/types/common.types.d.ts +1 -1
  59. package/build/types/common.types.d.ts.map +1 -1
  60. package/build/types/common.types.js.map +1 -1
  61. package/build/types/index.d.ts +1 -1
  62. package/build/types/index.d.ts.map +1 -1
  63. package/build/types/index.js.map +1 -1
  64. package/build/types/map-view.types.d.ts +13 -2
  65. package/build/types/map-view.types.d.ts.map +1 -1
  66. package/build/types/map-view.types.js.map +1 -1
  67. package/build/utils/GeoUtils.d.ts +2 -2
  68. package/build/utils/GeoUtils.d.ts.map +1 -1
  69. package/build/utils/GeoUtils.js +3 -3
  70. package/build/utils/GeoUtils.js.map +1 -1
  71. package/build/utils/RouteUtils.d.ts.map +1 -1
  72. package/build/utils/RouteUtils.js +5 -2
  73. package/build/utils/RouteUtils.js.map +1 -1
  74. package/ios/ExpoGaodeMapView.swift +281 -15
  75. package/ios/ExpoGaodeMapViewModule.swift +5 -0
  76. package/ios/managers/UIManager.swift +3 -0
  77. package/ios/overlays/MarkerView.swift +205 -38
  78. package/package.json +9 -7
  79. package/plugin/build/withGaodeMap.js +3 -2
@@ -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
  /// 是否启用旋转手势
@@ -96,6 +102,8 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
96
102
  private var uiManager: UIManager!
97
103
  /// 地图是否已加载完成
98
104
  private var isMapLoaded = false
105
+ /// 定位 annotation 首次出现后是否已补应用定位样式
106
+ private var hasAppliedUserLocationStyleAfterLocationUpdate = false
99
107
  /// 初始相机是否已应用(仅应用一次,避免与运行时相机控制冲突)
100
108
  private var hasAppliedInitialCameraPosition = false
101
109
  /// 是否正在处理 annotation 选择事件
@@ -106,6 +114,8 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
106
114
  private var overlayContainer: UIView!
107
115
  /// 显式跟踪所有覆盖物视图(新架构下 subviews 可能不可靠)
108
116
  private var overlayViews: [UIView] = []
117
+ /// MarkerView 短暂 detach 时延迟注销,避免 annotation 暂时找不到自定义 view 而回退默认样式
118
+ private var pendingMarkerOverlayUnregisterTasks: [ObjectIdentifier: DispatchWorkItem] = [:]
109
119
 
110
120
  // MARK: - 事件节流控制
111
121
 
@@ -113,9 +123,15 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
113
123
  private var pendingCameraMoveData: [String: Any]?
114
124
  private var cameraMoveDispatchWorkItem: DispatchWorkItem?
115
125
  private var lastCameraMoveDispatchTime: CFTimeInterval = 0
126
+ /// iOS 缩放惯性/连续 setZoom 可能多次触发 regionDidChange,合并后再派发 idle。
127
+ private var pendingCameraIdleData: [String: Any]?
128
+ private var cameraIdleDispatchWorkItem: DispatchWorkItem?
129
+ private let cameraIdleDebounceMs = 160
116
130
 
117
131
  /// 缩放手势识别器(用于模拟惯性)
118
132
  private var pinchGesture: UIPinchGestureRecognizer!
133
+ private var centerAnchoredPinchStartZoom: CGFloat?
134
+ private var centerAnchoredZoomCoordinate: CLLocationCoordinate2D?
119
135
 
120
136
  // 惯性动画相关属性
121
137
  private var displayLink: CADisplayLink?
@@ -237,6 +253,8 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
237
253
  }
238
254
 
239
255
  private func registerOverlayView(_ view: UIView) {
256
+ cancelPendingMarkerOverlayUnregister(for: view)
257
+
240
258
  guard !overlayViews.contains(where: { $0 === view }) else {
241
259
  return
242
260
  }
@@ -244,9 +262,31 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
244
262
  }
245
263
 
246
264
  private func unregisterOverlayView(_ view: UIView) {
265
+ cancelPendingMarkerOverlayUnregister(for: view)
247
266
  overlayViews.removeAll { $0 === view }
248
267
  }
249
268
 
269
+ private func scheduleMarkerOverlayUnregister(_ markerView: MarkerView) {
270
+ cancelPendingMarkerOverlayUnregister(for: markerView)
271
+
272
+ let identifier = ObjectIdentifier(markerView)
273
+ let task = DispatchWorkItem { [weak self, weak markerView] in
274
+ guard let self = self, let markerView = markerView else { return }
275
+ self.pendingMarkerOverlayUnregisterTasks[identifier] = nil
276
+ guard markerView.superview == nil else { return }
277
+ self.unregisterOverlayView(markerView)
278
+ }
279
+
280
+ pendingMarkerOverlayUnregisterTasks[identifier] = task
281
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: task)
282
+ }
283
+
284
+ private func cancelPendingMarkerOverlayUnregister(for view: UIView) {
285
+ let identifier = ObjectIdentifier(view)
286
+ pendingMarkerOverlayUnregisterTasks[identifier]?.cancel()
287
+ pendingMarkerOverlayUnregisterTasks[identifier] = nil
288
+ }
289
+
250
290
  private func prepareOverlayViewForHosting(_ view: UIView) {
251
291
  guard !(view is MarkerView) else {
252
292
  return
@@ -322,8 +362,8 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
322
362
  // 🔑 处理所有覆盖物 - 从跟踪数组中移除并确保 native 对象也从地图移除
323
363
  // 🔑 关键修复:先从数组移除,再调用 super,防止 super 触发的事件回调中引用已卸载的视图
324
364
  if let markerView = subview as? MarkerView {
325
- unregisterOverlayView(markerView)
326
- // MarkerView 内部的 willMove(toSuperview: nil) 会处理 annotation 的移除
365
+ scheduleMarkerOverlayUnregister(markerView)
366
+ // MarkerView 内部也会延迟确认 annotation 移除,避免 transient detach 闪烁。
327
367
  } else if let circleView = subview as? CircleView {
328
368
  unregisterOverlayView(circleView)
329
369
  if let mapView, let circle = circleView.circle {
@@ -361,12 +401,12 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
361
401
  uiManager.setMapType(1)
362
402
  uiManager.setShowsScale(showsScale)
363
403
  uiManager.setShowsCompass(showsCompass)
364
- uiManager.setZoomEnabled(isZoomEnabled)
404
+ applyZoomGestureSettings()
365
405
  uiManager.setScrollEnabled(isScrollEnabled)
366
406
  uiManager.setRotateEnabled(isRotateEnabled)
367
407
  uiManager.setTiltEnabled(isTiltEnabled)
368
408
  uiManager.setShowsUserLocation(showsUserLocation, followUser: followUserLocation)
369
- updatePinchGestureState()
409
+ applyUserLocationStyle()
370
410
  }
371
411
 
372
412
  /**
@@ -390,21 +430,21 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
390
430
 
391
431
  uiManager.setShowsScale(showsScale)
392
432
  uiManager.setShowsCompass(showsCompass)
393
- uiManager.setZoomEnabled(isZoomEnabled)
433
+ applyZoomGestureSettings()
394
434
  uiManager.setScrollEnabled(isScrollEnabled)
395
435
  uiManager.setRotateEnabled(isRotateEnabled)
396
436
  uiManager.setTiltEnabled(isTiltEnabled)
397
437
  uiManager.setShowsUserLocation(showsUserLocation, followUser: followUserLocation)
438
+ applyUserLocationStyle()
398
439
  uiManager.setShowsTraffic(showsTraffic)
399
440
  uiManager.setShowsBuildings(showsBuildings)
400
441
  uiManager.setShowsIndoorMap(showsIndoorMap)
401
- updatePinchGestureState()
402
442
  mapView.distanceFilter = distanceFilter
403
443
  mapView.headingFilter = headingFilter
404
444
  if let customMapStyleData {
405
445
  uiManager.setCustomMapStyle(customMapStyleData)
406
446
  }
407
-
447
+
408
448
  // 更新苹果地图样式
409
449
  updateAppleMapStyle()
410
450
 
@@ -439,6 +479,13 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
439
479
  // MARK: - 手势处理
440
480
 
441
481
  @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
482
+ guard isZoomEnabled else { return }
483
+
484
+ if zoomGestureAnchor == "center" {
485
+ handleCenterAnchoredPinch(gesture)
486
+ return
487
+ }
488
+
442
489
  if gesture.state == .began {
443
490
  // 手势开始,立即停止之前的惯性动画,避免冲突
444
491
  stopInertiaAnimation()
@@ -455,6 +502,44 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
455
502
  }
456
503
  }
457
504
  }
505
+
506
+ private func handleCenterAnchoredPinch(_ gesture: UIPinchGestureRecognizer) {
507
+ switch gesture.state {
508
+ case .began:
509
+ stopInertiaAnimation()
510
+ centerAnchoredPinchStartZoom = mapView.zoomLevel
511
+ centerAnchoredZoomCoordinate = mapView.centerCoordinate
512
+ case .changed:
513
+ let startZoom = centerAnchoredPinchStartZoom ?? mapView.zoomLevel
514
+ let center = centerAnchoredZoomCoordinate ?? mapView.centerCoordinate
515
+ let zoomDelta = log2(Double(max(gesture.scale, 0.0001)))
516
+ applyCenterAnchoredZoom(CGFloat(Double(startZoom) + zoomDelta), center: center)
517
+ case .ended, .cancelled, .failed:
518
+ centerAnchoredPinchStartZoom = nil
519
+ let velocity = gesture.velocity
520
+ if abs(velocity) > 0.1 {
521
+ zoomVelocity = Double(velocity) * 0.02
522
+ if centerAnchoredZoomCoordinate == nil {
523
+ centerAnchoredZoomCoordinate = mapView.centerCoordinate
524
+ }
525
+ startInertiaAnimation()
526
+ } else {
527
+ centerAnchoredZoomCoordinate = nil
528
+ }
529
+ default:
530
+ break
531
+ }
532
+ }
533
+
534
+ private func applyCenterAnchoredZoom(_ zoom: CGFloat, center: CLLocationCoordinate2D) {
535
+ let validZoom = max(mapView.minZoomLevel, min(mapView.maxZoomLevel, zoom))
536
+ let status = MAMapStatus()
537
+ status.centerCoordinate = center
538
+ status.zoomLevel = validZoom
539
+ status.rotationDegree = mapView.rotationDegree
540
+ status.cameraDegree = mapView.cameraDegree
541
+ mapView.setMapStatus(status, animated: false, duration: 0)
542
+ }
458
543
 
459
544
  private func startInertiaAnimation() {
460
545
  stopInertiaAnimation()
@@ -476,12 +561,21 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
476
561
  // 碰到边界,停止动画
477
562
  newZoom = max(mapView.minZoomLevel, min(mapView.maxZoomLevel, newZoom))
478
563
  stopInertiaAnimation()
479
- mapView.setZoomLevel(newZoom, animated: false)
564
+ if zoomGestureAnchor == "center" {
565
+ applyCenterAnchoredZoom(newZoom, center: centerAnchoredZoomCoordinate ?? mapView.centerCoordinate)
566
+ centerAnchoredZoomCoordinate = nil
567
+ } else {
568
+ mapView.setZoomLevel(newZoom, animated: false)
569
+ }
480
570
  return
481
571
  }
482
572
 
483
573
  // 更新地图缩放级别(animated: false 以保证逐帧控制的流畅性)
484
- mapView.setZoomLevel(newZoom, animated: false)
574
+ if zoomGestureAnchor == "center" {
575
+ applyCenterAnchoredZoom(newZoom, center: centerAnchoredZoomCoordinate ?? mapView.centerCoordinate)
576
+ } else {
577
+ mapView.setZoomLevel(newZoom, animated: false)
578
+ }
485
579
 
486
580
  // 减速(应用摩擦力)
487
581
  zoomVelocity *= friction
@@ -489,6 +583,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
489
583
  // 停止条件
490
584
  if abs(zoomVelocity) < velocityThreshold {
491
585
  stopInertiaAnimation()
586
+ centerAnchoredZoomCoordinate = nil
492
587
  }
493
588
  }
494
589
 
@@ -568,6 +663,9 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
568
663
  func setFollowUserLocation(_ follow: Bool) {
569
664
  followUserLocation = follow
570
665
  uiManager?.setShowsUserLocation(showsUserLocation, followUser: follow)
666
+ if showsUserLocation {
667
+ applyUserLocationStyle()
668
+ }
571
669
  }
572
670
 
573
671
  func setShowsUserLocation(_ show: Bool) {
@@ -580,6 +678,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
580
678
 
581
679
  func setUserLocationRepresentation(_ config: [String: Any]) {
582
680
  userLocationRepresentation = config
681
+ hasAppliedUserLocationStyleAfterLocationUpdate = false
583
682
  if showsUserLocation {
584
683
  uiManager?.setUserLocationRepresentation(config)
585
684
  }
@@ -589,7 +688,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
589
688
  * 应用用户位置样式
590
689
  */
591
690
  private func applyUserLocationStyle() {
592
- guard let config = userLocationRepresentation else { return }
691
+ guard showsUserLocation, let config = userLocationRepresentation else { return }
593
692
  uiManager?.setUserLocationRepresentation(config)
594
693
  }
595
694
 
@@ -806,6 +905,9 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
806
905
  pendingCameraMoveData = nil
807
906
  cameraMoveDispatchWorkItem?.cancel()
808
907
  cameraMoveDispatchWorkItem = nil
908
+ pendingCameraIdleData = nil
909
+ cameraIdleDispatchWorkItem?.cancel()
910
+ cameraIdleDispatchWorkItem = nil
809
911
  if let privacyObserver {
810
912
  NotificationCenter.default.removeObserver(privacyObserver)
811
913
  }
@@ -822,6 +924,8 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
822
924
  appleMapView?.removeOverlays(appleMapView?.overlays ?? [])
823
925
 
824
926
  // 清空覆盖物数组
927
+ pendingMarkerOverlayUnregisterTasks.values.forEach { $0.cancel() }
928
+ pendingMarkerOverlayUnregisterTasks.removeAll()
825
929
  overlayViews.removeAll()
826
930
 
827
931
  // 移除所有子视图
@@ -861,6 +965,7 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
861
965
  super.addSubview(resolvedMapView)
862
966
  isMapLoaded = false
863
967
  hasAppliedInitialCameraPosition = false
968
+ hasAppliedUserLocationStyleAfterLocationUpdate = false
864
969
 
865
970
  cameraManager = CameraManager(mapView: resolvedMapView)
866
971
  uiManager = UIManager(mapView: resolvedMapView)
@@ -879,13 +984,20 @@ class ExpoGaodeMapView: ExpoView, MAMapViewDelegate, UIGestureRecognizerDelegate
879
984
  pinchGesture.delegate = self
880
985
  resolvedMapView.addGestureRecognizer(pinchGesture)
881
986
  self.pinchGesture = pinchGesture
882
- updatePinchGestureState()
987
+ applyZoomGestureSettings()
883
988
  }
884
989
 
885
- private func updatePinchGestureState() {
990
+ private func applyZoomGestureSettings() {
991
+ uiManager?.setZoomEnabled(isZoomEnabled && zoomGestureAnchor != "center")
886
992
  pinchGesture?.isEnabled = isZoomEnabled
993
+ if zoomGestureAnchor != "center" {
994
+ centerAnchoredPinchStartZoom = nil
995
+ centerAnchoredZoomCoordinate = nil
996
+ }
887
997
  if !isZoomEnabled {
888
998
  stopInertiaAnimation()
999
+ centerAnchoredPinchStartZoom = nil
1000
+ centerAnchoredZoomCoordinate = nil
889
1001
  }
890
1002
  }
891
1003
  }
@@ -937,9 +1049,37 @@ extension ExpoGaodeMapView {
937
1049
  if let styleData = customMapStyleData {
938
1050
  uiManager?.setCustomMapStyle(styleData)
939
1051
  }
1052
+
1053
+ applyUserLocationStyle()
940
1054
 
941
1055
  onLoad(["loaded": true])
942
1056
  }
1057
+
1058
+ /**
1059
+ * 定位更新回调
1060
+ */
1061
+ public func mapView(_ mapView: MAMapView, didUpdate userLocation: MAUserLocation, updatingLocation: Bool) {
1062
+ guard updatingLocation, let location = userLocation.location else { return }
1063
+
1064
+ if !hasAppliedUserLocationStyleAfterLocationUpdate {
1065
+ applyUserLocationStyle()
1066
+ hasAppliedUserLocationStyleAfterLocationUpdate = true
1067
+ }
1068
+
1069
+ let latitude = location.coordinate.latitude
1070
+ let longitude = location.coordinate.longitude
1071
+ guard latitude >= -90 && latitude <= 90,
1072
+ longitude >= -180 && longitude <= 180 else {
1073
+ return
1074
+ }
1075
+
1076
+ onLocation([
1077
+ "latitude": latitude,
1078
+ "longitude": longitude,
1079
+ "accuracy": location.horizontalAccuracy,
1080
+ "timestamp": Date().timeIntervalSince1970 * 1000
1081
+ ])
1082
+ }
943
1083
 
944
1084
  /**
945
1085
  * 地图区域即将改变时触发
@@ -958,7 +1098,7 @@ extension ExpoGaodeMapView {
958
1098
  */
959
1099
  public func mapView(_ mapView: MAMapView, regionDidChangeAnimated animated: Bool) {
960
1100
  flushPendingCameraMoveEvent()
961
- onCameraIdle(buildCameraEventData(for: mapView))
1101
+ scheduleCameraIdleEvent(buildCameraEventData(for: mapView))
962
1102
 
963
1103
  // 这里的 overlayViews 是 [UIView] 类型,可能包含 ClusterView
964
1104
  for view in overlayViews {
@@ -1038,6 +1178,27 @@ extension ExpoGaodeMapView {
1038
1178
  lastCameraMoveDispatchTime = timestamp
1039
1179
  onCameraMove(eventData)
1040
1180
  }
1181
+
1182
+ private func scheduleCameraIdleEvent(_ eventData: [String: Any]) {
1183
+ pendingCameraIdleData = eventData
1184
+ cameraIdleDispatchWorkItem?.cancel()
1185
+
1186
+ let workItem = DispatchWorkItem { [weak self] in
1187
+ guard let self = self, let latestEventData = self.pendingCameraIdleData else {
1188
+ return
1189
+ }
1190
+
1191
+ self.pendingCameraIdleData = nil
1192
+ self.cameraIdleDispatchWorkItem = nil
1193
+ self.onCameraIdle(latestEventData)
1194
+ }
1195
+
1196
+ cameraIdleDispatchWorkItem = workItem
1197
+ DispatchQueue.main.asyncAfter(
1198
+ deadline: .now() + Double(cameraIdleDebounceMs) / 1000.0,
1199
+ execute: workItem
1200
+ )
1201
+ }
1041
1202
 
1042
1203
  /**
1043
1204
  * 地图单击事件
@@ -1262,13 +1423,118 @@ extension ExpoGaodeMapView {
1262
1423
  handleGaodePoiTouch(pois)
1263
1424
  }
1264
1425
 
1426
+ private func getUserLocationAnnotationView(for mapView: MAMapView, annotation: MAAnnotation) -> MAAnnotationView? {
1427
+ guard let config = userLocationRepresentation,
1428
+ let imagePath = config["image"] as? String,
1429
+ !imagePath.isEmpty else {
1430
+ return nil
1431
+ }
1432
+
1433
+ let reuseId = "ExpoGaodeMapUserLocationStyleReuseIdentifier"
1434
+ let annotationView: MAAnnotationView
1435
+ if let reusableView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) {
1436
+ annotationView = reusableView
1437
+ } else {
1438
+ annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
1439
+ }
1440
+ annotationView.annotation = annotation
1441
+ annotationView.canShowCallout = false
1442
+
1443
+ let imageWidth = config["imageWidth"] as? Double
1444
+ let imageHeight = config["imageHeight"] as? Double
1445
+ let cacheKey = userLocationImageCacheKey(imagePath: imagePath, imageWidth: imageWidth, imageHeight: imageHeight)
1446
+ annotationView.accessibilityIdentifier = cacheKey
1447
+
1448
+ if let cached = IconBitmapCache.shared.image(forKey: cacheKey) {
1449
+ annotationView.image = cached
1450
+ return annotationView
1451
+ }
1452
+
1453
+ annotationView.image = nil
1454
+ loadUserLocationImage(imagePath: imagePath, imageWidth: imageWidth, imageHeight: imageHeight, cacheKey: cacheKey) { [weak annotationView] image in
1455
+ guard let annotationView = annotationView,
1456
+ annotationView.accessibilityIdentifier == cacheKey else {
1457
+ return
1458
+ }
1459
+ annotationView.image = image
1460
+ }
1461
+
1462
+ return annotationView
1463
+ }
1464
+
1465
+ private func userLocationImageCacheKey(imagePath: String, imageWidth: Double?, imageHeight: Double?) -> String {
1466
+ let widthPart = imageWidth.map { String(Int($0.rounded())) } ?? "auto"
1467
+ let heightPart = imageHeight.map { String(Int($0.rounded())) } ?? "auto"
1468
+ return "userLocation|\(imagePath)|\(widthPart)x\(heightPart)"
1469
+ }
1470
+
1471
+ private func loadUserLocationImage(
1472
+ imagePath: String,
1473
+ imageWidth: Double?,
1474
+ imageHeight: Double?,
1475
+ cacheKey: String,
1476
+ completion: @escaping (UIImage?) -> Void
1477
+ ) {
1478
+ let finish: (UIImage?) -> Void = { image in
1479
+ if let image = image {
1480
+ IconBitmapCache.shared.setImage(image, forKey: cacheKey)
1481
+ }
1482
+ DispatchQueue.main.async {
1483
+ completion(image)
1484
+ }
1485
+ }
1486
+
1487
+ if imagePath.hasPrefix("http://") || imagePath.hasPrefix("https://") {
1488
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
1489
+ guard let self = self,
1490
+ let url = URL(string: imagePath),
1491
+ let data = try? Data(contentsOf: url),
1492
+ let image = UIImage(data: data) else {
1493
+ finish(nil)
1494
+ return
1495
+ }
1496
+ finish(self.resizeUserLocationImage(image, imageWidth: imageWidth, imageHeight: imageHeight))
1497
+ }
1498
+ return
1499
+ }
1500
+
1501
+ let image: UIImage?
1502
+ if imagePath.hasPrefix("file://") {
1503
+ let path = String(imagePath.dropFirst(7))
1504
+ image = UIImage(contentsOfFile: path)
1505
+ } else {
1506
+ image = UIImage(named: imagePath)
1507
+ }
1508
+
1509
+ guard let image = image else {
1510
+ finish(nil)
1511
+ return
1512
+ }
1513
+ finish(resizeUserLocationImage(image, imageWidth: imageWidth, imageHeight: imageHeight))
1514
+ }
1515
+
1516
+ private func resizeUserLocationImage(_ image: UIImage, imageWidth: Double?, imageHeight: Double?) -> UIImage {
1517
+ guard let imageWidth = imageWidth,
1518
+ let imageHeight = imageHeight,
1519
+ imageWidth > 0,
1520
+ imageHeight > 0 else {
1521
+ return image
1522
+ }
1523
+
1524
+ let targetSize = CGSize(width: imageWidth, height: imageHeight)
1525
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
1526
+ return renderer.image { _ in
1527
+ image.draw(in: CGRect(origin: .zero, size: targetSize))
1528
+ }
1529
+ }
1530
+
1265
1531
  /**
1266
1532
  * 创建标注视图
1267
- * 定位蓝点返回 nil 使用系统默认样式
1533
+ * 定位蓝点默认返回 nil 使用系统样式;传入 image 时按官方建议自定义定位 annotationView。
1268
1534
  */
1269
1535
  public func mapView(_ mapView: MAMapView, viewFor annotation: MAAnnotation) -> MAAnnotationView? {
1270
1536
  if annotation.isKind(of: MAUserLocation.self) {
1271
- return nil
1537
+ return getUserLocationAnnotationView(for: mapView, annotation: annotation)
1272
1538
  }
1273
1539
 
1274
1540
  // 🔑 支持 MAAnimatedAnnotation(平滑移动)
@@ -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
@@ -172,6 +172,9 @@ class UIManager: NSObject, MAMapViewDelegate {
172
172
  // 是否显示方向指示
173
173
  if let showsHeadingIndicator = config["showsHeadingIndicator"] as? Bool {
174
174
  representation.showsHeadingIndicator = showsHeadingIndicator
175
+ if showsHeadingIndicator {
176
+ mapView.userTrackingMode = .followWithHeading
177
+ }
175
178
  }
176
179
 
177
180
  // 精度圈填充颜色