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.
- package/README.md +4 -3
- package/android/build.gradle +11 -32
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +35 -12
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapViewModule.kt +2 -0
- package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +8 -0
- package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +13 -5
- package/build/ExpoGaodeMapOfflineModule.js +1 -1
- package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
- package/build/ExpoGaodeMapView.js +2 -8
- package/build/ExpoGaodeMapView.js.map +1 -1
- package/build/components/AreaMaskOverlay.d.ts +1 -2
- package/build/components/AreaMaskOverlay.d.ts.map +1 -1
- package/build/components/AreaMaskOverlay.js +2 -1
- package/build/components/AreaMaskOverlay.js.map +1 -1
- package/build/components/FoldableMapView.d.ts +3 -2
- package/build/components/FoldableMapView.d.ts.map +1 -1
- package/build/components/FoldableMapView.js +2 -1
- package/build/components/FoldableMapView.js.map +1 -1
- package/build/components/MapUI.js +2 -1
- package/build/components/MapUI.js.map +1 -1
- package/build/components/RouteOverlay.d.ts +1 -2
- package/build/components/RouteOverlay.d.ts.map +1 -1
- package/build/components/RouteOverlay.js +5 -5
- package/build/components/RouteOverlay.js.map +1 -1
- package/build/components/overlays/Circle.d.ts +1 -1
- package/build/components/overlays/Circle.d.ts.map +1 -1
- package/build/components/overlays/Circle.js +2 -1
- package/build/components/overlays/Circle.js.map +1 -1
- package/build/components/overlays/Cluster.d.ts +1 -1
- package/build/components/overlays/Cluster.d.ts.map +1 -1
- package/build/components/overlays/Cluster.js +2 -1
- package/build/components/overlays/Cluster.js.map +1 -1
- package/build/components/overlays/HeatMap.d.ts +1 -1
- package/build/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/components/overlays/HeatMap.js +2 -1
- package/build/components/overlays/HeatMap.js.map +1 -1
- package/build/components/overlays/Marker.d.ts +1 -1
- package/build/components/overlays/Marker.d.ts.map +1 -1
- package/build/components/overlays/Marker.js +8 -7
- package/build/components/overlays/Marker.js.map +1 -1
- package/build/components/overlays/MultiPoint.d.ts +1 -2
- package/build/components/overlays/MultiPoint.d.ts.map +1 -1
- package/build/components/overlays/MultiPoint.js +2 -1
- package/build/components/overlays/MultiPoint.js.map +1 -1
- package/build/components/overlays/Polygon.d.ts +1 -1
- package/build/components/overlays/Polygon.d.ts.map +1 -1
- package/build/components/overlays/Polygon.js +2 -1
- package/build/components/overlays/Polygon.js.map +1 -1
- package/build/components/overlays/Polyline.d.ts +1 -1
- package/build/components/overlays/Polyline.d.ts.map +1 -1
- package/build/components/overlays/Polyline.js +2 -1
- package/build/components/overlays/Polyline.js.map +1 -1
- package/build/module/geometry.d.ts.map +1 -1
- package/build/module/geometry.js +2 -1
- package/build/module/geometry.js.map +1 -1
- package/build/module/sdk.d.ts.map +1 -1
- package/build/module/sdk.js +10 -3
- package/build/module/sdk.js.map +1 -1
- package/build/search/ExpoGaodeMapSearchModule.d.ts +1 -1
- package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -1
- package/build/search/ExpoGaodeMapSearchModule.js.map +1 -1
- package/build/types/common.types.d.ts +1 -1
- package/build/types/common.types.d.ts.map +1 -1
- package/build/types/common.types.js.map +1 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js.map +1 -1
- package/build/types/map-view.types.d.ts +11 -0
- package/build/types/map-view.types.d.ts.map +1 -1
- package/build/types/map-view.types.js.map +1 -1
- package/build/utils/ErrorHandler.d.ts +4 -0
- package/build/utils/ErrorHandler.d.ts.map +1 -1
- package/build/utils/ErrorHandler.js +8 -0
- package/build/utils/ErrorHandler.js.map +1 -1
- package/build/utils/GeoUtils.d.ts +2 -2
- package/build/utils/GeoUtils.d.ts.map +1 -1
- package/build/utils/GeoUtils.js +3 -3
- package/build/utils/GeoUtils.js.map +1 -1
- package/build/utils/RouteUtils.d.ts.map +1 -1
- package/build/utils/RouteUtils.js +5 -2
- package/build/utils/RouteUtils.js.map +1 -1
- package/ios/ExpoGaodeMapView.swift +106 -10
- package/ios/ExpoGaodeMapViewModule.swift +5 -0
- package/ios/overlays/MarkerView.swift +104 -32
- package/package.json +14 -8
- package/plugin/build/withGaodeMap.js +3 -2
- package/shared/cpp/ClusterEngine.cpp +4 -14
- package/shared/cpp/ClusterEngine.hpp +1 -12
- package/shared/cpp/ClusterTypes.hpp +18 -0
- package/shared/cpp/ColorParser.cpp +22 -3
- package/shared/cpp/GeometryEngine.cpp +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
950
|
+
applyZoomGestureSettings()
|
|
883
951
|
}
|
|
884
952
|
|
|
885
|
-
private func
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
341
|
+
applyChildrenImage(cached, to: annotationView, signature: signature)
|
|
340
342
|
}
|
|
341
343
|
return annotationView
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
// 2)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "
|
|
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
|
-
"
|
|
76
|
-
"expo
|
|
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": "~
|
|
79
|
-
"react-native": "0.
|
|
80
|
-
"typescript": "^
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
15
|
-
|
|
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 (
|
|
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
|
|
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
|
}
|
|
@@ -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(),
|
|
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(),
|
|
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(),
|
|
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
|
|