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.
Files changed (134) hide show
  1. package/README.md +213 -73
  2. package/android/build.gradle +10 -0
  3. package/android/src/main/cpp/CMakeLists.txt +24 -0
  4. package/android/src/main/cpp/cluster_jni.cpp +848 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
  8. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
  9. package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
  10. package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
  11. package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
  12. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
  13. package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
  14. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
  15. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
  16. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
  17. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
  18. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
  19. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
  20. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
  21. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
  22. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
  23. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
  24. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
  25. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
  26. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
  27. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
  28. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
  29. package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
  30. package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
  31. package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
  32. package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
  33. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
  34. package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
  35. package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
  36. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -3
  37. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +4 -0
  38. package/build/ExpoGaodeMapNaviView.d.ts +7 -7
  39. package/build/ExpoGaodeMapNaviView.js +10 -11
  40. package/build/index.d.ts +1 -1
  41. package/build/index.js +2 -2
  42. package/build/map/ExpoGaodeMapModule.d.ts +2 -201
  43. package/build/map/ExpoGaodeMapModule.js +586 -18
  44. package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
  45. package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
  46. package/build/map/ExpoGaodeMapView.js +66 -58
  47. package/build/map/components/FoldableMapView.d.ts +38 -0
  48. package/build/map/components/FoldableMapView.js +209 -0
  49. package/build/map/components/MapContext.d.ts +12 -0
  50. package/build/map/components/MapContext.js +54 -0
  51. package/build/map/components/MapUI.d.ts +18 -0
  52. package/build/map/components/MapUI.js +29 -0
  53. package/build/map/components/overlays/Circle.js +34 -3
  54. package/build/map/components/overlays/Cluster.d.ts +3 -1
  55. package/build/map/components/overlays/Cluster.js +31 -2
  56. package/build/map/components/overlays/HeatMap.d.ts +3 -1
  57. package/build/map/components/overlays/HeatMap.js +33 -3
  58. package/build/map/components/overlays/Marker.d.ts +1 -1
  59. package/build/map/components/overlays/Marker.js +37 -32
  60. package/build/map/components/overlays/MultiPoint.js +1 -1
  61. package/build/map/components/overlays/Polygon.js +30 -3
  62. package/build/map/components/overlays/Polyline.js +36 -3
  63. package/build/map/index.d.ts +25 -5
  64. package/build/map/index.js +59 -18
  65. package/build/map/types/common.types.d.ts +40 -0
  66. package/build/map/types/common.types.js +0 -4
  67. package/build/map/types/index.d.ts +3 -2
  68. package/build/map/types/map-view.types.d.ts +108 -3
  69. package/build/map/types/native-module.types.d.ts +363 -0
  70. package/build/map/types/native-module.types.js +5 -0
  71. package/build/map/types/offline.types.d.ts +132 -0
  72. package/build/map/types/offline.types.js +5 -0
  73. package/build/map/types/overlays.types.d.ts +137 -24
  74. package/build/map/utils/ErrorHandler.d.ts +110 -0
  75. package/build/map/utils/ErrorHandler.js +421 -0
  76. package/build/map/utils/GeoUtils.d.ts +20 -0
  77. package/build/map/utils/GeoUtils.js +76 -0
  78. package/build/map/utils/OfflineMapManager.d.ts +148 -0
  79. package/build/map/utils/OfflineMapManager.js +217 -0
  80. package/build/map/utils/PermissionUtils.d.ts +91 -0
  81. package/build/map/utils/PermissionUtils.js +255 -0
  82. package/build/map/utils/PlatformDetector.d.ts +102 -0
  83. package/build/map/utils/PlatformDetector.js +186 -0
  84. package/build/types/naviview.types.d.ts +6 -1
  85. package/expo-module.config.json +12 -10
  86. package/ios/ExpoGaodeMapNavigation.podspec +9 -0
  87. package/ios/map/ExpoGaodeMapModule.swift +485 -75
  88. package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
  89. package/ios/map/ExpoGaodeMapView.swift +611 -62
  90. package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
  91. package/ios/map/MapPreloadManager.swift +348 -0
  92. package/ios/map/cpp/ClusterEngine.cpp +110 -0
  93. package/ios/map/cpp/ClusterEngine.hpp +20 -0
  94. package/ios/map/cpp/ColorParser.cpp +135 -0
  95. package/ios/map/cpp/ColorParser.hpp +14 -0
  96. package/ios/map/cpp/GeometryEngine.cpp +574 -0
  97. package/ios/map/cpp/GeometryEngine.hpp +159 -0
  98. package/ios/map/cpp/QuadTree.cpp +92 -0
  99. package/ios/map/cpp/QuadTree.hpp +42 -0
  100. package/ios/map/cpp/README.md +55 -0
  101. package/ios/map/cpp/tests/benchmark_js.js +41 -0
  102. package/ios/map/cpp/tests/run.sh +17 -0
  103. package/ios/map/cpp/tests/test_main.cpp +276 -0
  104. package/ios/map/managers/UIManager.swift +72 -1
  105. package/ios/map/modules/LocationManager.swift +123 -166
  106. package/ios/map/overlays/CircleView.swift +16 -32
  107. package/ios/map/overlays/CircleViewModule.swift +12 -12
  108. package/ios/map/overlays/ClusterAnnotation.swift +32 -0
  109. package/ios/map/overlays/ClusterView.swift +331 -45
  110. package/ios/map/overlays/ClusterViewModule.swift +20 -6
  111. package/ios/map/overlays/HeatMapView.swift +135 -32
  112. package/ios/map/overlays/HeatMapViewModule.swift +20 -8
  113. package/ios/map/overlays/MarkerView.swift +613 -130
  114. package/ios/map/overlays/MarkerViewModule.swift +38 -18
  115. package/ios/map/overlays/MultiPointView.swift +168 -10
  116. package/ios/map/overlays/MultiPointViewModule.swift +27 -5
  117. package/ios/map/overlays/PolygonView.swift +62 -23
  118. package/ios/map/overlays/PolygonViewModule.swift +18 -12
  119. package/ios/map/overlays/PolylineView.swift +21 -13
  120. package/ios/map/overlays/PolylineViewModule.swift +18 -12
  121. package/ios/map/utils/ClusterNative.h +96 -0
  122. package/ios/map/utils/ClusterNative.mm +377 -0
  123. package/ios/map/utils/ColorParser.swift +12 -1
  124. package/ios/map/utils/CppBridging.mm +13 -0
  125. package/ios/map/utils/GeometryUtils.swift +34 -0
  126. package/ios/map/utils/LatLngParser.swift +87 -0
  127. package/ios/map/utils/PermissionManager.swift +135 -6
  128. package/package.json +2 -2
  129. package/build/map/ExpoGaodeMap.types.d.ts +0 -41
  130. package/build/map/ExpoGaodeMap.types.js +0 -24
  131. package/build/map/utils/EventManager.d.ts +0 -10
  132. package/build/map/utils/EventManager.js +0 -26
  133. package/build/map/utils/ModuleLoader.d.ts +0 -73
  134. package/build/map/utils/ModuleLoader.js +0 -112
