expo-gaode-map 2.2.33 → 2.2.34

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 (57) hide show
  1. package/README.md +20 -14
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +14 -0
  4. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +7 -8
  5. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +150 -27
  6. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +24 -14
  7. package/android/src/main/java/expo/modules/gaodemap/managers/UIManager.kt +38 -41
  8. package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +18 -17
  9. package/android/src/main/java/expo/modules/gaodemap/overlays/CircleView.kt +3 -1
  10. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +6 -1
  11. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +124 -10
  12. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +2 -2
  13. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +10 -9
  14. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +7 -11
  15. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +3 -1
  16. package/android/src/main/java/expo/modules/gaodemap/overlays/PolygonView.kt +2 -1
  17. package/android/src/main/java/expo/modules/gaodemap/overlays/PolylineView.kt +1 -0
  18. package/android/src/main/java/expo/modules/gaodemap/search/ExpoGaodeMapSearchModule.kt +751 -0
  19. package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +5 -5
  20. package/android/src/main/java/expo/modules/gaodemap/utils/PermissionHelper.kt +13 -16
  21. package/build/ExpoGaodeMapOfflineModule.d.ts +5 -0
  22. package/build/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  23. package/build/ExpoGaodeMapOfflineModule.js.map +1 -1
  24. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  25. package/build/components/overlays/HeatMap.js +21 -2
  26. package/build/components/overlays/HeatMap.js.map +1 -1
  27. package/build/index.d.ts +3 -0
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +3 -0
  30. package/build/index.js.map +1 -1
  31. package/build/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  32. package/build/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  33. package/build/search/ExpoGaodeMapSearch.types.js +19 -0
  34. package/build/search/ExpoGaodeMapSearch.types.js.map +1 -0
  35. package/build/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  36. package/build/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  37. package/build/search/ExpoGaodeMapSearchModule.js +47 -0
  38. package/build/search/ExpoGaodeMapSearchModule.js.map +1 -0
  39. package/build/search/index.d.ts +156 -0
  40. package/build/search/index.d.ts.map +1 -0
  41. package/build/search/index.js +171 -0
  42. package/build/search/index.js.map +1 -0
  43. package/build/types/map-view.types.d.ts +4 -2
  44. package/build/types/map-view.types.d.ts.map +1 -1
  45. package/build/types/map-view.types.js.map +1 -1
  46. package/build/utils/OfflineMapManager.d.ts +4 -0
  47. package/build/utils/OfflineMapManager.d.ts.map +1 -1
  48. package/build/utils/OfflineMapManager.js +6 -0
  49. package/build/utils/OfflineMapManager.js.map +1 -1
  50. package/expo-module.config.json +4 -2
  51. package/ios/ExpoGaodeMap.podspec +2 -2
  52. package/ios/ExpoGaodeMapOfflineModule.swift +60 -0
  53. package/ios/ExpoGaodeMapSearchModule.swift +773 -0
  54. package/ios/modules/LocationManager.swift +9 -3
  55. package/ios/overlays/PolylineView.swift +6 -12
  56. package/package.json +1 -1
  57. package/plugin/build/withGaodeMap.js +12 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 📖 完整文档
8
8
 
