expo-gaode-map-navigation 1.1.5-next.0 → 1.1.5-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -73
- package/android/build.gradle +10 -0
- package/android/src/main/cpp/CMakeLists.txt +24 -0
- package/android/src/main/cpp/cluster_jni.cpp +848 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
- package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
- package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
- package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
- package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
- package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
- package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -3
- package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +4 -0
- package/build/ExpoGaodeMapNaviView.d.ts +7 -7
- package/build/ExpoGaodeMapNaviView.js +10 -11
- package/build/index.d.ts +1 -1
- package/build/index.js +2 -2
- package/build/map/ExpoGaodeMapModule.d.ts +2 -201
- package/build/map/ExpoGaodeMapModule.js +586 -18
- package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
- package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
- package/build/map/ExpoGaodeMapView.js +66 -58
- package/build/map/components/FoldableMapView.d.ts +38 -0
- package/build/map/components/FoldableMapView.js +209 -0
- package/build/map/components/MapContext.d.ts +12 -0
- package/build/map/components/MapContext.js +54 -0
- package/build/map/components/MapUI.d.ts +18 -0
- package/build/map/components/MapUI.js +29 -0
- package/build/map/components/overlays/Circle.js +34 -3
- package/build/map/components/overlays/Cluster.d.ts +3 -1
- package/build/map/components/overlays/Cluster.js +31 -2
- package/build/map/components/overlays/HeatMap.d.ts +3 -1
- package/build/map/components/overlays/HeatMap.js +33 -3
- package/build/map/components/overlays/Marker.d.ts +1 -1
- package/build/map/components/overlays/Marker.js +37 -32
- package/build/map/components/overlays/MultiPoint.js +1 -1
- package/build/map/components/overlays/Polygon.js +30 -3
- package/build/map/components/overlays/Polyline.js +36 -3
- package/build/map/index.d.ts +25 -5
- package/build/map/index.js +59 -18
- package/build/map/types/common.types.d.ts +40 -0
- package/build/map/types/common.types.js +0 -4
- package/build/map/types/index.d.ts +3 -2
- package/build/map/types/map-view.types.d.ts +108 -3
- package/build/map/types/native-module.types.d.ts +363 -0
- package/build/map/types/native-module.types.js +5 -0
- package/build/map/types/offline.types.d.ts +132 -0
- package/build/map/types/offline.types.js +5 -0
- package/build/map/types/overlays.types.d.ts +137 -24
- package/build/map/utils/ErrorHandler.d.ts +110 -0
- package/build/map/utils/ErrorHandler.js +421 -0
- package/build/map/utils/GeoUtils.d.ts +20 -0
- package/build/map/utils/GeoUtils.js +76 -0
- package/build/map/utils/OfflineMapManager.d.ts +148 -0
- package/build/map/utils/OfflineMapManager.js +217 -0
- package/build/map/utils/PermissionUtils.d.ts +91 -0
- package/build/map/utils/PermissionUtils.js +255 -0
- package/build/map/utils/PlatformDetector.d.ts +102 -0
- package/build/map/utils/PlatformDetector.js +186 -0
- package/build/types/naviview.types.d.ts +6 -1
- package/expo-module.config.json +12 -10
- package/ios/ExpoGaodeMapNavigation.podspec +9 -0
- package/ios/map/ExpoGaodeMapModule.swift +485 -75
- package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
- package/ios/map/ExpoGaodeMapView.swift +611 -62
- package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
- package/ios/map/MapPreloadManager.swift +348 -0
- package/ios/map/cpp/ClusterEngine.cpp +110 -0
- package/ios/map/cpp/ClusterEngine.hpp +20 -0
- package/ios/map/cpp/ColorParser.cpp +135 -0
- package/ios/map/cpp/ColorParser.hpp +14 -0
- package/ios/map/cpp/GeometryEngine.cpp +574 -0
- package/ios/map/cpp/GeometryEngine.hpp +159 -0
- package/ios/map/cpp/QuadTree.cpp +92 -0
- package/ios/map/cpp/QuadTree.hpp +42 -0
- package/ios/map/cpp/README.md +55 -0
- package/ios/map/cpp/tests/benchmark_js.js +41 -0
- package/ios/map/cpp/tests/run.sh +17 -0
- package/ios/map/cpp/tests/test_main.cpp +276 -0
- package/ios/map/managers/UIManager.swift +72 -1
- package/ios/map/modules/LocationManager.swift +123 -166
- package/ios/map/overlays/CircleView.swift +16 -32
- package/ios/map/overlays/CircleViewModule.swift +12 -12
- package/ios/map/overlays/ClusterAnnotation.swift +32 -0
- package/ios/map/overlays/ClusterView.swift +331 -45
- package/ios/map/overlays/ClusterViewModule.swift +20 -6
- package/ios/map/overlays/HeatMapView.swift +135 -32
- package/ios/map/overlays/HeatMapViewModule.swift +20 -8
- package/ios/map/overlays/MarkerView.swift +613 -130
- package/ios/map/overlays/MarkerViewModule.swift +38 -18
- package/ios/map/overlays/MultiPointView.swift +168 -10
- package/ios/map/overlays/MultiPointViewModule.swift +27 -5
- package/ios/map/overlays/PolygonView.swift +62 -23
- package/ios/map/overlays/PolygonViewModule.swift +18 -12
- package/ios/map/overlays/PolylineView.swift +21 -13
- package/ios/map/overlays/PolylineViewModule.swift +18 -12
- package/ios/map/utils/ClusterNative.h +96 -0
- package/ios/map/utils/ClusterNative.mm +377 -0
- package/ios/map/utils/ColorParser.swift +12 -1
- package/ios/map/utils/CppBridging.mm +13 -0
- package/ios/map/utils/GeometryUtils.swift +34 -0
- package/ios/map/utils/LatLngParser.swift +87 -0
- package/ios/map/utils/PermissionManager.swift +135 -6
- package/package.json +2 -2
- package/build/map/ExpoGaodeMap.types.d.ts +0 -41
- package/build/map/ExpoGaodeMap.types.js +0 -24
- package/build/map/utils/EventManager.d.ts +0 -10
- package/build/map/utils/EventManager.js +0 -26
- package/build/map/utils/ModuleLoader.d.ts +0 -73
- package/build/map/utils/ModuleLoader.js +0 -112
|
@@ -11,7 +11,7 @@ import UIKit
|
|
|
11
11
|
* - 支持拖拽功能
|
|
12
12
|
* - 支持自定义 children 视图
|
|
13
13
|
*/
|
|
14
|
-
class
|
|
14
|
+
class MarkerView: ExpoView {
|
|
15
15
|
// MARK: - 事件派发器(专属事件名避免冲突)
|
|
16
16
|
var onMarkerPress = EventDispatcher()
|
|
17
17
|
var onMarkerDragStart = EventDispatcher()
|
|
@@ -19,7 +19,7 @@ class NaviMarkerView: ExpoView {
|
|
|
19
19
|
var onMarkerDragEnd = EventDispatcher()
|
|
20
20
|
|
|
21
21
|
/// 标记点位置
|
|
22
|
-
var position: [String: Double]
|
|
22
|
+
var position: [String: Double]?
|
|
23
23
|
/// 临时存储的纬度
|
|
24
24
|
private var pendingLatitude: Double?
|
|
25
25
|
/// 临时存储的经度
|
|
@@ -48,12 +48,23 @@ class NaviMarkerView: ExpoView {
|
|
|
48
48
|
var pinColor: String = "red"
|
|
49
49
|
/// 是否显示气泡
|
|
50
50
|
var canShowCallout: Bool = true
|
|
51
|
+
/// 是否开启生长动画
|
|
52
|
+
var growAnimation: Bool = false
|
|
51
53
|
/// 地图视图引用
|
|
52
54
|
private var mapView: MAMapView?
|
|
53
55
|
/// 标记点对象
|
|
54
56
|
var annotation: MAPointAnnotation?
|
|
57
|
+
/// 在 MarkerView 中新增属性
|
|
58
|
+
var cacheKey: String?
|
|
55
59
|
/// 标记是否正在被移除(防止重复移除)
|
|
56
60
|
private var isRemoving: Bool = false
|
|
61
|
+
|
|
62
|
+
// 平滑移动相关
|
|
63
|
+
var smoothMovePath: [[String: Double]] = []
|
|
64
|
+
var smoothMoveDuration: Double = 0 // 🔑 修复:默认为 0,防止未设置时触发动画
|
|
65
|
+
var animatedAnnotation: MAAnimatedAnnotation? // internal: ExpoGaodeMapView 需要访问
|
|
66
|
+
var animatedAnnotationView: MAAnnotationView? // 平滑移动的 annotation view
|
|
67
|
+
private var isAnimating: Bool = false // 标记是否正在动画中
|
|
57
68
|
/// 标记点视图
|
|
58
69
|
private var annotationView: MAAnnotationView?
|
|
59
70
|
/// 待处理的位置(在 setMap 之前设置)
|
|
@@ -108,7 +119,7 @@ class NaviMarkerView: ExpoView {
|
|
|
108
119
|
return
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
|
|
122
|
+
_ = self.mapView == nil
|
|
112
123
|
self.mapView = map
|
|
113
124
|
lastSetMapView = map
|
|
114
125
|
|
|
@@ -124,19 +135,24 @@ class NaviMarkerView: ExpoView {
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
|
-
*
|
|
138
|
+
* 更新标记点(立即执行,与其他覆盖物保持一致)
|
|
128
139
|
*/
|
|
129
140
|
func updateAnnotation() {
|
|
130
|
-
//
|
|
131
|
-
|
|
141
|
+
// 🔑 性能优化:立即执行
|
|
142
|
+
performUpdateAnnotation()
|
|
132
143
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
// 🔑 只有当正在导航(isNavigating 在 JS 侧对应的逻辑)且路径时长合法时才自动启动
|
|
145
|
+
// 我们通过 smoothMovePath.isEmpty 来判断是否应该停止或不启动
|
|
146
|
+
if mapView != nil && !smoothMovePath.isEmpty && smoothMoveDuration > 0 {
|
|
147
|
+
startSmoothMove()
|
|
136
148
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// JS 侧可以调用
|
|
152
|
+
func setCacheKey(_ key: String?) {
|
|
153
|
+
self.cacheKey = key
|
|
154
|
+
// 发生变化时刷新 annotation
|
|
155
|
+
updateAnnotation()
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
/**
|
|
@@ -144,14 +160,7 @@ class NaviMarkerView: ExpoView {
|
|
|
144
160
|
*/
|
|
145
161
|
private func performUpdateAnnotation() {
|
|
146
162
|
guard let mapView = mapView,
|
|
147
|
-
let
|
|
148
|
-
let longitude = position["longitude"] else {
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 🔑 坐标验证:防止无效坐标导致崩溃
|
|
153
|
-
guard latitude >= -90 && latitude <= 90,
|
|
154
|
-
longitude >= -180 && longitude <= 180 else {
|
|
163
|
+
let coordinate = LatLngParser.parseLatLng(position) else {
|
|
155
164
|
return
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -159,24 +168,128 @@ class NaviMarkerView: ExpoView {
|
|
|
159
168
|
pendingAddTask?.cancel()
|
|
160
169
|
pendingAddTask = nil
|
|
161
170
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
// 🔑 修复抖动:如果正在进行原生平滑移动动画,不要执行普通的静态位置更新
|
|
172
|
+
// 但如果 animatedAnnotation 为 nil,说明动画已经停止或正在清理中,此时允许更新
|
|
173
|
+
if isAnimating && animatedAnnotation != nil {
|
|
174
|
+
return
|
|
165
175
|
}
|
|
166
|
-
|
|
167
|
-
//
|
|
176
|
+
|
|
177
|
+
// 如果已有 annotation,尝试更新坐标与属性
|
|
178
|
+
if let existing = annotation {
|
|
179
|
+
// 🔑 显式设置坐标,MAPointAnnotation 的 coordinate 赋值通常是不带动画的
|
|
180
|
+
existing.coordinate = coordinate
|
|
181
|
+
existing.title = title
|
|
182
|
+
existing.subtitle = markerDescription
|
|
183
|
+
|
|
184
|
+
// 🔑 确保 annotation 在地图上
|
|
185
|
+
if !mapView.annotations.contains(where: { ($0 as? NSObject) === existing }) {
|
|
186
|
+
mapView.addAnnotation(existing)
|
|
187
|
+
}
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 如果没有,则创建并添加
|
|
168
192
|
let annotation = MAPointAnnotation()
|
|
169
|
-
annotation.coordinate =
|
|
193
|
+
annotation.coordinate = coordinate
|
|
170
194
|
annotation.title = title
|
|
171
195
|
annotation.subtitle = markerDescription
|
|
172
|
-
|
|
173
196
|
self.annotation = annotation
|
|
174
197
|
|
|
175
|
-
//
|
|
176
|
-
// 不再使用延迟添加,避免新架构下的时序问题
|
|
198
|
+
// 立即添加到地图
|
|
177
199
|
mapView.addAnnotation(annotation)
|
|
178
200
|
}
|
|
179
201
|
|
|
202
|
+
/**
|
|
203
|
+
* 获取 animated annotation 视图(由 ExpoGaodeMapView 调用)
|
|
204
|
+
* 为 MAAnimatedAnnotation 提供图标支持
|
|
205
|
+
*/
|
|
206
|
+
func getAnimatedAnnotationView(for mapView: MAMapView, annotation: MAAnnotation) -> MAAnnotationView? {
|
|
207
|
+
let reuseId = "animated_marker_\(ObjectIdentifier(self).hashValue)" + (growAnimation ? "_grow" : "")
|
|
208
|
+
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
|
|
209
|
+
|
|
210
|
+
if annotationView == nil {
|
|
211
|
+
if growAnimation {
|
|
212
|
+
annotationView = ExpoGrowAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
213
|
+
} else {
|
|
214
|
+
annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if let growView = annotationView as? ExpoGrowAnnotationView {
|
|
219
|
+
growView.enableGrowAnimation = true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
annotationView?.annotation = annotation
|
|
223
|
+
self.animatedAnnotationView = annotationView
|
|
224
|
+
|
|
225
|
+
// 优先级:children > icon > pinColor
|
|
226
|
+
|
|
227
|
+
// 1. 如果有 children,使用自定义视图
|
|
228
|
+
if self.subviews.count > 0 {
|
|
229
|
+
let key = cacheKey ?? "children_\(ObjectIdentifier(self).hashValue)"
|
|
230
|
+
if let cached = IconBitmapCache.shared.image(forKey: key) {
|
|
231
|
+
annotationView?.image = cached
|
|
232
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: 0)
|
|
233
|
+
return annotationView
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 异步渲染并设置
|
|
237
|
+
DispatchQueue.main.async { [weak self, weak annotationView] in
|
|
238
|
+
guard let self = self, let annotationView = annotationView else { return }
|
|
239
|
+
if let generated = self.createImageFromSubviews() {
|
|
240
|
+
IconBitmapCache.shared.setImage(generated, forKey: key)
|
|
241
|
+
annotationView.image = generated
|
|
242
|
+
annotationView.centerOffset = CGPoint(x: 0, y: 0)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return annotationView
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. 如果有 icon 属性,使用自定义图标
|
|
249
|
+
if let iconUri = iconUri, !iconUri.isEmpty {
|
|
250
|
+
let key = cacheKey ?? "icon|\(iconUri)|\(Int(iconWidth))x\(Int(iconHeight))"
|
|
251
|
+
if let cached = IconBitmapCache.shared.image(forKey: key) {
|
|
252
|
+
annotationView?.image = cached
|
|
253
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: -cached.size.height / 2)
|
|
254
|
+
return annotationView
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 异步加载图标
|
|
258
|
+
loadIcon(iconUri: iconUri) { [weak self, weak annotationView] image in
|
|
259
|
+
guard let self = self, let image = image, let annotationView = annotationView else { return }
|
|
260
|
+
let size = CGSize(width: self.iconWidth, height: self.iconHeight)
|
|
261
|
+
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
262
|
+
image.draw(in: CGRect(origin: .zero, size: size))
|
|
263
|
+
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
264
|
+
UIGraphicsEndImageContext()
|
|
265
|
+
|
|
266
|
+
if let img = resizedImage {
|
|
267
|
+
IconBitmapCache.shared.setImage(img, forKey: key)
|
|
268
|
+
annotationView.image = img
|
|
269
|
+
annotationView.centerOffset = CGPoint(x: 0, y: -img.size.height / 2)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return annotationView
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. 使用默认大头针颜色
|
|
276
|
+
switch pinColor.lowercased() {
|
|
277
|
+
case "green":
|
|
278
|
+
// 使用绿色图标
|
|
279
|
+
let greenIcon = UIImage(named: "map_marker_green") ?? UIImage(systemName: "mappin.circle.fill")
|
|
280
|
+
annotationView?.image = greenIcon
|
|
281
|
+
case "purple":
|
|
282
|
+
let purpleIcon = UIImage(named: "map_marker_purple") ?? UIImage(systemName: "mappin.circle.fill")
|
|
283
|
+
annotationView?.image = purpleIcon
|
|
284
|
+
default:
|
|
285
|
+
// 默认红色
|
|
286
|
+
let redIcon = UIImage(named: "map_marker_red") ?? UIImage(systemName: "mappin.circle.fill")
|
|
287
|
+
annotationView?.image = redIcon
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return annotationView
|
|
291
|
+
}
|
|
292
|
+
|
|
180
293
|
/**
|
|
181
294
|
* 获取 annotation 视图(由 ExpoGaodeMapView 调用)
|
|
182
295
|
*/
|
|
@@ -184,86 +297,147 @@ class NaviMarkerView: ExpoView {
|
|
|
184
297
|
|
|
185
298
|
// 🔑 如果有 children,使用自定义视图
|
|
186
299
|
if self.subviews.count > 0 {
|
|
187
|
-
|
|
300
|
+
// 使用 class-level reuseId,便于系统复用 view,减少内存
|
|
301
|
+
let reuseId = "custom_marker_children" + (growAnimation ? "_grow" : "")
|
|
188
302
|
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
|
|
189
|
-
|
|
190
303
|
if annotationView == nil {
|
|
191
|
-
|
|
304
|
+
if growAnimation {
|
|
305
|
+
annotationView = ExpoGrowAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
306
|
+
} else {
|
|
307
|
+
annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
308
|
+
}
|
|
192
309
|
}
|
|
193
310
|
|
|
311
|
+
if let growView = annotationView as? ExpoGrowAnnotationView {
|
|
312
|
+
growView.enableGrowAnimation = true
|
|
313
|
+
}
|
|
314
|
+
|
|
194
315
|
annotationView?.annotation = annotation
|
|
195
|
-
// 🔑 关键修复:有自定义内容时不显示默认 callout(信息窗口)
|
|
196
316
|
annotationView?.canShowCallout = false
|
|
197
317
|
annotationView?.isDraggable = draggable
|
|
198
318
|
self.annotationView = annotationView
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
319
|
+
|
|
320
|
+
// 生成 cacheKey 或 fallback 到 identifier
|
|
321
|
+
let key = cacheKey ?? "children_\(ObjectIdentifier(self).hashValue)"
|
|
322
|
+
|
|
323
|
+
// 1) 如果缓存命中,直接同步返回图像(fast path)
|
|
324
|
+
if let cached = IconBitmapCache.shared.image(forKey: key) {
|
|
325
|
+
annotationView?.image = cached
|
|
326
|
+
// 🔑 修复:自定义视图使用中心偏移,不需要底部偏移
|
|
327
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: 0)
|
|
328
|
+
return annotationView
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 2) 缓存未命中:返回占位(透明),并异步在主线程生成图像然后回填
|
|
332
|
+
let size = CGSize(width: CGFloat(customViewWidth > 0 ? customViewWidth : 200),
|
|
333
|
+
height: CGFloat(customViewHeight > 0 ? customViewHeight : 40))
|
|
334
|
+
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
335
|
+
let transparentImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
336
|
+
UIGraphicsEndImageContext()
|
|
337
|
+
annotationView?.image = transparentImage
|
|
338
|
+
|
|
339
|
+
// 🔑 修复:延长延迟时间,给 React Native Image 更多加载时间
|
|
340
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak annotationView] in
|
|
341
|
+
guard let self = self, let annotationView = annotationView else { return }
|
|
342
|
+
// 再次检查缓存(避免重复渲染)
|
|
343
|
+
if let cached = IconBitmapCache.shared.image(forKey: key) {
|
|
344
|
+
annotationView.image = cached
|
|
345
|
+
annotationView.centerOffset = CGPoint(x: 0, y: 0)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
211
348
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
349
|
+
// 调用你的原生渲染逻辑(保留空白检测、多次 layout)
|
|
350
|
+
if let generated = self.createImageFromSubviews() {
|
|
351
|
+
// 写入缓存(仅当用户传了 cacheKey 才缓存;否则建议仍缓存由 fingerprint 决定)
|
|
352
|
+
IconBitmapCache.shared.setImage(generated, forKey: key)
|
|
353
|
+
annotationView.image = generated
|
|
354
|
+
annotationView.centerOffset = CGPoint(x: 0, y: 0)
|
|
355
|
+
} else {
|
|
219
356
|
}
|
|
220
357
|
}
|
|
221
|
-
|
|
358
|
+
|
|
222
359
|
return annotationView
|
|
223
360
|
}
|
|
361
|
+
|
|
224
362
|
|
|
225
363
|
// 🔑 如果有 icon 属性,使用自定义图标
|
|
226
364
|
if let iconUri = iconUri, !iconUri.isEmpty {
|
|
227
|
-
let reuseId = "custom_marker_icon_\(ObjectIdentifier(self).hashValue)"
|
|
365
|
+
let reuseId = "custom_marker_icon_\(ObjectIdentifier(self).hashValue)" + (growAnimation ? "_grow" : "")
|
|
228
366
|
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
|
|
229
367
|
|
|
230
368
|
if annotationView == nil {
|
|
231
|
-
|
|
369
|
+
if growAnimation {
|
|
370
|
+
annotationView = ExpoGrowAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
371
|
+
} else {
|
|
372
|
+
annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
373
|
+
}
|
|
232
374
|
}
|
|
233
375
|
|
|
376
|
+
if let growView = annotationView as? ExpoGrowAnnotationView {
|
|
377
|
+
growView.enableGrowAnimation = true
|
|
378
|
+
}
|
|
379
|
+
|
|
234
380
|
annotationView?.annotation = annotation
|
|
235
381
|
// 只有在没有自定义内容时才使用 canShowCallout 设置
|
|
236
382
|
annotationView?.canShowCallout = canShowCallout
|
|
237
383
|
annotationView?.isDraggable = draggable
|
|
238
384
|
self.annotationView = annotationView
|
|
239
385
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
386
|
+
// 构建 key
|
|
387
|
+
let key = cacheKey ?? "icon|\(iconUri)|\(Int(iconWidth))x\(Int(iconHeight))"
|
|
388
|
+
if let cached = IconBitmapCache.shared.image(forKey: key) {
|
|
389
|
+
annotationView?.image = cached
|
|
390
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: -cached.size.height / 2)
|
|
391
|
+
return annotationView
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 原有异步加载,不变:只是在回调里先缓存 then set
|
|
395
|
+
loadIcon(iconUri: iconUri) { [weak self, weak annotationView] image in
|
|
396
|
+
guard let self = self, let image = image else { return }
|
|
245
397
|
let size = CGSize(width: self.iconWidth, height: self.iconHeight)
|
|
246
|
-
|
|
247
398
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
248
399
|
image.draw(in: CGRect(origin: .zero, size: size))
|
|
249
400
|
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
250
401
|
UIGraphicsEndImageContext()
|
|
251
|
-
|
|
402
|
+
|
|
252
403
|
DispatchQueue.main.async {
|
|
253
|
-
|
|
254
|
-
|
|
404
|
+
if let img = resizedImage {
|
|
405
|
+
IconBitmapCache.shared.setImage(img, forKey: key)
|
|
406
|
+
annotationView?.image = img
|
|
407
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: -img.size.height / 2)
|
|
408
|
+
}
|
|
255
409
|
}
|
|
256
410
|
}
|
|
411
|
+
|
|
257
412
|
|
|
258
413
|
return annotationView
|
|
259
414
|
}
|
|
260
415
|
|
|
261
416
|
// 🔑 既没有 children 也没有 icon,使用系统默认大头针
|
|
262
|
-
|
|
417
|
+
// 🔑 性能优化:使用颜色作为 reuseId,让系统复用相同颜色的大头针
|
|
418
|
+
let reuseId = "pin_marker_\(pinColor)" + (growAnimation ? "_grow" : "")
|
|
263
419
|
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MAPinAnnotationView
|
|
264
420
|
|
|
265
421
|
if pinView == nil {
|
|
266
|
-
|
|
422
|
+
if growAnimation {
|
|
423
|
+
pinView = ExpoGrowPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
424
|
+
} else {
|
|
425
|
+
pinView = MAPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 🔑 创建时设置颜色(只在创建时设置一次)
|
|
429
|
+
switch pinColor.lowercased() {
|
|
430
|
+
case "green":
|
|
431
|
+
pinView?.pinColor = .green
|
|
432
|
+
case "purple":
|
|
433
|
+
pinView?.pinColor = .purple
|
|
434
|
+
default:
|
|
435
|
+
pinView?.pinColor = .red
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if let growView = pinView as? ExpoGrowPinAnnotationView {
|
|
440
|
+
growView.enableGrowAnimation = true
|
|
267
441
|
}
|
|
268
442
|
|
|
269
443
|
pinView?.annotation = annotation
|
|
@@ -271,16 +445,6 @@ class NaviMarkerView: ExpoView {
|
|
|
271
445
|
pinView?.isDraggable = draggable
|
|
272
446
|
pinView?.animatesDrop = animatesDrop
|
|
273
447
|
|
|
274
|
-
// 设置大头针颜色
|
|
275
|
-
switch pinColor.lowercased() {
|
|
276
|
-
case "green":
|
|
277
|
-
pinView?.pinColor = .green
|
|
278
|
-
case "purple":
|
|
279
|
-
pinView?.pinColor = .purple
|
|
280
|
-
default:
|
|
281
|
-
pinView?.pinColor = .red
|
|
282
|
-
}
|
|
283
|
-
|
|
284
448
|
self.annotationView = pinView
|
|
285
449
|
return pinView
|
|
286
450
|
}
|
|
@@ -318,12 +482,16 @@ class NaviMarkerView: ExpoView {
|
|
|
318
482
|
* 将子视图转换为图片
|
|
319
483
|
*/
|
|
320
484
|
private func createImageFromSubviews() -> UIImage? {
|
|
485
|
+
// 🔑 如果有 cacheKey 且命中缓存,直接返回缓存图片
|
|
486
|
+
if let key = cacheKey, let cachedImage = IconBitmapCache.shared.image(forKey: key) {
|
|
487
|
+
return cachedImage
|
|
488
|
+
}
|
|
489
|
+
|
|
321
490
|
guard let firstSubview = subviews.first else {
|
|
322
491
|
return nil
|
|
323
492
|
}
|
|
324
493
|
|
|
325
494
|
// 优先使用 customViewWidth/customViewHeight(用于 children),其次使用子视图尺寸,最后使用默认值
|
|
326
|
-
// 注意:iconWidth/iconHeight 是用于自定义图标的,不用于 children
|
|
327
495
|
let width: CGFloat
|
|
328
496
|
let height: CGFloat
|
|
329
497
|
|
|
@@ -348,7 +516,7 @@ class NaviMarkerView: ExpoView {
|
|
|
348
516
|
// 强制子视图使用指定尺寸布局
|
|
349
517
|
firstSubview.frame = CGRect(origin: .zero, size: size)
|
|
350
518
|
|
|
351
|
-
// 🔑
|
|
519
|
+
// 🔑 多次强制布局,确保 React Native Text 完全渲染
|
|
352
520
|
for _ in 0..<3 {
|
|
353
521
|
forceLayoutRecursively(view: firstSubview)
|
|
354
522
|
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
|
|
@@ -357,46 +525,27 @@ class NaviMarkerView: ExpoView {
|
|
|
357
525
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
358
526
|
defer { UIGraphicsEndImageContext() }
|
|
359
527
|
|
|
360
|
-
guard let
|
|
528
|
+
guard let _ = UIGraphicsGetCurrentContext() else {
|
|
361
529
|
return nil
|
|
362
530
|
}
|
|
363
531
|
|
|
364
532
|
// 使用 drawHierarchy 而不是 layer.render,这样能正确渲染 Text
|
|
365
|
-
|
|
533
|
+
firstSubview.drawHierarchy(in: CGRect(origin: .zero, size: size), afterScreenUpdates: true)
|
|
366
534
|
|
|
367
535
|
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
|
|
368
536
|
return nil
|
|
369
537
|
}
|
|
370
538
|
|
|
371
|
-
|
|
372
|
-
guard let cgImage = image.cgImage else {
|
|
373
|
-
return nil
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 检查图片数据是否为空白
|
|
377
|
-
let dataProvider = cgImage.dataProvider
|
|
378
|
-
let data = dataProvider?.data
|
|
379
|
-
let buffer = CFDataGetBytePtr(data)
|
|
380
|
-
|
|
381
|
-
var isBlank = true
|
|
382
|
-
if let buffer = buffer {
|
|
383
|
-
let length = CFDataGetLength(data)
|
|
384
|
-
// 检查前 100 个字节是否都是 0(空白)
|
|
385
|
-
let checkLength = min(100, length)
|
|
386
|
-
for i in 0..<checkLength {
|
|
387
|
-
if buffer[i] != 0 {
|
|
388
|
-
isBlank = false
|
|
389
|
-
break
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
539
|
+
|
|
393
540
|
|
|
394
|
-
|
|
395
|
-
|
|
541
|
+
// 🔑 写入缓存
|
|
542
|
+
if let key = cacheKey {
|
|
543
|
+
IconBitmapCache.shared.setImage(image, forKey: key)
|
|
396
544
|
}
|
|
397
545
|
|
|
398
546
|
return image
|
|
399
547
|
}
|
|
548
|
+
|
|
400
549
|
|
|
401
550
|
/**
|
|
402
551
|
* 递归强制布局视图及其所有子视图
|
|
@@ -429,30 +578,41 @@ class NaviMarkerView: ExpoView {
|
|
|
429
578
|
private func removeAnnotationFromMap() {
|
|
430
579
|
guard !isRemoving else { return }
|
|
431
580
|
isRemoving = true
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// 立即保存引用并清空属性,避免在异步块中访问 self
|
|
440
|
-
guard let mapView = mapView, let annotation = annotation else {
|
|
441
|
-
return
|
|
581
|
+
pendingAddTask?.cancel(); pendingAddTask = nil
|
|
582
|
+
pendingUpdateTask?.cancel(); pendingUpdateTask = nil
|
|
583
|
+
|
|
584
|
+
guard let mapView = mapView else {
|
|
585
|
+
isRemoving = false
|
|
586
|
+
return
|
|
442
587
|
}
|
|
443
|
-
self.annotation = nil
|
|
444
|
-
self.annotationView = nil
|
|
445
588
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
mapView
|
|
449
|
-
|
|
450
|
-
|
|
589
|
+
// 确保在主线程执行移除操作
|
|
590
|
+
let cleanup = { [weak self, weak mapView] in
|
|
591
|
+
guard let self = self, let mapView = mapView else { return }
|
|
592
|
+
|
|
593
|
+
// 1. 停止任何正在进行的平滑移动
|
|
594
|
+
if self.animatedAnnotation != nil {
|
|
595
|
+
self.stopSmoothMove()
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 2. 移除普通标记点
|
|
599
|
+
if let annotation = self.annotation {
|
|
451
600
|
mapView.removeAnnotation(annotation)
|
|
601
|
+
self.annotation = nil
|
|
452
602
|
}
|
|
603
|
+
|
|
604
|
+
self.annotationView = nil
|
|
605
|
+
self.animatedAnnotationView = nil
|
|
606
|
+
self.isRemoving = false
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if Thread.isMainThread {
|
|
610
|
+
cleanup()
|
|
611
|
+
} else {
|
|
612
|
+
DispatchQueue.main.async(execute: cleanup)
|
|
453
613
|
}
|
|
454
614
|
}
|
|
455
|
-
|
|
615
|
+
|
|
456
616
|
override func willRemoveSubview(_ subview: UIView) {
|
|
457
617
|
super.willRemoveSubview(subview)
|
|
458
618
|
|
|
@@ -541,14 +701,17 @@ class NaviMarkerView: ExpoView {
|
|
|
541
701
|
* 设置位置(兼容旧的 API)
|
|
542
702
|
* @param position 位置坐标 {latitude, longitude}
|
|
543
703
|
*/
|
|
544
|
-
func setPosition(_ position: [String: Double]) {
|
|
545
|
-
if
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
704
|
+
func setPosition(_ position: [String: Double]?) {
|
|
705
|
+
if let coord = LatLngParser.parseLatLng(position) {
|
|
706
|
+
let pos = ["latitude": coord.latitude, "longitude": coord.longitude]
|
|
707
|
+
if mapView != nil {
|
|
708
|
+
// 地图已设置,直接更新
|
|
709
|
+
self.position = pos
|
|
710
|
+
updateAnnotation()
|
|
711
|
+
} else {
|
|
712
|
+
// 地图还未设置,保存位置待后续应用
|
|
713
|
+
pendingPosition = pos
|
|
714
|
+
}
|
|
552
715
|
}
|
|
553
716
|
}
|
|
554
717
|
|
|
@@ -600,6 +763,206 @@ class NaviMarkerView: ExpoView {
|
|
|
600
763
|
self.canShowCallout = show
|
|
601
764
|
}
|
|
602
765
|
|
|
766
|
+
// MARK: - 平滑移动相关方法
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* 设置平滑移动路径
|
|
770
|
+
* @param path 坐标点数组
|
|
771
|
+
*/
|
|
772
|
+
func setSmoothMovePath(_ path: [[String: Double]]) {
|
|
773
|
+
self.smoothMovePath = path
|
|
774
|
+
|
|
775
|
+
// 🔑 修复逻辑:如果路径为空,立即停止动画
|
|
776
|
+
if path.isEmpty {
|
|
777
|
+
if animatedAnnotation != nil || isAnimating {
|
|
778
|
+
stopSmoothMove()
|
|
779
|
+
}
|
|
780
|
+
} else if mapView != nil && smoothMoveDuration > 0 {
|
|
781
|
+
// 只有在时长也准备好的情况下才自动启动
|
|
782
|
+
startSmoothMove()
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* 停止平滑移动并恢复静态标记
|
|
788
|
+
*/
|
|
789
|
+
func stopSmoothMove() {
|
|
790
|
+
// 🔑 确保在主线程执行
|
|
791
|
+
let cleanup = { [weak self, weak mapView] in
|
|
792
|
+
guard let self = self, let mapView = mapView else { return }
|
|
793
|
+
|
|
794
|
+
// 1. 获取并取消所有动画 (遵循官方文档建议)
|
|
795
|
+
if let animAnnotation = self.animatedAnnotation {
|
|
796
|
+
let animations = animAnnotation.allMoveAnimations()
|
|
797
|
+
if let animations = animations {
|
|
798
|
+
for animation in animations {
|
|
799
|
+
animation.cancel()
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 2. 从地图移除动画标注
|
|
804
|
+
mapView.removeAnnotation(animAnnotation)
|
|
805
|
+
self.animatedAnnotation = nil
|
|
806
|
+
self.animatedAnnotationView = nil
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// 🔑 强制重置所有状态
|
|
810
|
+
self.isAnimating = false
|
|
811
|
+
self.smoothMovePath = []
|
|
812
|
+
self.smoothMoveDuration = 0
|
|
813
|
+
|
|
814
|
+
// 3. 恢复静态标注(立即跳转到 position 所在位置)
|
|
815
|
+
self.performUpdateAnnotation()
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if Thread.isMainThread {
|
|
819
|
+
cleanup()
|
|
820
|
+
} else {
|
|
821
|
+
DispatchQueue.main.async(execute: cleanup)
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* 设置平滑移动时长(秒)
|
|
827
|
+
*/
|
|
828
|
+
func setSmoothMoveDuration(_ duration: Double) {
|
|
829
|
+
// 🔑 修复:不要在这里设置默认值 10,如果 JS 传 0 或未定义,就应该是 0
|
|
830
|
+
self.smoothMoveDuration = duration
|
|
831
|
+
|
|
832
|
+
// 🔑 如果时长被设为 0 或负数,停止当前动画
|
|
833
|
+
if duration <= 0 {
|
|
834
|
+
if animatedAnnotation != nil || isAnimating {
|
|
835
|
+
stopSmoothMove()
|
|
836
|
+
}
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// 🔑 只有当路径、时长都合法且地图就绪时,才启动平滑移动
|
|
841
|
+
if !smoothMovePath.isEmpty && duration > 0 && mapView != nil {
|
|
842
|
+
startSmoothMove()
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* 启动平滑移动(由 JS 端手动调用)
|
|
848
|
+
*/
|
|
849
|
+
func startSmoothMove() {
|
|
850
|
+
guard !isRemoving, let mapView = mapView, !smoothMovePath.isEmpty, smoothMoveDuration > 0 else {
|
|
851
|
+
if smoothMovePath.isEmpty && animatedAnnotation != nil {
|
|
852
|
+
stopSmoothMove()
|
|
853
|
+
}
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 🔑 确保在主线程执行
|
|
858
|
+
if !Thread.isMainThread {
|
|
859
|
+
DispatchQueue.main.async { [weak self] in
|
|
860
|
+
self?.startSmoothMove()
|
|
861
|
+
}
|
|
862
|
+
return
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 转换路径为 CLLocationCoordinate2D 数组
|
|
866
|
+
// 使用 C++ 优化计算路径中的最近点
|
|
867
|
+
var adjustedPath: [[String: Double]]? = nil
|
|
868
|
+
|
|
869
|
+
// 只有当有当前位置时才尝试寻找最近点
|
|
870
|
+
if let pos = position, let currentLat = pos["latitude"], let currentLng = pos["longitude"] {
|
|
871
|
+
// 准备数据给 C++
|
|
872
|
+
let latitudes = smoothMovePath.compactMap { $0["latitude"] as NSNumber? }
|
|
873
|
+
let longitudes = smoothMovePath.compactMap { $0["longitude"] as NSNumber? }
|
|
874
|
+
|
|
875
|
+
if latitudes.count == longitudes.count && !latitudes.isEmpty {
|
|
876
|
+
let lats = latitudes
|
|
877
|
+
let lons = longitudes
|
|
878
|
+
if let result = ClusterNative.getNearestPointOnPath(latitudes: lats,
|
|
879
|
+
longitudes: lons,
|
|
880
|
+
targetLat: currentLat,
|
|
881
|
+
targetLon: currentLng) as? [String: Any] {
|
|
882
|
+
|
|
883
|
+
if let indexNum = result["index"] as? NSNumber,
|
|
884
|
+
let lat = result["latitude"] as? Double,
|
|
885
|
+
let lon = result["longitude"] as? Double {
|
|
886
|
+
|
|
887
|
+
let index = indexNum.intValue
|
|
888
|
+
if index >= 0 && index < smoothMovePath.count - 1 {
|
|
889
|
+
// 从 index + 1 开始截取
|
|
890
|
+
let subPath = Array(smoothMovePath[(index + 1)...])
|
|
891
|
+
// 插入投影点作为起点
|
|
892
|
+
var newPath = subPath
|
|
893
|
+
newPath.insert(["latitude": lat, "longitude": lon], at: 0)
|
|
894
|
+
adjustedPath = newPath
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// 如果没有调整路径(C++计算失败或不需要调整),使用原始路径
|
|
902
|
+
let finalPath = adjustedPath ?? smoothMovePath
|
|
903
|
+
|
|
904
|
+
var coordinates = LatLngParser.parseLatLngList(finalPath)
|
|
905
|
+
|
|
906
|
+
guard !coordinates.isEmpty else { return }
|
|
907
|
+
|
|
908
|
+
// 🔑 停止之前的动画(如果存在)
|
|
909
|
+
if let animAnnotation = animatedAnnotation,
|
|
910
|
+
let animations = animAnnotation.allMoveAnimations() {
|
|
911
|
+
for animation in animations {
|
|
912
|
+
animation.cancel()
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// 🔑 重置动画标志
|
|
917
|
+
isAnimating = false
|
|
918
|
+
|
|
919
|
+
// 创建 MAAnimatedAnnotation(如果还没有)
|
|
920
|
+
if animatedAnnotation == nil {
|
|
921
|
+
animatedAnnotation = MAAnimatedAnnotation()
|
|
922
|
+
|
|
923
|
+
// 设置初始位置
|
|
924
|
+
if let pos = position, let startLat = pos["latitude"], let startLng = pos["longitude"] {
|
|
925
|
+
animatedAnnotation?.coordinate = CLLocationCoordinate2D(latitude: startLat, longitude: startLng)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// 隐藏原始 annotation
|
|
929
|
+
if let existingAnnotation = annotation {
|
|
930
|
+
mapView.removeAnnotation(existingAnnotation)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 添加 animated annotation
|
|
934
|
+
if let anim = animatedAnnotation {
|
|
935
|
+
mapView.addAnnotation(anim)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// 添加移动动画
|
|
940
|
+
guard let animAnnotation = animatedAnnotation else { return }
|
|
941
|
+
|
|
942
|
+
// 复制到局部变量,避免 Swift 内存安全冲突
|
|
943
|
+
let coordinateCount = coordinates.count
|
|
944
|
+
let duration = smoothMoveDuration
|
|
945
|
+
|
|
946
|
+
// 🔑 设置动画标志
|
|
947
|
+
isAnimating = true
|
|
948
|
+
|
|
949
|
+
// 转换为 UnsafeMutablePointer 传递给 C 风格的 API
|
|
950
|
+
coordinates.withUnsafeMutableBufferPointer { buffer in
|
|
951
|
+
let coords = buffer.baseAddress!
|
|
952
|
+
|
|
953
|
+
animAnnotation.addMoveAnimation(
|
|
954
|
+
withKeyCoordinates: coords,
|
|
955
|
+
count: UInt(coordinateCount),
|
|
956
|
+
withDuration: CGFloat(duration),
|
|
957
|
+
withName: nil,
|
|
958
|
+
completeCallback: { [weak self] isFinished in
|
|
959
|
+
// 动画完成时重置标志
|
|
960
|
+
self?.isAnimating = false
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
603
966
|
/**
|
|
604
967
|
* 析构函数 - 不执行任何清理
|
|
605
968
|
* 清理工作已在 willMove(toSuperview:) 中完成
|
|
@@ -616,3 +979,123 @@ class NaviMarkerView: ExpoView {
|
|
|
616
979
|
lastSetMapView = nil
|
|
617
980
|
}
|
|
618
981
|
}
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
/// 增强版内存缓存(带 cost 与清理)
|
|
985
|
+
class IconBitmapCache {
|
|
986
|
+
static let shared = IconBitmapCache()
|
|
987
|
+
private init() {
|
|
988
|
+
// 设置 totalCostLimit = 1/8 可用内存(以字节计)
|
|
989
|
+
let mem = ProcessInfo.processInfo.physicalMemory
|
|
990
|
+
// 限制在可用物理内存的 1/8(可按需调整)
|
|
991
|
+
let limit = Int(mem / 8)
|
|
992
|
+
cache.totalCostLimit = limit
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private var cache = NSCache<NSString, UIImage>()
|
|
996
|
+
|
|
997
|
+
func image(forKey key: String) -> UIImage? {
|
|
998
|
+
return cache.object(forKey: key as NSString)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
func setImage(_ image: UIImage, forKey key: String) {
|
|
1002
|
+
// 以 bitmap 字节数作为 cost(更可靠)
|
|
1003
|
+
let cost = imageCostInBytes(image)
|
|
1004
|
+
cache.setObject(image, forKey: key as NSString, cost: cost)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
func removeImage(forKey key: String) {
|
|
1008
|
+
cache.removeObject(forKey: key as NSString)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
func clear() {
|
|
1012
|
+
cache.removeAllObjects()
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private func imageCostInBytes(_ image: UIImage) -> Int {
|
|
1016
|
+
if let cg = image.cgImage {
|
|
1017
|
+
return cg.bytesPerRow * cg.height
|
|
1018
|
+
}
|
|
1019
|
+
// fallback estimate
|
|
1020
|
+
return Int(image.size.width * image.size.height * 4)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// MARK: - 自定义 AnnotationView (支持生长动画)
|
|
1025
|
+
|
|
1026
|
+
class ExpoGrowAnnotationView: MAAnnotationView, CAAnimationDelegate {
|
|
1027
|
+
var enableGrowAnimation: Bool = false
|
|
1028
|
+
private var didAnimateOnce: Bool = false
|
|
1029
|
+
|
|
1030
|
+
override func prepareForReuse() {
|
|
1031
|
+
super.prepareForReuse()
|
|
1032
|
+
didAnimateOnce = false
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
override func willMove(toSuperview newSuperview: UIView?) {
|
|
1036
|
+
super.willMove(toSuperview: newSuperview)
|
|
1037
|
+
|
|
1038
|
+
if enableGrowAnimation, let _ = newSuperview, !didAnimateOnce {
|
|
1039
|
+
didAnimateOnce = true
|
|
1040
|
+
|
|
1041
|
+
// 缩放动画
|
|
1042
|
+
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
|
|
1043
|
+
scaleAnimation.fromValue = 0
|
|
1044
|
+
scaleAnimation.toValue = 1.0
|
|
1045
|
+
|
|
1046
|
+
// 透明度动画
|
|
1047
|
+
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
|
|
1048
|
+
opacityAnimation.fromValue = 0
|
|
1049
|
+
opacityAnimation.toValue = 1.0
|
|
1050
|
+
|
|
1051
|
+
// 组合动画
|
|
1052
|
+
let groupAnimation = CAAnimationGroup()
|
|
1053
|
+
groupAnimation.animations = [scaleAnimation, opacityAnimation]
|
|
1054
|
+
groupAnimation.delegate = self
|
|
1055
|
+
groupAnimation.duration = 0.8 // 与 Android 保持一致 (500ms)
|
|
1056
|
+
groupAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
1057
|
+
groupAnimation.fillMode = .forwards
|
|
1058
|
+
groupAnimation.isRemovedOnCompletion = false
|
|
1059
|
+
|
|
1060
|
+
self.layer.add(groupAnimation, forKey: "growAnimation")
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
class ExpoGrowPinAnnotationView: MAPinAnnotationView, CAAnimationDelegate {
|
|
1066
|
+
var enableGrowAnimation: Bool = false
|
|
1067
|
+
private var didAnimateOnce: Bool = false
|
|
1068
|
+
|
|
1069
|
+
override func prepareForReuse() {
|
|
1070
|
+
super.prepareForReuse()
|
|
1071
|
+
didAnimateOnce = false
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
override func willMove(toSuperview newSuperview: UIView?) {
|
|
1075
|
+
super.willMove(toSuperview: newSuperview)
|
|
1076
|
+
|
|
1077
|
+
if enableGrowAnimation, let _ = newSuperview, !didAnimateOnce {
|
|
1078
|
+
didAnimateOnce = true
|
|
1079
|
+
// 缩放动画
|
|
1080
|
+
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
|
|
1081
|
+
scaleAnimation.fromValue = 0
|
|
1082
|
+
scaleAnimation.toValue = 1.0
|
|
1083
|
+
|
|
1084
|
+
// 透明度动画
|
|
1085
|
+
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
|
|
1086
|
+
opacityAnimation.fromValue = 0
|
|
1087
|
+
opacityAnimation.toValue = 1.0
|
|
1088
|
+
|
|
1089
|
+
// 组合动画
|
|
1090
|
+
let groupAnimation = CAAnimationGroup()
|
|
1091
|
+
groupAnimation.animations = [scaleAnimation, opacityAnimation]
|
|
1092
|
+
groupAnimation.delegate = self
|
|
1093
|
+
groupAnimation.duration = 0.5 // 与 Android 保持一致 (500ms)
|
|
1094
|
+
groupAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
1095
|
+
groupAnimation.fillMode = .forwards
|
|
1096
|
+
groupAnimation.isRemovedOnCompletion = false
|
|
1097
|
+
|
|
1098
|
+
self.layer.add(groupAnimation, forKey: "growAnimation")
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|