@@ -11,7 +11,7 @@ import UIKit
11
11
  * - 支持拖拽功能
12
12
  * - 支持自定义 children 视图
13
13
  */
14
- class NaviMarkerView: ExpoView {
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
- let isNewMap = self.mapView == nil
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
- pendingUpdateTask?.cancel()
141
+ // 🔑 性能优化:立即执行
142
+ performUpdateAnnotation()
132
143
 
133
- // 延迟 16ms(一帧)批量更新
134
- let task = DispatchWorkItem { [weak self] in
135
- self?.performUpdateAnnotation()
144
+ // 🔑 只有当正在导航(isNavigating 在 JS 侧对应的逻辑)且路径时长合法时才自动启动
145
+ // 我们通过 smoothMovePath.isEmpty 来判断是否应该停止或不启动
146
+ if mapView != nil && !smoothMovePath.isEmpty && smoothMoveDuration > 0 {
147
+ startSmoothMove()
136
148
  }
137
- pendingUpdateTask = task
138
-
139
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.016, execute: task)
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 latitude = position["latitude"],
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
- if let oldAnnotation = annotation {
164
- mapView.removeAnnotation(oldAnnotation)
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 = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
193
+ annotation.coordinate = coordinate
170
194
  annotation.title = title
171
195
  annotation.subtitle = markerDescription
172
-
173
196
  self.annotation = annotation
174
197
 
175
- // 🔑 关键修复:立即添加到地图(与 CircleView 等保持一致)
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
- let reuseId = "custom_marker_children_\(ObjectIdentifier(self).hashValue)"
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
- annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
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
- if let image = self.createImageFromSubviews() {
201
- annotationView?.image = image
202
- annotationView?.centerOffset = CGPoint(x: 0, y: -image.size.height / 2)
203
- } else {
204
- // 🔑 关键修复:不返回 nil,而是设置透明图片,然后延迟重试
205
- let size = CGSize(width: CGFloat(customViewWidth > 0 ? customViewWidth : 200),
206
- height: CGFloat(customViewHeight > 0 ? customViewHeight : 40))
207
- UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
208
- let transparentImage = UIGraphicsGetImageFromCurrentImageContext()
209
- UIGraphicsEndImageContext()
210
- annotationView?.image = transparentImage
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
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak annotationView] in
214
- guard let self = self, let annotationView = annotationView else { return }
215
- if let image = self.createImageFromSubviews() {
216
- annotationView.image = image
217
- annotationView.centerOffset = CGPoint(x: 0, y: -image.size.height / 2)
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
- annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
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
- loadIcon(iconUri: iconUri) { [weak self] image in
242
- guard let self = self, let image = image else {
243
- return
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
- annotationView?.image = resizedImage
254
- annotationView?.centerOffset = CGPoint(x: 0, y: -self.iconHeight / 2)
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
- let reuseId = "pin_marker_\(ObjectIdentifier(self).hashValue)"
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
- pinView = MAPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
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
- // 🔑 关键修复:多次强制布局,确保 React Native Text 完全渲染
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 context = UIGraphicsGetCurrentContext() else {
528
+ guard let _ = UIGraphicsGetCurrentContext() else {
361
529
  return nil
362
530
  }
363
531
 
364
532
  // 使用 drawHierarchy 而不是 layer.render,这样能正确渲染 Text
365
- let success = firstSubview.drawHierarchy(in: CGRect(origin: .zero, size: size), afterScreenUpdates: true)
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
- if isBlank {
395
- return nil
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
- pendingAddTask?.cancel()
435
- pendingAddTask = nil
436
- pendingUpdateTask?.cancel()
437
- pendingUpdateTask = nil
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
- if Thread.isMainThread {
448
- mapView.removeAnnotation(annotation)
449
- } else {
450
- DispatchQueue.main.sync {
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 mapView != nil {
546
- // 地图已设置,直接更新
547
- self.position = position
548
- updateAnnotation()
549
- } else {
550
- // 地图还未设置,保存位置待后续应用
551
- pendingPosition = position
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
+ }