9
- **👉 [在线文档网站](https://TomWq.github.io/expo-gaode-map/)** · **👉 [示例仓库](https://github.com/TomWq/expo-gaode-map-example)**
9
+ **👉 [在线文档网站](https://TomWq.github.io/expo-gaode-map/)** · **👉 本地示例:[`example/`](../../example) / [`example-navigation/`](../../example-navigation)**
10
10
 
11
11
  包含完整的 API 文档、使用指南和示例代码:
12
12
  - [快速开始](https://TomWq.github.io/expo-gaode-map/guide/getting-started.html)
@@ -15,7 +15,7 @@
15
15
  - [导航功能](https://TomWq.github.io/expo-gaode-map/guide/navigation.html)
16
16
  - [Web API](https://TomWq.github.io/expo-gaode-map/guide/web-api.html)
17
17
  - [API 参考](https://TomWq.github.io/expo-gaode-map/api/)
18
- - [使用示例](https://github.com/TomWq/expo-gaode-map-example)
18
+ - [本地示例工程](../../example) / [导航示例工程](../../example-navigation)
19
19
 
20
20
  ## ✨ 主要特性
21
21
 
@@ -37,10 +37,16 @@
37
37
 
38
38
 
39
39
  ### 可选模块
40
- - 🔍 **搜索功能**(expo-gaode-map-search)- POI 搜索、周边搜索、关键字搜索、地理编码等
40
+ - 🔍 **搜索功能** - 已内置在 `expo-gaode-map` 中,提供 POI 搜索、周边搜索、关键字搜索、地理编码等
41
41
  - 🧭 **导航功能**(expo-gaode-map-navigation)- 驾车、步行、骑行、货车路径规划,实时导航
42
42
  - 🌐 **Web API**(expo-gaode-map-web-api)- 纯 JavaScript 实现的路径规划、地理编码、POI 搜索等
43
43
 
44
+ > ⚠️ **Search 模块维护说明**
45
+ >
46
+ > 从下个版本开始,搜索能力已经集成到 `expo-gaode-map`(core)和 `expo-gaode-map-navigation` 的 map 能力中,`expo-gaode-map-search` 将不再作为独立模块继续维护。`2.2.33` 是最后一个支持 `expo-gaode-map-search` 单独集成的版本;新项目请直接从 `expo-gaode-map` 或 `expo-gaode-map-navigation` 导入搜索 API。
47
+ >
48
+ > 高德官方 Android SDK 在 `10.0.700` 之后将远程依赖由“地图 + 定位”调整为“地图 + 定位 + 搜索”,依赖地址从 `com.amap.api:3dmap:latest.integration` 调整为 `com.amap.api:3dmap-location-search:latest.integration`。继续单独维护 search 模块会带来重复合包和依赖冲突成本,因此搜索能力改为随 core / navigation 一起维护。
49
+
44
50
  ## 📦 安装
45
51
 
46
52
  > ⚠️ **版本兼容性说明**:
@@ -57,7 +63,6 @@
57
63
  npm install expo-gaode-map
58
64
 
59
65
  # 可选模块
60
- npm install expo-gaode-map-search # 搜索功能
61
66
  npm install expo-gaode-map-web-api # Web API
62
67
  ```
63
68
 
@@ -104,7 +109,7 @@ npx expo run:ios
104
109
 
105
110
  详细的初始化和使用指南请查看:
106
111
  - 📖 [快速开始文档](https://TomWq.github.io/expo-gaode-map/guide/getting-started.html)
107
- - 💻 [完整示例代码](https://github.com/TomWq/expo-gaode-map-example)
112
+ - 💻 [地图示例工程](../../example) / [导航示例工程](../../example-navigation)
108
113
 
109
114
  ## 📚 功能模块对比
110
115
 
@@ -113,8 +118,8 @@ npx expo run:ios
113
118
  | 地图显示 | ✅ | ❌ | ✅ | ❌ |
114
119
  | 定位 | ✅ | ❌ | ✅ | ❌ |
115
120
  | 覆盖物 | ✅ | ❌ | ✅ | ❌ |
116
- | POI 搜索 | | | | ✅ |
117
- | 地理编码 | | | | ✅ |
121
+ | POI 搜索 | | ⚠️ 2.2.33 及以下 | | ✅ |
122
+ | 地理编码 | | ⚠️ 2.2.33 及以下 | | ✅ |
118
123
  | 路径规划 | ❌ | ❌ | ✅ | ✅ |
119
124
  | 实时导航 | ❌ | ❌ | ✅ | ❌ |
120
125
  | 平台 | 原生 | 原生 | 原生 | Web/原生 |
@@ -125,11 +130,11 @@ npx expo run:ios
125
130
  expo-gaode-map/
126
131
  ├── packages/
127
132
  │ ├── core/ # expo-gaode-map(核心包)
128
- │ │ └── 地图显示、定位、覆盖物
129
- │ ├── search/ # expo-gaode-map-search(搜索包)
130
- │ │ └── POI 搜索、地理编码
133
+ │ │ └── 地图显示、定位、覆盖物、搜索
134
+ │ ├── search/ # expo-gaode-map-search(独立搜索包,2.2.33 后不再维护)
135
+ │ │ └── POI 搜索、地理编码(历史兼容)
131
136
  │ ├── navigation/ # expo-gaode-map-navigation(导航包)
132
- │ │ └── 地图+导航(替代 core)
137
+ │ │ └── 地图+搜索+导航(替代 core)
133
138
  │ └── web-api/ # expo-gaode-map-web-api(Web API)
134
139
  │ └── 纯 JS 实现的POI 搜索、地理编码、路径规划等
135
140
  └── 注意:core 和 navigation 不能同时安装
@@ -145,7 +150,8 @@ expo-gaode-map/
145
150
 
146
151
  ### 2. 搜索功能和 Web API 有什么区别?
147
152
 
148
- - **搜索包**(`expo-gaode-map-search`):原生实现,性能更好,无需网络请求,需要配置原生环境
153
+ - **内置原生搜索**(`expo-gaode-map` / `expo-gaode-map-navigation`):原生实现,性能更好,无需网络请求,需要配置原生环境
154
+ - **独立搜索包**(`expo-gaode-map-search`):仅建议历史项目固定 `2.2.33` 使用,后续不再单独维护
149
155
  - **Web API**(`expo-gaode-map-web-api`):纯 JavaScript,无需原生配置,跨平台更好,需要网络请求,但功能更加强大和完善
150
156
 
151
157
  ### 3. 如何配置 API Key?
@@ -231,8 +237,8 @@ MIT
231
237
  - [错误处理指南](./ERROR_HANDLING_GUIDE.md) 🆕
232
238
  - [性能优化指南](./PERFORMANCE_GUIDE.md) 🆕
233
239
  - [GitHub 仓库](https://github.com/TomWq/expo-gaode-map)
234
- - [示例项目(地图)](https://github.com/TomWq/expo-gaode-map-example)
235
- - [示例项目(导航)](https://github.com/TomWq/expo-gaode-map-navigation-example)
240
+ - [地图示例工程](../../example)
241
+ - [导航示例工程](../../example-navigation)
236
242
  - [高德地图开放平台](https://lbs.amap.com/)
237
243
  - [Expo Modules API](https://docs.expo.dev/modules/overview/)
238
244
 
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.gaodemap'
4
- version = '2.2.11'
4
+ version = '2.2.34'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -38,7 +38,7 @@ android {
38
38
  namespace "expo.modules.gaodemap"
39
39
  defaultConfig {
40
40
  versionCode 1
41
- versionName "2.2.11"
41
+ versionName "2.2.34"
42
42
  externalNativeBuild {
43
43
  cmake {
44
44
  cppFlags "-std=c++17"
@@ -55,8 +55,10 @@ android {
55
55
  }
56
56
  }
57
57
 
58
+
59
+
58
60
  dependencies {
59
- // 高德地图 3D SDK
61
+ // 高德地图组合 SDK:地图 + 定位 + 搜索。
60
62
  def customSdkPath = null
61
63
  println "ExpoGaodeMap: Checking for custom SDK property in rootProject: ${rootProject.name}"
62
64
  if (rootProject.hasProperty("EXPO_GAODE_MAP_CUSTOM_SDK_PATH")) {
@@ -80,6 +82,8 @@ dependencies {
80
82
  throw new FileNotFoundException("ExpoGaodeMap: Could not find custom SDK at ${customSdkPath}. Please check your customMapSdkPath configuration.")
81
83
  }
82
84
  } else {
83
- implementation ('com.amap.api:3dmap:latest.integration')
85
+ // 10.1.600 is the last Android 3D map SDK line that supports offline
86
+ // map downloads without the separate 11.x offline-download permission.
87
+ implementation('com.amap.api:3dmap-location-search:10.1.700_loc6.5.1_sea9.7.4')
84
88
  }
85
89
  }
@@ -5,6 +5,14 @@
5
5
  <uses-permission android:name="android.permission.INTERNET" />
6
6
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
7
7
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
8
+
9
+ <!-- 离线地图读写本地地图包 (Android 12 及以下) -->
10
+ <uses-permission
11
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE"
12
+ android:maxSdkVersion="32" />
13
+ <uses-permission
14
+ android:name="android.permission.READ_EXTERNAL_STORAGE"
15
+ android:maxSdkVersion="32" />
8
16
 
9
17
  <application allowBackup="false" >
10
18
 
@@ -13,6 +21,12 @@
13
21
  android:name="com.amap.api.location.APSService"
14
22
  android:enabled="true"
15
23
  android:exported="false" />
24
+
25
+ <!-- 高德 3D 地图 SDK 官方离线地图 UI 组件 -->
26
+ <activity
27
+ android:name="com.amap.api.maps.offlinemap.OfflineMapActivity"
28
+ android:screenOrientation="portrait"
29
+ android:exported="false" />
16
30
 
17
31
  </application>
18
32
 
@@ -1,7 +1,6 @@
1
1
  package expo.modules.gaodemap
2
2
 
3
3
  import com.amap.api.maps.MapsInitializer
4
- import com.amap.api.maps.model.LatLng
5
4
  import expo.modules.kotlin.modules.Module
6
5
  import expo.modules.kotlin.modules.ModuleDefinition
7
6
  import expo.modules.gaodemap.modules.SDKInitializer
@@ -170,8 +169,8 @@ class ExpoGaodeMapModule : Module() {
170
169
  AsyncFunction("coordinateConvert") { coordinate: Map<String, Any>?, type: Int, promise: expo.modules.kotlin.Promise ->
171
170
  val latLng = LatLngParser.parseLatLng(coordinate)
172
171
  if (latLng != null) {
173
- val coordMap = mapOf("latitude" to latLng.latitude, "longitude" to latLng.longitude)
174
- getLocationManager().coordinateConvert(coordMap, type, promise)
172
+ val cordMap = mapOf("latitude" to latLng.latitude, "longitude" to latLng.longitude)
173
+ getLocationManager().coordinateConvert(cordMap, type, promise)
175
174
  } else {
176
175
  promise.reject("INVALID_COORDINATE", "Invalid coordinate format", null)
177
176
  }
@@ -186,10 +185,10 @@ class ExpoGaodeMapModule : Module() {
186
185
  * @returns 两点之间的距离(单位:米)
187
186
  */
188
187
  Function("distanceBetweenCoordinates") { p1: Map<String, Any>?, p2: Map<String, Any>? ->
189
- val coord1 = LatLngParser.parseLatLng(p1)
190
- val coord2 = LatLngParser.parseLatLng(p2)
191
- if (coord1 != null && coord2 != null) {
192
- GeometryUtils.calculateDistance(coord1, coord2)
188
+ val cord1 = LatLngParser.parseLatLng(p1)
189
+ val cord2 = LatLngParser.parseLatLng(p2)
190
+ if (cord1 != null && cord2 != null) {
191
+ GeometryUtils.calculateDistance(cord1, cord2)
193
192
  } else {
194
193
  0.0
195
194
  }
@@ -580,7 +579,7 @@ class ExpoGaodeMapModule : Module() {
580
579
  * @param gridSizeMeters 网格大小(米)
581
580
  */
582
581
  Function("generateHeatmapGrid") { points: List<Map<String, Any>>?, gridSizeMeters: Double ->
583
- if (points == null || points.isEmpty()) return@Function emptyList<Map<String, Any>>()
582
+ if (points.isNullOrEmpty()) return@Function emptyList<Map<String, Any>>()
584
583
 
585
584
  val count = points.size
586
585
  val latitudes = DoubleArray(count)
@@ -1,8 +1,13 @@
1
1
  package expo.modules.gaodemap
2
2
 
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.Intent
3
6
  import android.os.Bundle
4
7
  import android.os.StatFs
5
8
  import android.os.Environment
9
+ import android.util.Log
10
+ import com.amap.api.maps.offlinemap.OfflineMapActivity
6
11
  import com.amap.api.maps.offlinemap.OfflineMapCity
7
12
  import com.amap.api.maps.offlinemap.OfflineMapManager
8
13
  import com.amap.api.maps.offlinemap.OfflineMapProvince
@@ -17,6 +22,7 @@ import expo.modules.kotlin.modules.ModuleDefinition
17
22
  */
18
23
  class ExpoGaodeMapOfflineModule : Module() {
19
24
 
25
+ private val logTag = "ExpoGaodeMapOffline"
20
26
  private var offlineMapManager: OfflineMapManager? = null
21
27
  private val downloadingCities = mutableSetOf<String>()
22
28
  private val pausedCities = mutableSetOf<String>()
@@ -39,6 +45,11 @@ class ExpoGaodeMapOfflineModule : Module() {
39
45
  }
40
46
 
41
47
  private fun getOfflineMapManager(): OfflineMapManager {
48
+ val reactContext = appContext.reactContext
49
+ ?: throw CodedException("NO_CONTEXT", "React context not available", null)
50
+
51
+ SDKInitializer.restorePersistedState(reactContext.applicationContext)
52
+
42
53
  if (!SDKInitializer.isPrivacyReady()) {
43
54
  throw CodedException(
44
55
  "PRIVACY_NOT_AGREED",
@@ -47,9 +58,6 @@ class ExpoGaodeMapOfflineModule : Module() {
47
58
  )
48
59
  }
49
60
 
50
- val reactContext = appContext.reactContext
51
- ?: throw CodedException("NO_CONTEXT", "React context not available", null)
52
-
53
61
  if (offlineMapManager == null) {
54
62
  offlineMapManager = OfflineMapManager(
55
63
  reactContext.applicationContext,
@@ -59,6 +67,31 @@ class ExpoGaodeMapOfflineModule : Module() {
59
67
 
60
68
  return offlineMapManager!!
61
69
  }
70
+
71
+ private fun getActivityLaunchContext(): Context {
72
+ return appContext.currentActivity
73
+ ?: appContext.reactContext
74
+ ?: throw CodedException("NO_CONTEXT", "React context not available", null)
75
+ }
76
+
77
+ private fun openOfflineMapUI() {
78
+ val context = getActivityLaunchContext()
79
+ SDKInitializer.restorePersistedState(context.applicationContext)
80
+
81
+ if (!SDKInitializer.isPrivacyReady()) {
82
+ throw CodedException(
83
+ "PRIVACY_NOT_AGREED",
84
+ "隐私协议未完成确认,请先调用 setPrivacyConfig",
85
+ null
86
+ )
87
+ }
88
+
89
+ val intent = Intent(context, OfflineMapActivity::class.java)
90
+ if (context !is Activity) {
91
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
92
+ }
93
+ context.startActivity(intent)
94
+ }
62
95
 
63
96
  override fun definition() = ModuleDefinition {
64
97
  Name("ExpoGaodeMapOffline")
@@ -79,12 +112,18 @@ class ExpoGaodeMapOfflineModule : Module() {
79
112
  offlineMapManager = null
80
113
  downloadingCities.clear()
81
114
  }
115
+
116
+ AsyncFunction("openOfflineMapUI") {
117
+ openOfflineMapUI()
118
+ }
82
119
 
83
120
  // ==================== 地图列表管理 ====================
84
121
 
85
122
  AsyncFunction("getAvailableCities") {
86
- val cities = getOfflineMapManager().offlineMapCityList ?: emptyList()
87
- cities.map { city -> convertCityToMap(city) }
123
+ val manager = getOfflineMapManager()
124
+ val cities = manager.offlineMapCityList ?: emptyList()
125
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
126
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
88
127
  }
89
128
 
90
129
  AsyncFunction("getAvailableProvinces") {
@@ -93,15 +132,19 @@ class ExpoGaodeMapOfflineModule : Module() {
93
132
  }
94
133
 
95
134
  AsyncFunction("getCitiesByProvince") { provinceCode: String ->
96
- val province = getOfflineMapManager().offlineMapProvinceList?.find {
135
+ val manager = getOfflineMapManager()
136
+ val province = manager.offlineMapProvinceList?.find {
97
137
  it.provinceCode == provinceCode
98
138
  }
99
- province?.cityList?.map { city -> convertCityToMap(city) } ?: emptyList()
139
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
140
+ province?.cityList?.map { city -> convertCityToMap(city, downloadedCityKeys) } ?: emptyList()
100
141
  }
101
142
 
102
143
  AsyncFunction("getDownloadedMaps") {
103
- val cities = getOfflineMapManager().downloadOfflineMapCityList ?: emptyList()
104
- cities.map { city -> convertCityToMap(city) }
144
+ val manager = getOfflineMapManager()
145
+ val cities = manager.downloadOfflineMapCityList ?: emptyList()
146
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
147
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
105
148
  }
106
149
 
107
150
  // ==================== 下载管理 ====================
@@ -114,7 +157,7 @@ class ExpoGaodeMapOfflineModule : Module() {
114
157
  downloadingCities.add(cityCode)
115
158
  pausedCities.remove(cityCode)
116
159
  }
117
- getOfflineMapManager().downloadByCityCode(cityCode)
160
+ startCityDownload(getOfflineMapManager(), cityCode, "startDownload")
118
161
  }
119
162
 
120
163
  AsyncFunction("pauseDownload") { cityCode: String ->
@@ -145,7 +188,7 @@ class ExpoGaodeMapOfflineModule : Module() {
145
188
  }
146
189
  // Android SDK 没有针对单个城市的恢复方法
147
190
  // 需要重新调用 downloadByCityCode 来继续下载
148
- getOfflineMapManager().downloadByCityCode(cityCode)
191
+ startCityDownload(getOfflineMapManager(), cityCode, "resumeDownload")
149
192
  }
150
193
 
151
194
  AsyncFunction("cancelDownload") { cityCode: String ->
@@ -195,14 +238,17 @@ class ExpoGaodeMapOfflineModule : Module() {
195
238
  // ==================== 状态查询 ====================
196
239
 
197
240
  AsyncFunction("isMapDownloaded") { cityCode: String ->
198
- val city = getOfflineMapManager().getItemByCityCode(cityCode)
199
- city?.state == OfflineMapStatus.SUCCESS ||
200
- city?.state == OfflineMapStatus.CHECKUPDATES
241
+ val manager = getOfflineMapManager()
242
+ val city = manager.getItemByCityCode(cityCode)
243
+ city?.state == OfflineMapStatus.SUCCESS ||
244
+ city?.let { isDownloadedCity(it, getDownloadedCityKeys(manager)) } == true
201
245
  }
202
246
 
203
247
  AsyncFunction("getMapStatus") { cityCode: String ->
204
- val city = getOfflineMapManager().getItemByCityCode(cityCode)
205
- city?.let { convertCityToMap(it) } ?: Bundle()
248
+ val manager = getOfflineMapManager()
249
+ val city = manager.getItemByCityCode(cityCode)
250
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
251
+ city?.let { convertCityToMap(it, downloadedCityKeys) } ?: Bundle()
206
252
  }
207
253
 
208
254
  AsyncFunction("getTotalProgress") {
@@ -279,7 +325,7 @@ class ExpoGaodeMapOfflineModule : Module() {
279
325
  }
280
326
  }
281
327
  cityCodes.forEach { cityCode ->
282
- getOfflineMapManager().downloadByCityCode(cityCode)
328
+ startCityDownload(getOfflineMapManager(), cityCode, "batchDownload")
283
329
  }
284
330
  }
285
331
 
@@ -340,17 +386,53 @@ class ExpoGaodeMapOfflineModule : Module() {
340
386
  downloadingCities.add(cityCode)
341
387
  pausedCities.remove(cityCode)
342
388
  }
343
- getOfflineMapManager().downloadByCityCode(cityCode)
389
+ startCityDownload(getOfflineMapManager(), cityCode, "resumeAllDownloads")
344
390
  }
345
391
  }
346
392
  }
347
393
 
348
394
  // ==================== 辅助方法 ====================
395
+
396
+ private fun findCity(manager: OfflineMapManager, cityCode: String): OfflineMapCity? {
397
+ return manager.getItemByCityCode(cityCode)
398
+ ?: manager.offlineMapCityList?.find { it.code == cityCode || it.adcode == cityCode }
399
+ }
400
+
401
+ private fun startCityDownload(manager: OfflineMapManager, cityCode: String, action: String) {
402
+ val city = findCity(manager, cityCode)
403
+ ?: throw IllegalArgumentException("City not found: $cityCode")
404
+
405
+ logCityBeforeDownload(city, cityCode, action)
406
+ downloadCity(manager, city)
407
+ }
408
+
409
+ private fun downloadCity(manager: OfflineMapManager, city: OfflineMapCity) {
410
+ try {
411
+ manager.downloadByCityCode(city.code)
412
+ return
413
+ } catch (codeError: Exception) {
414
+ Log.w(logTag, "downloadByCityCode failed cityCode=${city.code} cityName=${city.city} error=${codeError.message}")
415
+ try {
416
+ manager.downloadByCityName(city.city)
417
+ } catch (nameError: Exception) {
418
+ throw IllegalStateException(
419
+ "离线地图下载失败: cityCode=${city.code}, cityName=${city.city}, codeError=${codeError.message}, nameError=${nameError.message}",
420
+ nameError
421
+ )
422
+ }
423
+ }
424
+ }
425
+
426
+ private fun logCityBeforeDownload(city: OfflineMapCity?, cityCode: String, action: String) {
427
+ Log.i(logTag, "$action cityCode=$cityCode cityName=${city?.city} code=${city?.code} adcode=${city?.adcode} state=${city?.state} progress=${city?.let { getDownloadProgress(it) }} size=${city?.size} url=${city?.url}")
428
+ }
349
429
 
350
430
  /**
351
431
  * 处理下载状态回调
352
432
  */
353
433
  private fun handleDownloadStatus(status: Int, completeCode: Int, downName: String?) {
434
+ Log.i(logTag, "onDownload raw status=$status completeCode=$completeCode downName=$downName")
435
+
354
436
  if (downName == null) return
355
437
 
356
438
  // downName 可能是城市代码或城市名称,尝试两种方式查找
@@ -360,10 +442,14 @@ class ExpoGaodeMapOfflineModule : Module() {
360
442
  city = manager.offlineMapCityList?.find { it.city == downName }
361
443
  }
362
444
 
363
- if (city == null) return
445
+ if (city == null) {
446
+ Log.w(logTag, "onDownload city not found for downName=$downName status=$status completeCode=$completeCode")
447
+ return
448
+ }
364
449
 
365
450
  val cityCode = city.code
366
451
  val cityName = city.city
452
+ Log.i(logTag, "onDownload city cityCode=$cityCode cityName=$cityName adcode=${city.adcode} state=${city.state} progress=${getDownloadProgress(city)} size=${city.size} url=${city.url} status=$status completeCode=$completeCode")
367
453
 
368
454
  when (status) {
369
455
  OfflineMapStatus.SUCCESS -> {
@@ -404,6 +490,18 @@ class ExpoGaodeMapOfflineModule : Module() {
404
490
  putString("error", "解压失败,数据可能有问题")
405
491
  })
406
492
  }
493
+
494
+ startDownloadFailedCode -> {
495
+ synchronized(lock) {
496
+ downloadingCities.remove(cityCode)
497
+ }
498
+ sendEvent("onDownloadError", Bundle().apply {
499
+ putString("cityCode", cityCode)
500
+ putString("cityName", cityName)
501
+ putString("error", "下载地址为空或离线包不可用")
502
+ putInt("errorCode", status)
503
+ })
504
+ }
407
505
 
408
506
  OfflineMapStatus.EXCEPTION_NETWORK_LOADING -> {
409
507
  sendEvent("onDownloadError", Bundle().apply {
@@ -451,13 +549,37 @@ class ExpoGaodeMapOfflineModule : Module() {
451
549
  /**
452
550
  * 转换城市对象为 Map
453
551
  */
454
- private fun convertCityToMap(city: OfflineMapCity): Bundle {
552
+ private fun getDownloadedCityKeys(manager: OfflineMapManager): Set<String> {
553
+ return manager.downloadOfflineMapCityList
554
+ ?.flatMap { getCityIdentityKeys(it) }
555
+ ?.toSet()
556
+ ?: emptySet()
557
+ }
558
+
559
+ private fun getCityIdentityKeys(city: OfflineMapCity): List<String> {
560
+ return listOfNotNull(
561
+ city.code?.trim()?.takeIf { it.isNotEmpty() }?.let { "code:$it" },
562
+ city.adcode?.trim()?.takeIf { it.isNotEmpty() }?.let { "adcode:$it" },
563
+ city.city?.trim()?.takeIf { it.isNotEmpty() }?.let { "name:$it" }
564
+ )
565
+ }
566
+
567
+ private fun isDownloadedCity(city: OfflineMapCity, downloadedCityKeys: Set<String>): Boolean {
568
+ return getCityIdentityKeys(city).any { downloadedCityKeys.contains(it) }
569
+ }
570
+
571
+ private fun convertCityToMap(
572
+ city: OfflineMapCity,
573
+ downloadedCityKeys: Set<String> = emptySet()
574
+ ): Bundle {
455
575
  val isPaused = synchronized(lock) { pausedCities.contains(city.code) }
456
576
  val isDownloading = synchronized(lock) { downloadingCities.contains(city.code) }
577
+ val isDownloaded = isDownloadedCity(city, downloadedCityKeys)
457
578
 
458
579
  val status = when {
459
580
  isPaused -> "paused"
460
581
  isDownloading -> "downloading"
582
+ isDownloaded -> "downloaded"
461
583
  else -> getStatusString(city.state)
462
584
  }
463
585
 
@@ -495,10 +617,10 @@ class ExpoGaodeMapOfflineModule : Module() {
495
617
  private val startDownloadFailedCode: Int by lazy {
496
618
  try {
497
619
  OfflineMapStatus::class.java.getField("START_DOWNLOAD_FAILED").getInt(null)
498
- } catch (e: Exception) {
620
+ } catch (_: Exception) {
499
621
  try {
500
622
  OfflineMapStatus::class.java.getField("START_DOWNLOAD_FAILD").getInt(null)
501
- } catch (e2: Exception) {
623
+ } catch (_: Exception) {
502
624
  -1
503
625
  }
504
626
  }
@@ -513,17 +635,17 @@ class ExpoGaodeMapOfflineModule : Module() {
513
635
  // 尝试标准版的命名 (getcompleteCode)
514
636
  val method = obj.javaClass.getMethod("getcompleteCode")
515
637
  return method.invoke(obj) as Int
516
- } catch (e: Exception) {
638
+ } catch (_: Exception) {
517
639
  try {
518
640
  // 尝试修正后的命名 (getCompleteCode) - Google Play 版本可能使用此命名
519
641
  val method = obj.javaClass.getMethod("getCompleteCode")
520
642
  return method.invoke(obj) as Int
521
- } catch (e2: Exception) {
643
+ } catch (_: Exception) {
522
644
  // 如果都失败了,尝试直接访问 completeCode 字段
523
645
  try {
524
646
  val field = obj.javaClass.getField("completeCode")
525
647
  return field.getInt(obj)
526
- } catch (e3: Exception) {
648
+ } catch (_: Exception) {
527
649
  return 0
528
650
  }
529
651
  }
@@ -531,8 +653,9 @@ class ExpoGaodeMapOfflineModule : Module() {
531
653
  }
532
654
 
533
655
  /**
534
- * 获取状态字符串
535
- * 注意:只有 SUCCESS 状态才表示真正下载完成
656
+ * 获取状态字符串。
657
+ * CHECKUPDATES / NEW_VERSION 在 10.1.600 冷启动时可能出现在普通城市列表,
658
+ * 不能单独作为已下载依据;已下载状态以 downloadOfflineMapCityList 为准。
536
659
  */
537
660
  private fun getStatusString(state: Int): String {
538
661
  return when (state) {
@@ -7,7 +7,6 @@ import android.view.View
7
7
  import android.view.ViewGroup
8
8
  import com.amap.api.maps.AMap
9
9
  import com.amap.api.maps.TextureMapView
10
- import com.amap.api.maps.MapsInitializer
11
10
  import com.amap.api.maps.model.LatLng
12
11
  import expo.modules.kotlin.AppContext
13
12
  import expo.modules.kotlin.viewevent.EventDispatcher
@@ -19,6 +18,7 @@ import expo.modules.gaodemap.overlays.*
19
18
  import androidx.core.graphics.createBitmap
20
19
  import androidx.core.view.isVisible
21
20
  import androidx.core.graphics.withTranslation
21
+ import androidx.core.view.isGone
22
22
 
23
23
  /**
24
24
  * 高德地图视图组件
@@ -50,20 +50,20 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
50
50
  }
51
51
 
52
52
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
53
- val measuredWidth = View.MeasureSpec.getSize(widthMeasureSpec)
54
- val measuredHeight = View.MeasureSpec.getSize(heightMeasureSpec)
53
+ val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
54
+ val measuredHeight = MeasureSpec.getSize(heightMeasureSpec)
55
55
 
56
56
  setMeasuredDimension(measuredWidth, measuredHeight)
57
57
 
58
58
  for (i in 0 until childCount) {
59
59
  val child = getChildAt(i) ?: continue
60
- if (child.visibility == View.GONE) {
60
+ if (child.isGone) {
61
61
  continue
62
62
  }
63
63
 
64
64
  if (child === mapView) {
65
- val childWidthSpec = View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY)
66
- val childHeightSpec = View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY)
65
+ val childWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
66
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
67
67
  child.measure(childWidthSpec, childHeightSpec)
68
68
  continue
69
69
  }
@@ -71,15 +71,15 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
71
71
  val lp = child.layoutParams
72
72
  val childWidthSpec = when {
73
73
  lp?.width != null && lp.width > 0 ->
74
- View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY)
74
+ MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY)
75
75
  else ->
76
- View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.AT_MOST)
76
+ MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.AT_MOST)
77
77
  }
78
78
  val childHeightSpec = when {
79
79
  lp?.height != null && lp.height > 0 ->
80
- View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY)
80
+ MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY)
81
81
  else ->
82
- View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.AT_MOST)
82
+ MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.AT_MOST)
83
83
  }
84
84
  child.measure(childWidthSpec, childHeightSpec)
85
85
  }
@@ -91,7 +91,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
91
91
 
92
92
  for (i in 0 until childCount) {
93
93
  val child = getChildAt(i) ?: continue
94
- if (child.visibility == View.GONE) {
94
+ if (child.isGone) {
95
95
  continue
96
96
  }
97
97
 
@@ -150,6 +150,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
150
150
  // 缓存初始相机位置,等待地图加载完成后设置
151
151
  private var pendingCameraPosition: Map<String, Any?>? = null
152
152
  private var isMapLoaded = false
153
+ private var hasAppliedInitialCameraPosition = false
153
154
 
154
155
  init {
155
156
  try {
@@ -192,7 +193,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
192
193
 
193
194
  val positionToApply = initialCameraPosition ?: pendingCameraPosition
194
195
  positionToApply?.let { position ->
195
- applyInitialCameraPosition(position)
196
+ applyInitialCameraPositionIfNeeded(position)
196
197
  pendingCameraPosition = null
197
198
  }
198
199
 
@@ -352,10 +353,14 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
352
353
  fun setInitialCameraPosition(position: Map<String, Any?>) {
353
354
  initialCameraPosition = position
354
355
 
356
+ if (hasAppliedInitialCameraPosition) {
357
+ return
358
+ }
359
+
355
360
  // 如果地图已加载,立即应用;否则缓存等待地图加载完成
356
361
  if (isMapLoaded) {
357
362
  mainHandler.post {
358
- applyInitialCameraPosition(position)
363
+ applyInitialCameraPositionIfNeeded(position)
359
364
  }
360
365
  } else {
361
366
  pendingCameraPosition = position
@@ -366,8 +371,12 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
366
371
  * 实际应用相机位置
367
372
  * @param position 相机位置配置
368
373
  */
369
- private fun applyInitialCameraPosition(position: Map<String, Any?>) {
374
+ private fun applyInitialCameraPositionIfNeeded(position: Map<String, Any?>) {
375
+ if (hasAppliedInitialCameraPosition) {
376
+ return
377
+ }
370
378
  cameraManager.setInitialCameraPosition(position)
379
+ hasAppliedInitialCameraPosition = true
371
380
  }
372
381
 
373
382
  // ==================== UI 控件和手势 ====================
@@ -649,6 +658,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
649
658
 
650
659
  // 销毁地图实例
651
660
  mapView.onDestroy()
661
+ hasAppliedInitialCameraPosition = false
652
662
  } catch (e: Exception) {
653
663
  // 静默处理异常,确保销毁流程不会中断
654
664
  android.util.Log.e("ExpoGaodeMapView", "Error destroying map", e)