expo-gaode-map-navigation 2.0.6 → 2.0.7

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 (30) hide show
  1. package/README.md +25 -0
  2. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +71 -45
  3. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +35 -0
  4. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +0 -1
  5. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +13 -2
  6. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +385 -1
  7. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +180 -4
  8. package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +182 -7
  9. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +31 -1
  10. package/build/index.d.ts +7 -2
  11. package/build/index.d.ts.map +1 -1
  12. package/build/index.js +5 -0
  13. package/build/index.js.map +1 -1
  14. package/build/types/independent.types.d.ts +7 -0
  15. package/build/types/independent.types.d.ts.map +1 -1
  16. package/build/types/independent.types.js.map +1 -1
  17. package/build/types/native-module.types.d.ts +5 -1
  18. package/build/types/native-module.types.d.ts.map +1 -1
  19. package/build/types/native-module.types.js.map +1 -1
  20. package/build/types/route.types.d.ts +112 -0
  21. package/build/types/route.types.d.ts.map +1 -1
  22. package/build/types/route.types.js.map +1 -1
  23. package/ios/ExpoGaodeMapNaviView.swift +56 -4
  24. package/ios/ExpoGaodeMapNavigationModule.swift +585 -12
  25. package/ios/map/ExpoGaodeMapOfflineModule.swift +58 -34
  26. package/ios/map/GaodeMapPrivacyManager.swift +23 -1
  27. package/ios/map/overlays/MarkerView.swift +148 -11
  28. package/ios/services/IndependentRouteService.swift +186 -44
  29. package/package.json +1 -1
  30. package/plugin/build/withGaodeMap.js +28 -0
@@ -27,9 +27,7 @@ public class ExpoGaodeMapOfflineModule: Module {
27
27
  set { stateQueue.async(flags: .barrier) { self._pausedCities = newValue } }
28
28
  }
29
29
 
30
- private var offlineMapManager: MAOfflineMap {
31
- return MAOfflineMap.shared()
32
- }
30
+ private var offlineMapManager: MAOfflineMap?
33
31
 
34
32
  // 数据缓存
35
33
  private var cachedCities: [MAOfflineCity]?
@@ -60,7 +58,11 @@ public class ExpoGaodeMapOfflineModule: Module {
60
58
  }
61
59
 
62
60
  OnDestroy {
63
- MAOfflineMap.shared().cancelAll()
61
+ self.getOfflineMapManager()?.cancelAll()
62
+ self.offlineMapManager = nil
63
+ self.isSetupComplete = false
64
+ self.isSetupInProgress = false
65
+ self.setupWaiters.removeAll()
64
66
  self.downloadingCities.removeAll()
65
67
  self.pausedCities.removeAll()
66
68
  }
@@ -70,7 +72,7 @@ public class ExpoGaodeMapOfflineModule: Module {
70
72
  AsyncFunction("getAvailableCities") { (promise: Promise) in
71
73
  self.ensureSetup { success in
72
74
  guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
73
- let cities = self.cachedCities ?? self.offlineMapManager.cities ?? []
75
+ let cities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
74
76
  promise.resolve(cities.map { self.convertCityToDict($0) })
75
77
  }
76
78
  }
@@ -78,7 +80,7 @@ public class ExpoGaodeMapOfflineModule: Module {
78
80
  AsyncFunction("getAvailableProvinces") { (promise: Promise) in
79
81
  self.ensureSetup { success in
80
82
  guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
81
- let provinces = self.cachedProvinces ?? self.offlineMapManager.provinces ?? []
83
+ let provinces = self.cachedProvinces ?? self.getOfflineMapManager()?.provinces ?? []
82
84
  promise.resolve(provinces.map { self.convertProvinceToDict($0) })
83
85
  }
84
86
  }
@@ -86,7 +88,7 @@ public class ExpoGaodeMapOfflineModule: Module {
86
88
  AsyncFunction("getCitiesByProvince") { (provinceCode: String, promise: Promise) in
87
89
  self.ensureSetup { success in
88
90
  guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
89
- let provinces = self.cachedProvinces ?? self.offlineMapManager.provinces ?? []
91
+ let provinces = self.cachedProvinces ?? self.getOfflineMapManager()?.provinces ?? []
90
92
  if let province = provinces.first(where: { $0.adcode == provinceCode }) {
91
93
  let cities = province.cities.compactMap { ($0 as? MAOfflineCity).map { self.convertCityToDict($0) } }
92
94
  promise.resolve(cities)
@@ -99,7 +101,7 @@ public class ExpoGaodeMapOfflineModule: Module {
99
101
  AsyncFunction("getDownloadedMaps") { (promise: Promise) in
100
102
  self.ensureSetup { success in
101
103
  guard success else { promise.reject("ERR_SETUP", "Setup failed"); return }
102
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
104
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
103
105
  let downloaded = allCities.filter { $0.itemStatus == .installed }
104
106
  promise.resolve(downloaded.map { self.convertCityToDict($0) })
105
107
  }
@@ -120,28 +122,28 @@ public class ExpoGaodeMapOfflineModule: Module {
120
122
  }
121
123
 
122
124
  Function("pauseDownload") { (cityCode: String) in
123
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
125
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
124
126
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
125
127
  self.pausedCities.insert(cityCode)
126
128
  self.downloadingCities.remove(cityCode)
127
- self.offlineMapManager.pause(city)
129
+ self.getOfflineMapManager()?.pause(city)
128
130
  }
129
131
  }
130
132
 
131
133
  AsyncFunction("cancelDownload") { (cityCode: String) in
132
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
134
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
133
135
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
134
136
  self.downloadingCities.remove(cityCode)
135
137
  self.pausedCities.remove(cityCode)
136
- self.offlineMapManager.pause(city) // iOS SDK 中 pause 停止网络
138
+ self.getOfflineMapManager()?.pause(city) // iOS SDK 中 pause 停止网络
137
139
  self.sendEvent("onDownloadCancelled", ["cityCode": cityCode, "cityName": city.name ?? ""])
138
140
  }
139
141
  }
140
142
 
141
143
  AsyncFunction("deleteMap") { (cityCode: String) in
142
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
144
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
143
145
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
144
- self.offlineMapManager.delete(city)
146
+ self.getOfflineMapManager()?.delete(city)
145
147
  self.downloadingCities.remove(cityCode)
146
148
  self.pausedCities.remove(cityCode)
147
149
  }
@@ -153,7 +155,11 @@ public class ExpoGaodeMapOfflineModule: Module {
153
155
 
154
156
  AsyncFunction("checkUpdate") { (cityCode: String, promise: Promise) in
155
157
  // 检查特定城市或全局更新
156
- self.offlineMapManager.checkNewestVersion { hasNewestVersion in
158
+ guard let manager = self.getOfflineMapManager() else {
159
+ promise.reject("ERR_PRIVACY", "Privacy consent is required before using offline map APIs")
160
+ return
161
+ }
162
+ manager.checkNewestVersion { hasNewestVersion in
157
163
  if hasNewestVersion {
158
164
  // 刷新缓存以获取最新数据
159
165
  self.stateQueue.async(flags: .barrier) {
@@ -169,13 +175,13 @@ public class ExpoGaodeMapOfflineModule: Module {
169
175
  // ==================== 3. 状态查询 ====================
170
176
 
171
177
  AsyncFunction("isMapDownloaded") { (cityCode: String) -> Bool in
172
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
178
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
173
179
  return allCities.first(where: { $0.adcode == cityCode })?.itemStatus == .installed
174
180
  }
175
181
 
176
182
  // 恢复原有方法:getMapStatus
177
183
  AsyncFunction("getMapStatus") { (cityCode: String) -> [String: Any] in
178
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
184
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
179
185
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
180
186
  return self.convertCityToDict(city)
181
187
  }
@@ -194,7 +200,7 @@ public class ExpoGaodeMapOfflineModule: Module {
194
200
  // ==================== 4. 存储管理 ====================
195
201
 
196
202
  AsyncFunction("getStorageSize") { () -> Int64 in
197
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
203
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
198
204
  let installed = allCities.filter { $0.itemStatus == .installed }
199
205
  // 修复类型转换报错:Int64(0)
200
206
  return installed.reduce(Int64(0)) { $0 + ($1.downloadedSize > 0 ? $1.downloadedSize : $1.size) }
@@ -203,7 +209,7 @@ public class ExpoGaodeMapOfflineModule: Module {
203
209
  // 恢复原有方法:getStorageInfo
204
210
  AsyncFunction("getStorageInfo") { () -> [String: Any] in
205
211
  // 1. 计算离线地图占用
206
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
212
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
207
213
  let installed = allCities.filter { $0.itemStatus == .installed }
208
214
  let offlineMapSize = installed.reduce(Int64(0)) { $0 + ($1.downloadedSize > 0 ? $1.downloadedSize : $1.size) }
209
215
 
@@ -231,7 +237,7 @@ public class ExpoGaodeMapOfflineModule: Module {
231
237
  }
232
238
 
233
239
  AsyncFunction("clearAllMaps") {
234
- self.offlineMapManager.clearDisk()
240
+ self.getOfflineMapManager()?.clearDisk()
235
241
  self.downloadingCities.removeAll()
236
242
  self.pausedCities.removeAll()
237
243
  self.cachedCities = nil
@@ -254,14 +260,14 @@ public class ExpoGaodeMapOfflineModule: Module {
254
260
  // 确保初始化
255
261
  self.ensureSetup { success in
256
262
  guard success else { return }
257
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
263
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
258
264
 
259
265
  cityCodes.forEach { cityCode in
260
266
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
261
267
  self.downloadingCities.insert(cityCode)
262
268
  self.pausedCities.remove(cityCode)
263
269
  // 批量下载也建议开启后台
264
- self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
270
+ self.getOfflineMapManager()?.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
265
271
  guard let self = self, let item = item else { return }
266
272
  self.handleDownloadCallback(item: item, status: status, info: info)
267
273
  }
@@ -272,10 +278,10 @@ public class ExpoGaodeMapOfflineModule: Module {
272
278
 
273
279
  // 恢复原有方法:batchDelete
274
280
  AsyncFunction("batchDelete") { (cityCodes: [String]) in
275
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
281
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
276
282
  cityCodes.forEach { cityCode in
277
283
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
278
- self.offlineMapManager.delete(city)
284
+ self.getOfflineMapManager()?.delete(city)
279
285
  }
280
286
  self.downloadingCities.remove(cityCode)
281
287
  self.pausedCities.remove(cityCode)
@@ -286,7 +292,7 @@ public class ExpoGaodeMapOfflineModule: Module {
286
292
  AsyncFunction("batchUpdate") { (cityCodes: [String]) in
287
293
  self.ensureSetup { success in
288
294
  guard success else { return }
289
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
295
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
290
296
 
291
297
  cityCodes.forEach { cityCode in
292
298
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
@@ -296,7 +302,7 @@ public class ExpoGaodeMapOfflineModule: Module {
296
302
  self._downloadingCities.insert(cityCode)
297
303
  self._pausedCities.remove(cityCode)
298
304
  }
299
- self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
305
+ self.getOfflineMapManager()?.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
300
306
  guard let self = self, let item = item else { return }
301
307
  self.handleDownloadCallback(item: item, status: status, info: info)
302
308
  }
@@ -307,7 +313,7 @@ public class ExpoGaodeMapOfflineModule: Module {
307
313
  }
308
314
 
309
315
  AsyncFunction("pauseAllDownloads") {
310
- self.offlineMapManager.cancelAll()
316
+ self.getOfflineMapManager()?.cancelAll()
311
317
  for cityCode in self.downloadingCities {
312
318
  self.pausedCities.insert(cityCode)
313
319
  self.sendEvent("onDownloadPaused", ["cityCode": cityCode, "cityName": ""])
@@ -320,14 +326,14 @@ public class ExpoGaodeMapOfflineModule: Module {
320
326
  // iOS SDK 没有 resumeAll,只能尝试恢复 pausedCities 列表中的城市
321
327
  self.ensureSetup { success in
322
328
  guard success else { return }
323
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
329
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
324
330
  let pausedList = Array(self.pausedCities)
325
331
 
326
332
  pausedList.forEach { cityCode in
327
333
  if let city = allCities.first(where: { $0.adcode == cityCode }) {
328
334
  self.pausedCities.remove(cityCode)
329
335
  self.downloadingCities.insert(cityCode)
330
- self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
336
+ self.getOfflineMapManager()?.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
331
337
  guard let self = self, let item = item else { return }
332
338
  self.handleDownloadCallback(item: item, status: status, info: info)
333
339
  }
@@ -346,7 +352,7 @@ public class ExpoGaodeMapOfflineModule: Module {
346
352
  return
347
353
  }
348
354
 
349
- let allCities = self.cachedCities ?? self.offlineMapManager.cities ?? []
355
+ let allCities = self.cachedCities ?? self.getOfflineMapManager()?.cities ?? []
350
356
  guard let city = allCities.first(where: { $0.adcode == cityCode }) else {
351
357
  promise.reject("ERR_CITY", "City not found: \(cityCode)")
352
358
  return
@@ -356,21 +362,39 @@ public class ExpoGaodeMapOfflineModule: Module {
356
362
  self.pausedCities.remove(cityCode)
357
363
 
358
364
  // 开启后台下载
359
- self.offlineMapManager.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
365
+ self.getOfflineMapManager()?.downloadItem(city, shouldContinueWhenAppEntersBackground: true) { [weak self] item, status, info in
360
366
  guard let self = self, let item = item else { return }
361
367
  self.handleDownloadCallback(item: item, status: status, info: info)
362
368
  }
363
369
  promise.resolve(true)
364
370
  }
365
371
  }
372
+
373
+ private func getOfflineMapManager() -> MAOfflineMap? {
374
+ GaodeMapPrivacyManager.restorePersistedState()
375
+ guard GaodeMapPrivacyManager.isReady else {
376
+ return nil
377
+ }
378
+ GaodeMapPrivacyManager.applyPrivacyState()
379
+
380
+ if offlineMapManager == nil {
381
+ offlineMapManager = MAOfflineMap.shared()
382
+ }
383
+
384
+ return offlineMapManager
385
+ }
366
386
 
367
387
  private func ensureSetup(completion: @escaping (Bool) -> Void) {
388
+ guard let manager = getOfflineMapManager() else {
389
+ completion(false)
390
+ return
391
+ }
368
392
  if isSetupComplete { completion(true); return }
369
393
  setupWaiters.append(completion)
370
394
  if isSetupInProgress { return }
371
395
 
372
396
  isSetupInProgress = true
373
- MAOfflineMap.shared().setup { [weak self] success in
397
+ manager.setup { [weak self] success in
374
398
  guard let self = self else { return }
375
399
  self.isSetupInProgress = false
376
400
  self.isSetupComplete = success
@@ -382,7 +406,7 @@ public class ExpoGaodeMapOfflineModule: Module {
382
406
  }
383
407
 
384
408
  private func parseOfflineMapData() {
385
- let map = MAOfflineMap.shared()
409
+ let map = getOfflineMapManager()
386
410
  stateQueue.async(flags: .barrier) {
387
411
  self.cachedCities = map?.cities
388
412
  self.cachedProvinces = map?.provinces
@@ -462,7 +486,7 @@ public class ExpoGaodeMapOfflineModule: Module {
462
486
  "size": city.size,
463
487
  "status": status,
464
488
  "downloadedSize": city.downloadedSize,
465
- "version": self.offlineMapManager.version ?? "",
489
+ "version": self.getOfflineMapManager()?.version ?? "",
466
490
  "progress": city.size > 0 ? Int((Double(city.downloadedSize) / Double(city.size)) * 100) : 0
467
491
  ]
468
492
  }
@@ -89,18 +89,40 @@ enum GaodeMapPrivacyManager {
89
89
  applyPrivacyState()
90
90
  notifyIfNeeded(previousStatus: previousStatus)
91
91
  }
92
-
92
+
93
93
  static func applyPrivacyState() {
94
94
  let showStatus: AMapPrivacyShowStatus = hasShow ? .didShow : .notShow
95
95
  let infoStatus: AMapPrivacyInfoStatus = hasContainsPrivacy ? .didContain : .notContain
96
96
  let agreeStatus: AMapPrivacyAgreeStatus = hasAgree ? .didAgree : .notAgree
97
97
 
98
+ // Map & Location(类方法 ✅)
98
99
  MAMapView.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
99
100
  MAMapView.updatePrivacyAgree(agreeStatus)
100
101
  AMapLocationManager.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
101
102
  AMapLocationManager.updatePrivacyAgree(agreeStatus)
103
+
104
+ // Navi(实例方法 ⚠️)
105
+ let naviConfig = AMapNaviManagerConfig()
106
+ naviConfig.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
107
+ naviConfig.updatePrivacyAgree(agreeStatus)
102
108
  }
103
109
 
110
+ // static func applyPrivacyState() {
111
+ // let showStatus: AMapPrivacyShowStatus = hasShow ? .didShow : .notShow
112
+ // let infoStatus: AMapPrivacyInfoStatus = hasContainsPrivacy ? .didContain : .notContain
113
+ // let agreeStatus: AMapPrivacyAgreeStatus = hasAgree ? .didAgree : .notAgree
114
+ //
115
+ // MAMapView.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
116
+ // MAMapView.updatePrivacyAgree(agreeStatus)
117
+ // AMapLocationManager.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
118
+ // AMapLocationManager.updatePrivacyAgree(agreeStatus)
119
+ // let config = AMapNaviManagerConfig()
120
+ // config.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
121
+ // config.updatePrivacyAgree(agreeStatus)
122
+ //// AMapNaviManagerConfig.updatePrivacyShow(showStatus, privacyInfo: infoStatus)
123
+ //// AMapNaviManagerConfig.updatePrivacyAgree(agreeStatus)
124
+ // }
125
+
104
126
  static func status() -> [String: Any] {
105
127
  [
106
128
  "hasShow": hasShow,
@@ -154,9 +154,9 @@ class MarkerView: ExpoView {
154
154
 
155
155
  // JS 侧可以调用
156
156
  func setCacheKey(_ key: String?) {
157
+ guard cacheKey != key else { return }
157
158
  self.cacheKey = key
158
- // 发生变化时刷新 annotation
159
- updateAnnotation()
159
+ refreshAnnotationAppearance(invalidateChildrenCache: !subviews.isEmpty)
160
160
  }
161
161
 
162
162
  /**
@@ -246,8 +246,6 @@ class MarkerView: ExpoView {
246
246
  if let generated = self.createImageFromSubviews() {
247
247
  annotationView.image = generated
248
248
  self.applyCenterOffset(to: annotationView, defaultOffset: .zero)
249
- } else if self.hasPendingImageContent() {
250
- self.scheduleSubviewRefresh(allowFallbackToDefault: false)
251
249
  }
252
250
  }
253
251
  return annotationView
@@ -255,10 +253,12 @@ class MarkerView: ExpoView {
255
253
 
256
254
  // 2. 如果有 icon 属性,使用自定义图标
257
255
  if let iconUri = iconUri, !iconUri.isEmpty {
258
- let key = cacheKey ?? "icon|\(iconUri)|\(Int(iconWidth))x\(Int(iconHeight))"
256
+ let key = iconCacheKey(for: iconUri)
259
257
  if let cached = IconBitmapCache.shared.image(forKey: key) {
260
258
  annotationView?.image = cached
261
- annotationView?.centerOffset = CGPoint(x: 0, y: -cached.size.height / 2)
259
+ if let annotationView = annotationView {
260
+ applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -cached.size.height / 2))
261
+ }
262
262
  return annotationView
263
263
  }
264
264
 
@@ -274,7 +274,7 @@ class MarkerView: ExpoView {
274
274
  if let img = resizedImage {
275
275
  IconBitmapCache.shared.setImage(img, forKey: key)
276
276
  annotationView.image = img
277
- annotationView.centerOffset = CGPoint(x: 0, y: -img.size.height / 2)
277
+ self.applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -img.size.height / 2))
278
278
  }
279
279
  }
280
280
  return annotationView
@@ -391,10 +391,12 @@ class MarkerView: ExpoView {
391
391
  self.annotationView = annotationView
392
392
 
393
393
  // 构建 key
394
- let key = cacheKey ?? "icon|\(iconUri)|\(Int(iconWidth))x\(Int(iconHeight))"
394
+ let key = iconCacheKey(for: iconUri)
395
395
  if let cached = IconBitmapCache.shared.image(forKey: key) {
396
396
  annotationView?.image = cached
397
- annotationView?.centerOffset = CGPoint(x: 0, y: -cached.size.height / 2)
397
+ if let annotationView = annotationView {
398
+ applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -cached.size.height / 2))
399
+ }
398
400
  return annotationView
399
401
  }
400
402
 
@@ -411,7 +413,9 @@ class MarkerView: ExpoView {
411
413
  if let img = resizedImage {
412
414
  IconBitmapCache.shared.setImage(img, forKey: key)
413
415
  annotationView?.image = img
414
- annotationView?.centerOffset = CGPoint(x: 0, y: -img.size.height / 2)
416
+ if let annotationView = annotationView {
417
+ self.applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -img.size.height / 2))
418
+ }
415
419
  }
416
420
  }
417
421
  }
@@ -451,6 +455,9 @@ class MarkerView: ExpoView {
451
455
  pinView?.canShowCallout = canShowCallout
452
456
  pinView?.isDraggable = draggable
453
457
  pinView?.animatesDrop = animatesDrop
458
+ if let pinView = pinView {
459
+ applyCenterOffset(to: pinView, defaultOffset: .zero)
460
+ }
454
461
 
455
462
  self.annotationView = pinView
456
463
  return pinView
@@ -538,6 +545,41 @@ class MarkerView: ExpoView {
538
545
  return image
539
546
  }
540
547
 
548
+ private func iconCacheKey(for iconUri: String) -> String {
549
+ let baseKey = cacheKey ?? "icon|\(iconUri)"
550
+ return "\(baseKey)|\(Int(iconWidth.rounded()))x\(Int(iconHeight.rounded()))"
551
+ }
552
+
553
+ private func applyCenterOffset(to annotationView: MAAnnotationView, defaultOffset: CGPoint) {
554
+ annotationView.centerOffset = resolvedCenterOffset(defaultOffset: defaultOffset)
555
+ }
556
+
557
+ private func resolvedCenterOffset(defaultOffset: CGPoint) -> CGPoint {
558
+ guard let centerOffset else {
559
+ return defaultOffset
560
+ }
561
+
562
+ let x = centerOffset["x"] ?? Double(defaultOffset.x)
563
+ let y = centerOffset["y"] ?? Double(defaultOffset.y)
564
+ return CGPoint(x: x, y: y)
565
+ }
566
+
567
+ private func currentDefaultCenterOffset() -> CGPoint {
568
+ if !subviews.isEmpty {
569
+ return .zero
570
+ }
571
+
572
+ if let image = annotationView?.image ?? animatedAnnotationView?.image {
573
+ return CGPoint(x: 0, y: -image.size.height / 2)
574
+ }
575
+
576
+ if let iconUri, !iconUri.isEmpty {
577
+ return CGPoint(x: 0, y: -iconHeight / 2)
578
+ }
579
+
580
+ return .zero
581
+ }
582
+
541
583
  private func resolvedCustomSubviewSize(defaultSize: CGSize) -> CGSize {
542
584
  guard let firstSubview = subviews.first else {
543
585
  return defaultSize
@@ -808,6 +850,45 @@ class MarkerView: ExpoView {
808
850
  IconBitmapCache.shared.removeImage(forKey: childrenCacheKey(for: size))
809
851
  }
810
852
  }
853
+
854
+ private func refreshAnnotationAppearance(invalidateChildrenCache: Bool = false) {
855
+ guard !isRemoving else { return }
856
+
857
+ let refresh = { [weak self] in
858
+ guard let self = self, let mapView = self.mapView else { return }
859
+
860
+ self.pendingSubviewRefreshTask?.cancel()
861
+ self.pendingSubviewRefreshTask = nil
862
+
863
+ if invalidateChildrenCache {
864
+ self.lastRenderedChildrenSignature = nil
865
+ if !self.subviews.isEmpty {
866
+ self.invalidateCurrentChildrenCache()
867
+ }
868
+ }
869
+
870
+ if let animatedAnnotation = self.animatedAnnotation {
871
+ self.animatedAnnotationView = nil
872
+ mapView.removeAnnotation(animatedAnnotation)
873
+ mapView.addAnnotation(animatedAnnotation)
874
+ return
875
+ }
876
+
877
+ if let annotation = self.annotation {
878
+ self.annotationView = nil
879
+ mapView.removeAnnotation(annotation)
880
+ mapView.addAnnotation(annotation)
881
+ } else {
882
+ self.updateAnnotation()
883
+ }
884
+ }
885
+
886
+ if Thread.isMainThread {
887
+ refresh()
888
+ } else {
889
+ DispatchQueue.main.async(execute: refresh)
890
+ }
891
+ }
811
892
 
812
893
  /**
813
894
  * 设置纬度
@@ -893,28 +974,83 @@ class MarkerView: ExpoView {
893
974
  */
894
975
  func setDraggable(_ draggable: Bool) {
895
976
  self.draggable = draggable
977
+ annotationView?.isDraggable = draggable
978
+ animatedAnnotationView?.isDraggable = draggable
896
979
  updateAnnotation()
897
980
  }
898
981
 
899
982
  func setIconUri(_ uri: String?) {
900
983
  self.iconUri = uri
901
- updateAnnotation()
984
+ refreshAnnotationAppearance()
985
+ }
986
+
987
+ func setIconWidth(_ width: Double) {
988
+ guard iconWidth != width else { return }
989
+ iconWidth = width
990
+ if let iconUri, !iconUri.isEmpty {
991
+ refreshAnnotationAppearance()
992
+ }
993
+ }
994
+
995
+ func setIconHeight(_ height: Double) {
996
+ guard iconHeight != height else { return }
997
+ iconHeight = height
998
+ if let iconUri, !iconUri.isEmpty {
999
+ refreshAnnotationAppearance()
1000
+ }
1001
+ }
1002
+
1003
+ func setCustomViewWidth(_ width: Double) {
1004
+ guard customViewWidth != width else { return }
1005
+ customViewWidth = width
1006
+ if !subviews.isEmpty {
1007
+ refreshAnnotationAppearance(invalidateChildrenCache: true)
1008
+ }
1009
+ }
1010
+
1011
+ func setCustomViewHeight(_ height: Double) {
1012
+ guard customViewHeight != height else { return }
1013
+ customViewHeight = height
1014
+ if !subviews.isEmpty {
1015
+ refreshAnnotationAppearance(invalidateChildrenCache: true)
1016
+ }
902
1017
  }
903
1018
 
904
1019
  func setCenterOffset(_ offset: [String: Double]) {
905
1020
  self.centerOffset = offset
1021
+ if let annotationView = annotationView {
1022
+ applyCenterOffset(to: annotationView, defaultOffset: currentDefaultCenterOffset())
1023
+ }
1024
+ if let animatedAnnotationView = animatedAnnotationView {
1025
+ applyCenterOffset(to: animatedAnnotationView, defaultOffset: currentDefaultCenterOffset())
1026
+ }
906
1027
  }
907
1028
 
908
1029
  func setAnimatesDrop(_ animate: Bool) {
909
1030
  self.animatesDrop = animate
1031
+ (annotationView as? MAPinAnnotationView)?.animatesDrop = animate
910
1032
  }
911
1033
 
912
1034
  func setPinColor(_ color: String) {
1035
+ guard pinColor != color else { return }
913
1036
  self.pinColor = color
1037
+ if subviews.isEmpty && (iconUri?.isEmpty ?? true) {
1038
+ refreshAnnotationAppearance()
1039
+ }
914
1040
  }
915
1041
 
916
1042
  func setCanShowCallout(_ show: Bool) {
917
1043
  self.canShowCallout = show
1044
+ if subviews.isEmpty {
1045
+ annotationView?.canShowCallout = show
1046
+ animatedAnnotationView?.canShowCallout = show
1047
+ }
1048
+ }
1049
+
1050
+ func setGrowAnimation(_ enabled: Bool) {
1051
+ guard growAnimation != enabled else { return }
1052
+ growAnimation = enabled
1053
+ refreshAnnotationAppearance()
918
1054
  }
919
1055
 
920
1056
  // MARK: - 平滑移动相关方法
@@ -1125,6 +1261,7 @@ class MarkerView: ExpoView {
1125
1261
  // 取消待处理的任务
1126
1262
  pendingAddTask?.cancel()
1127
1263
  pendingUpdateTask?.cancel()
1264
+ pendingSubviewRefreshTask?.cancel()
1128
1265
 
1129
1266
  // 清理引用,防止内存泄漏
1130
1267
  mapView = nil