expo-gaode-map-navigation 2.0.12 → 2.0.14

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 (71) hide show
  1. package/README.md +29 -16
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
  6. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
  7. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
  8. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
  9. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
  10. package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
  11. package/build/index.d.ts +26 -126
  12. package/build/index.d.ts.map +1 -1
  13. package/build/index.js +11 -612
  14. package/build/index.js.map +1 -1
  15. package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
  16. package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  17. package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
  18. package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
  19. package/build/map/components/overlays/HeatMap.js +21 -2
  20. package/build/map/components/overlays/HeatMap.js.map +1 -1
  21. package/build/map/index.d.ts +3 -0
  22. package/build/map/index.d.ts.map +1 -1
  23. package/build/map/index.js +3 -0
  24. package/build/map/index.js.map +1 -1
  25. package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  26. package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  27. package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
  28. package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
  29. package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  30. package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  31. package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
  32. package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
  33. package/build/map/search/index.d.ts +156 -0
  34. package/build/map/search/index.d.ts.map +1 -0
  35. package/build/map/search/index.js +171 -0
  36. package/build/map/search/index.js.map +1 -0
  37. package/build/map/types/map-view.types.d.ts +4 -2
  38. package/build/map/types/map-view.types.d.ts.map +1 -1
  39. package/build/map/types/map-view.types.js.map +1 -1
  40. package/build/map/utils/ErrorHandler.js +11 -11
  41. package/build/map/utils/ErrorHandler.js.map +1 -1
  42. package/build/map/utils/OfflineMapManager.d.ts +4 -0
  43. package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
  44. package/build/map/utils/OfflineMapManager.js +6 -0
  45. package/build/map/utils/OfflineMapManager.js.map +1 -1
  46. package/build/route-geometry.d.ts +13 -0
  47. package/build/route-geometry.d.ts.map +1 -0
  48. package/build/route-geometry.js +154 -0
  49. package/build/route-geometry.js.map +1 -0
  50. package/build/route-planning.d.ts +21 -0
  51. package/build/route-planning.d.ts.map +1 -0
  52. package/build/route-planning.js +67 -0
  53. package/build/route-planning.js.map +1 -0
  54. package/build/web-api-fallback.d.ts +5 -0
  55. package/build/web-api-fallback.d.ts.map +1 -0
  56. package/build/web-api-fallback.js +160 -0
  57. package/build/web-api-fallback.js.map +1 -0
  58. package/build/web-route-following.d.ts +3 -0
  59. package/build/web-route-following.d.ts.map +1 -0
  60. package/build/web-route-following.js +178 -0
  61. package/build/web-route-following.js.map +1 -0
  62. package/expo-module.config.json +4 -2
  63. package/ios/ExpoGaodeMapNaviView.swift +16 -17
  64. package/ios/ExpoGaodeMapNavigation.podspec +2 -1
  65. package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
  66. package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
  67. package/ios/map/modules/LocationManager.swift +9 -3
  68. package/ios/map/overlays/PolylineView.swift +6 -12
  69. package/package.json +2 -2
  70. package/plugin/build/withGaodeMap.js +12 -0
  71. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
package/README.md CHANGED
@@ -5,15 +5,16 @@
5
5
  ## 特性
6
6
 
7
7
  - 🗺️ **地图渲染**:内置完整地图能力,支持 Marker、Polyline、Polygon、Circle、Cluster、HeatMap 等覆盖物。
8
+ - 🔍 **原生搜索**:内置 POI 搜索、周边搜索、沿途搜索、输入提示、逆地理编码等搜索能力。
8
9
  - 🚗 **多模式路径规划**:支持驾车、步行、骑行、电动车、货车、摩托车等多种出行方式。
9
- - 🧭 **实时导航 UI**:提供 `NaviView` 官方嵌入视图,并暴露完整事件与原生参数,方便你自行定制导航界面。
10
+ - 🧭 **实时导航 UI**:提供 `ExpoGaodeMapNaviView` 官方嵌入视图,并暴露完整事件与原生参数,方便你自行定制导航界面。
10
11
  - 🛣️ **独立路径规划**:支持“先算路、再导航”的高级模式,可实现多路线对比与选择。
11
12
  - ⚙️ **策略丰富**:支持速度优先、避让拥堵、少收费、不走高速等多种算路策略。
12
13
  - ✅ **开箱即用**:封装了 Android/iOS 原生导航 SDK,统一 JS 接口。
13
14
 
14
15
  ## 安装
15
16
 
16
- 本模块已包含地图与导航的所有能力,**不需要**、也不应同时安装 `expo-gaode-map`。
17
+ 本模块已包含地图、搜索与导航能力,**不需要**、也不应同时安装 `expo-gaode-map` 或 `expo-gaode-map-search`。
17
18
 
18
19
  ```bash
19
20
  # bun
@@ -29,6 +30,10 @@ npm install expo-gaode-map-navigation
29
30
  **⚠️ 重要提示:**
30
31
  如果项目中已安装 `expo-gaode-map`,请务必先卸载,否则会导致 Android 端二进制冲突(`3dmap` vs `navi-3dmap`)。`expo-gaode-map` 和 `expo-gaode-map-navigation` 由于 SDK 冲突不能同时安装,二选一使用。
31
32
 
33
+ `expo-gaode-map-search` 的独立集成只维护到 `2.2.33`。从 `2.0.13` 开始,搜索能力随 `expo-gaode-map` / `expo-gaode-map-navigation` 一起维护;在导航包中请直接从 `expo-gaode-map-navigation` 导入搜索 API。
34
+
35
+ 高德官方 Android SDK 在 `10.0.700` 之后将远程依赖由“地图 + 定位”调整为“地图 + 定位 + 搜索”,依赖地址从 `com.amap.api:3dmap:latest.integration` 调整为 `com.amap.api:3dmap-location-search:latest.integration`。继续单独维护 search 模块会带来重复合包和依赖冲突成本,因此搜索能力改为随 core / navigation 一起维护。
36
+
32
37
  > ⚠️ **版本兼容性说明**:
33
38
  > - 如果你的项目使用 **Expo SDK 54 及以上**,请安装 默认的 版本。
34
39
  > - 如果你的项目使用 **Expo SDK 53 及以下**(如 50, 51, 52, 53),请使用 **V1** 版本(Tag: `v1`)。
@@ -75,8 +80,8 @@ npx expo run:ios
75
80
  - `enableBackgroundAudio` 仅 iOS 生效(默认随 `enableBackgroundLocation` 自动开启),用于注入 `UIBackgroundModes: audio`,保障后台导航语音持续播报。
76
81
  - `enableIOSLiveActivity` 仅 iOS 生效,用于注入 `NSSupportsLiveActivities`。
77
82
  - `enableIOSLiveActivityFrequentUpdates` 仅 iOS 生效,用于注入 `NSSupportsLiveActivitiesFrequentUpdates`。
78
- - 运行时还需要在 `NaviView` 里显式传 `androidBackgroundNavigationNotificationEnabled={true}` 才会在应用退到后台后显示导航常驻通知。
79
- - iOS 运行时还需要在 `NaviView` 里显式传 `iosLiveActivityEnabled={true}` 才会持续更新 Live Activity。
83
+ - 运行时还需要在 `ExpoGaodeMapNaviView` 里显式传 `androidBackgroundNavigationNotificationEnabled={true}` 才会在应用退到后台后显示导航常驻通知。
84
+ - iOS 运行时还需要在 `ExpoGaodeMapNaviView` 里显式传 `iosLiveActivityEnabled={true}` 才会持续更新 Live Activity。
80
85
 
81
86
  ## 示例工程
82
87
 
@@ -84,7 +89,7 @@ npx expo run:ios
84
89
 
85
90
  推荐场景:
86
91
 
87
- - 调试 `NaviView` 与示例工程里的自定义 HUD / 车道 HUD / 路况光柱
92
+ - 调试 `ExpoGaodeMapNaviView` 与示例工程里的自定义 HUD / 车道 HUD / 路况光柱
88
93
  - 对比官方黑盒页、官方嵌入式页、自绘嵌入式页
89
94
  - 验证独立算路、多路线选择、近似跟线导航
90
95
 
@@ -165,15 +170,16 @@ export default function BasicMapScreen() {
165
170
 
166
171
  ### 2. 嵌入导航视图
167
172
 
168
- 使用 `NaviView` 组件直接嵌入导航界面:
173
+ 使用 `ExpoGaodeMapNaviView` 组件直接嵌入导航界面:
169
174
 
170
175
  ```tsx
171
176
  import React, { useEffect, useRef } from 'react';
172
177
  import { View } from 'react-native';
173
- import { NaviView, type NaviViewRef } from 'expo-gaode-map-navigation';
178
+ import { ExpoGaodeMapNaviView } from 'expo-gaode-map-navigation';
179
+ import type { ExpoGaodeMapNaviViewRef } from 'expo-gaode-map-navigation';
174
180
 
175
181
  export default function NavigationScreen() {
176
- const naviRef = useRef<NaviViewRef>(null);
182
+ const naviRef = useRef<ExpoGaodeMapNaviViewRef>(null);
177
183
 
178
184
  useEffect(() => {
179
185
  // 延迟 1 秒后开始导航
@@ -191,7 +197,7 @@ export default function NavigationScreen() {
191
197
 
192
198
  return (
193
199
  <View style={{ flex: 1 }}>
194
- <NaviView
200
+ <ExpoGaodeMapNaviView
195
201
  ref={naviRef}
196
202
  style={{ flex: 1 }}
197
203
  showCamera={true} // 显示摄像头
@@ -206,17 +212,17 @@ export default function NavigationScreen() {
206
212
 
207
213
  ### 3. 自定义嵌入式导航 UI
208
214
 
209
- 如果你要做“嵌入在自己页面里的导航页”,库本身提供的是底层 `NaviView`、导航事件和原生参数;完整的自定义 HUD / 车道 HUD / 路况光柱参考实现,已经迁移到仓库内的 [`example-navigation`](/Volumes/xinxin/expo-gaode-map/example-navigation/README.md)。
215
+ 如果你要做“嵌入在自己页面里的导航页”,库本身提供的是底层 `ExpoGaodeMapNaviView`、导航事件和原生参数;完整的自定义 HUD / 车道 HUD / 路况光柱参考实现,已经迁移到仓库内的 [`example-navigation`](/Volumes/xinxin/expo-gaode-map/example-navigation/README.md)。
210
216
 
211
217
  建议做法:
212
218
 
213
- - 用 `NaviView` 负责底层导航地图、语音、车道事件、路况事件、路口大图事件
219
+ - 用 `ExpoGaodeMapNaviView` 负责底层导航地图、语音、车道事件、路况事件、路口大图事件
214
220
  - 用 `onNaviInfoUpdate`、`onLaneInfoUpdate`、`onTrafficStatusesUpdate`、`onNaviVisualStateChange` 在业务侧自绘 HUD
215
221
  - 直接参考 `example-navigation/lib/navigation-ui/EmbeddedNaviView.tsx` 及配套 UI 文件,按你的产品需求裁剪
216
222
 
217
223
  注意:
218
224
 
219
- - Android 官方嵌入式 `NaviView` 在部分 React Native / Expo 宿主中,顶部信息区、车道条、路口大图联动效果可能与高德官方 Demo 不完全一致
225
+ - Android 官方嵌入式 `ExpoGaodeMapNaviView` 在部分 React Native / Expo 宿主中,顶部信息区、车道条、路口大图联动效果可能与高德官方 Demo 不完全一致
220
226
  - 如果你要验证官方嵌入式 UI 本身,请直接跑 `example-navigation` 里的 `official-embedded` 示例页
221
227
  - 如果你要交付稳定的嵌入式导航页,建议以示例工程里的“自定义 UI 导航界面”作为起点
222
228
 
@@ -489,6 +495,13 @@ const result = await calculateTransitRoute({
489
495
  - 可共享的范围仅限纯 TS 的 route / AOI 数据适配工具、文档和测试思路
490
496
  - 原生地图桥接、overlay 宿主逻辑、MapView facade 不会和核心包合并
491
497
 
498
+ ### 导入入口
499
+
500
+ - 运行时能力优先使用命名导出,例如 `calculateRoute`、`ExpoGaodeMapNaviView`、`openOfficialNaviPage`
501
+ - 类型请用 `import type`,例如 `RouteOptions`、`FollowWebPlannedRouteResult`
502
+ - 默认导出保留给需要整包挂载的场景,内容与常用路径规划和导航函数一致
503
+ - `NaviView` / `NaviViewRef` 仍作为兼容别名保留,新代码建议使用 `ExpoGaodeMapNaviView` / `ExpoGaodeMapNaviViewRef`
504
+
492
505
  ## API 参考
493
506
 
494
507
  ### DriveStrategy (驾车策略)
@@ -522,7 +535,7 @@ const result = await calculateTransitRoute({
522
535
  - 基于 `onTrafficStatusesUpdate` 的自绘路况光柱
523
536
  - “全览 / 锁车”与路况开关等浮层控制按钮
524
537
 
525
- ### NaviView Props
538
+ ### ExpoGaodeMapNaviView Props
526
539
 
527
540
  | 属性 | 类型 | 说明 |
528
541
  |---|---|---|
@@ -564,7 +577,7 @@ const result = await calculateTransitRoute({
564
577
  | `onNaviInfoUpdate` | function | 导航信息更新(剩余距离、时间等) |
565
578
  | `onLaneInfoUpdate` | function | Android / iOS 车道信息更新,用于自绘车道 HUD |
566
579
 
567
- ### NaviView UI 能力清单
580
+ ### ExpoGaodeMapNaviView UI 能力清单
568
581
 
569
582
  已开放且两端都有实现:
570
583
 
@@ -636,14 +649,14 @@ const result = await calculateTransitRoute({
636
649
  2. **Web API**:如果需要更灵活的 HTTP 算路(如公交跨城规划、Web端展示),推荐配合 `expo-gaode-map-web-api` 使用。
637
650
  3. **权限**:使用导航功能前,请确保应用已获取定位权限(`ACCESS_FINE_LOCATION`)。
638
651
  4. **Android 状态栏兼容性**:`naviStatusBarEnabled` 依赖高德 Android 导航 SDK 某些版本才提供的 `AMapNaviViewOptions.setNaviStatusBarEnabled(...)`。当前封装已做兼容处理:若宿主工程解析到的 SDK 不包含该方法,则不会再编译失败,而是在运行时跳过该设置并输出 warning。此时该 prop 在 Android 上等价于 no-op。
639
- 5. **嵌入式 UI 边界**:库导出的是底层 `NaviView` 能力;完整自定义导航界面请参考 `example-navigation` 里的示例实现,它也不是高德官方黑盒导航页的 UI 替代品。
652
+ 5. **嵌入式 UI 边界**:库导出的是底层 `ExpoGaodeMapNaviView` 能力;完整自定义导航界面请参考 `example-navigation` 里的示例实现,它也不是高德官方黑盒导航页的 UI 替代品。
640
653
 
641
654
 
642
655
  ## 📚 文档与资源
643
656
 
644
657
  - [在线文档](https://tomwq.github.io/expo-gaode-map/api/navigation.html)
645
658
  - [GitHub 仓库](https://github.com/TomWq/expo-gaode-map/packages/navigation)
646
- - [示例项目(导航)](https://github.com/TomWq/expo-gaode-map-navigation-example)
659
+ - [导航示例工程](../../example-navigation)
647
660
  - [高德地图开放平台](https://lbs.amap.com/)
648
661
  - [Expo Modules API](https://docs.expo.dev/modules/overview/)
649
662
 
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.gaodemap.navigation'
4
- version = '2.0.0'
4
+ version = '2.0.13'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -34,7 +34,7 @@ android {
34
34
  namespace "expo.modules.gaodemap.navigation"
35
35
  defaultConfig {
36
36
  versionCode 1
37
- versionName "2.0.0"
37
+ versionName "2.0.13"
38
38
  }
39
39
  lintOptions {
40
40
  abortOnError false
@@ -60,10 +60,14 @@ dependencies {
60
60
  implementation files(sdkFile)
61
61
  } else {
62
62
  println "ExpoGaodeMapNavigation: Custom SDK path configured but file not found, falling back to Maven dependency."
63
- implementation 'com.amap.api:navi-3dmap:latest.integration'
63
+ implementation 'com.amap.api:navi-3dmap-location-search:10.1.600_3dmap10.1.600_loc6.5.1_sea9.7.4'
64
+
64
65
  }
65
66
  } else {
66
- implementation 'com.amap.api:navi-3dmap:latest.integration'
67
+ // 10.1.600 is the last Android 3D map SDK line that supports offline
68
+ // map downloads without the separate 11.x offline-download permission.
69
+ implementation 'com.amap.api:navi-3dmap-location-search:10.1.600_3dmap10.1.600_loc6.5.1_sea9.7.4'
70
+
67
71
  }
68
72
 
69
73
  // Live Update APIs on NotificationCompat.Builder
@@ -21,4 +21,12 @@
21
21
  <!-- 蓝牙权限 - 用于室内定位 (可选) -->
22
22
  <uses-permission android:name="android.permission.BLUETOOTH" />
23
23
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
24
+
25
+ <application>
26
+ <!-- 高德 3D 地图 SDK 官方离线地图 UI 组件 -->
27
+ <activity
28
+ android:name="com.amap.api.maps.offlinemap.OfflineMapActivity"
29
+ android:screenOrientation="portrait"
30
+ android:exported="false" />
31
+ </application>
24
32
  </manifest>
@@ -1,14 +1,19 @@
1
1
  package expo.modules.gaodemap.map
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
6
9
  import android.util.Log
10
+ import com.amap.api.maps.offlinemap.OfflineMapActivity
7
11
  import com.amap.api.maps.offlinemap.OfflineMapCity
8
12
  import com.amap.api.maps.offlinemap.OfflineMapManager
9
13
  import com.amap.api.maps.offlinemap.OfflineMapProvince
10
14
  import com.amap.api.maps.offlinemap.OfflineMapStatus
11
15
  import expo.modules.gaodemap.map.modules.SDKInitializer
16
+ import expo.modules.kotlin.exception.CodedException
12
17
  import expo.modules.kotlin.modules.Module
13
18
  import expo.modules.kotlin.modules.ModuleDefinition
14
19
 
@@ -92,12 +97,18 @@ class ExpoGaodeMapOfflineModule : Module() {
92
97
  offlineMapManager = null
93
98
  downloadingCities.clear()
94
99
  }
100
+
101
+ AsyncFunction("openOfflineMapUI") {
102
+ openOfflineMapUI()
103
+ }
95
104
 
96
105
  // ==================== 地图列表管理 ====================
97
106
 
98
107
  AsyncFunction("getAvailableCities") {
99
- val cities = getOfflineMapManager()?.offlineMapCityList ?: emptyList()
100
- cities.map { city -> convertCityToMap(city) }
108
+ val manager = getOfflineMapManager()
109
+ val cities = manager?.offlineMapCityList ?: emptyList()
110
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
111
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
101
112
  }
102
113
 
103
114
  AsyncFunction("getAvailableProvinces") {
@@ -106,15 +117,19 @@ class ExpoGaodeMapOfflineModule : Module() {
106
117
  }
107
118
 
108
119
  AsyncFunction("getCitiesByProvince") { provinceCode: String ->
109
- val province = getOfflineMapManager()?.offlineMapProvinceList?.find {
110
- it.provinceCode == provinceCode
120
+ val manager = getOfflineMapManager()
121
+ val province = manager?.offlineMapProvinceList?.find {
122
+ it.provinceCode == provinceCode
111
123
  }
112
- province?.cityList?.map { city -> convertCityToMap(city) } ?: emptyList()
124
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
125
+ province?.cityList?.map { city -> convertCityToMap(city, downloadedCityKeys) } ?: emptyList()
113
126
  }
114
127
 
115
128
  AsyncFunction("getDownloadedMaps") {
116
- val cities = getOfflineMapManager()?.downloadOfflineMapCityList ?: emptyList()
117
- cities.map { city -> convertCityToMap(city) }
129
+ val manager = getOfflineMapManager()
130
+ val cities = manager?.downloadOfflineMapCityList ?: emptyList()
131
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
132
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
118
133
  }
119
134
 
120
135
  // ==================== 下载管理 ====================
@@ -208,14 +223,17 @@ class ExpoGaodeMapOfflineModule : Module() {
208
223
  // ==================== 状态查询 ====================
209
224
 
210
225
  AsyncFunction("isMapDownloaded") { cityCode: String ->
211
- val city = getOfflineMapManager()?.getItemByCityCode(cityCode)
212
- city?.state == OfflineMapStatus.SUCCESS ||
213
- city?.state == OfflineMapStatus.CHECKUPDATES
226
+ val manager = getOfflineMapManager()
227
+ val city = manager?.getItemByCityCode(cityCode)
228
+ city?.state == OfflineMapStatus.SUCCESS ||
229
+ city?.let { isDownloadedCity(it, getDownloadedCityKeys(manager)) } == true
214
230
  }
215
231
 
216
232
  AsyncFunction("getMapStatus") { cityCode: String ->
217
- val city = getOfflineMapManager()?.getItemByCityCode(cityCode)
218
- city?.let { convertCityToMap(it) } ?: Bundle()
233
+ val manager = getOfflineMapManager()
234
+ val city = manager?.getItemByCityCode(cityCode)
235
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
236
+ city?.let { convertCityToMap(it, downloadedCityKeys) } ?: Bundle()
219
237
  }
220
238
 
221
239
  AsyncFunction("getTotalProgress") {
@@ -463,13 +481,37 @@ class ExpoGaodeMapOfflineModule : Module() {
463
481
  /**
464
482
  * 转换城市对象为 Map
465
483
  */
466
- private fun convertCityToMap(city: OfflineMapCity): Bundle {
484
+ private fun getDownloadedCityKeys(manager: OfflineMapManager?): Set<String> {
485
+ return manager?.downloadOfflineMapCityList
486
+ ?.flatMap { getCityIdentityKeys(it) }
487
+ ?.toSet()
488
+ ?: emptySet()
489
+ }
490
+
491
+ private fun getCityIdentityKeys(city: OfflineMapCity): List<String> {
492
+ return listOfNotNull(
493
+ city.code?.trim()?.takeIf { it.isNotEmpty() }?.let { "code:$it" },
494
+ city.adcode?.trim()?.takeIf { it.isNotEmpty() }?.let { "adcode:$it" },
495
+ city.city?.trim()?.takeIf { it.isNotEmpty() }?.let { "name:$it" }
496
+ )
497
+ }
498
+
499
+ private fun isDownloadedCity(city: OfflineMapCity, downloadedCityKeys: Set<String>): Boolean {
500
+ return getCityIdentityKeys(city).any { downloadedCityKeys.contains(it) }
501
+ }
502
+
503
+ private fun convertCityToMap(
504
+ city: OfflineMapCity,
505
+ downloadedCityKeys: Set<String> = emptySet()
506
+ ): Bundle {
467
507
  val isPaused = synchronized(lock) { pausedCities.contains(city.code) }
468
508
  val isDownloading = synchronized(lock) { downloadingCities.contains(city.code) }
509
+ val isDownloaded = isDownloadedCity(city, downloadedCityKeys)
469
510
 
470
511
  val status = when {
471
512
  isPaused -> "paused"
472
513
  isDownloading -> "downloading"
514
+ isDownloaded -> "downloaded"
473
515
  else -> getStatusString(city.state)
474
516
  }
475
517
 
@@ -501,8 +543,9 @@ class ExpoGaodeMapOfflineModule : Module() {
501
543
  }
502
544
 
503
545
  /**
504
- * 获取状态字符串
505
- * 注意:只有 SUCCESS 状态才表示真正下载完成
546
+ * 获取状态字符串。
547
+ * CHECKUPDATES / NEW_VERSION 在 10.1.600 冷启动时可能出现在普通城市列表,
548
+ * 不能单独作为已下载依据;已下载状态以 downloadOfflineMapCityList 为准。
506
549
  */
507
550
  private fun getStatusString(state: Int): String {
508
551
  return when (state) {
@@ -558,4 +601,29 @@ class ExpoGaodeMapOfflineModule : Module() {
558
601
  null
559
602
  }
560
603
  }
604
+
605
+ private fun getActivityLaunchContext(): Context {
606
+ return appContext.currentActivity
607
+ ?: appContext.reactContext
608
+ ?: throw CodedException("NO_CONTEXT", "React context not available", null)
609
+ }
610
+
611
+ private fun openOfflineMapUI() {
612
+ val context = getActivityLaunchContext()
613
+ SDKInitializer.restorePersistedState(context.applicationContext)
614
+
615
+ if (!SDKInitializer.isPrivacyReady()) {
616
+ throw CodedException(
617
+ "PRIVACY_NOT_AGREED",
618
+ "隐私协议未完成确认,请先调用 setPrivacyConfig",
619
+ null
620
+ )
621
+ }
622
+
623
+ val intent = Intent(context, OfflineMapActivity::class.java)
624
+ if (context !is Activity) {
625
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
626
+ }
627
+ context.startActivity(intent)
628
+ }
561
629
  }
@@ -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
 
@@ -351,10 +352,14 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
351
352
  fun setInitialCameraPosition(position: Map<String, Any?>) {
352
353
  initialCameraPosition = position
353
354
 
355
+ if (hasAppliedInitialCameraPosition) {
356
+ return
357
+ }
358
+
354
359
  // 如果地图已加载,立即应用;否则缓存等待地图加载完成
355
360
  if (isMapLoaded) {
356
361
  mainHandler.post {
357
- applyInitialCameraPosition(position)
362
+ applyInitialCameraPositionIfNeeded(position)
358
363
  }
359
364
  } else {
360
365
  pendingCameraPosition = position
@@ -365,8 +370,12 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
365
370
  * 实际应用相机位置
366
371
  * @param position 相机位置配置
367
372
  */
368
- private fun applyInitialCameraPosition(position: Map<String, Any?>) {
373
+ private fun applyInitialCameraPositionIfNeeded(position: Map<String, Any?>) {
374
+ if (hasAppliedInitialCameraPosition) {
375
+ return
376
+ }
369
377
  cameraManager.setInitialCameraPosition(position)
378
+ hasAppliedInitialCameraPosition = true
370
379
  }
371
380
 
372
381
  // ==================== UI 控件和手势 ====================
@@ -643,6 +652,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
643
652
 
644
653
  // 销毁地图实例
645
654
  mapView.onDestroy()
655
+ hasAppliedInitialCameraPosition = false
646
656
  } catch (e: Exception) {
647
657
  // 静默处理异常,确保销毁流程不会中断
648
658
  android.util.Log.e("ExpoGaodeMapView", "Error destroying map", e)
@@ -87,36 +87,46 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
87
87
  // ==================== 图层显示 ====================
88
88
 
89
89
  private var currentLocationStyle: MyLocationStyle? = null
90
+ private var currentFollowUserLocation: Boolean = false
91
+ private var explicitLocationType: Int? = null
92
+
93
+ private fun parseLocationType(locationType: String): Int? {
94
+ return when (locationType) {
95
+ "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
96
+ "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
97
+ "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
98
+ "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
99
+ "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
100
+ "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
101
+ "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
102
+ "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
103
+ else -> null
104
+ }
105
+ }
106
+
107
+ private fun resolveLocationType(followUserLocation: Boolean): Int {
108
+ explicitLocationType?.let { return it }
109
+ return if (followUserLocation) {
110
+ MyLocationStyle.LOCATION_TYPE_FOLLOW
111
+ } else {
112
+ MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
113
+ }
114
+ }
90
115
 
91
116
  /**
92
117
  * 设置是否显示用户位置
93
118
  */
94
119
  fun setShowsUserLocation(show: Boolean, followUserLocation: Boolean = false) {
120
+ currentFollowUserLocation = followUserLocation
95
121
  if (show) {
96
- // 创建默认的定位样式
97
122
  if (currentLocationStyle == null) {
98
- currentLocationStyle = MyLocationStyle().apply {
99
- // 根据是否跟随设置定位类型
100
- val locationType = if (followUserLocation) {
101
- MyLocationStyle.LOCATION_TYPE_FOLLOW // 连续定位并跟随
102
- } else {
103
- MyLocationStyle.LOCATION_TYPE_SHOW // 只显示定位点,不跟随
104
- }
105
- myLocationType(locationType)
106
- interval(2000) // 2秒定位一次
107
- showMyLocation(true)
108
- }
109
- } else {
110
- // 更新定位类型
111
- val locationType = if (followUserLocation) {
112
- MyLocationStyle.LOCATION_TYPE_FOLLOW
113
- } else {
114
- MyLocationStyle.LOCATION_TYPE_SHOW
115
- }
116
- currentLocationStyle?.apply {
117
- myLocationType(locationType)
118
- interval(2000)
119
- }
123
+ currentLocationStyle = MyLocationStyle()
124
+ }
125
+
126
+ currentLocationStyle?.apply {
127
+ myLocationType(resolveLocationType(followUserLocation))
128
+ interval(2000) // 2秒定位一次
129
+ showMyLocation(true)
120
130
  }
121
131
 
122
132
  // 监听定位变化(用于通知 React Native)
@@ -171,9 +181,10 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
171
181
  */
172
182
  @SuppressLint("DiscouragedApi")
173
183
  fun setUserLocationRepresentation(config: Map<String, Any>) {
184
+ explicitLocationType = (config["locationType"] as? String)?.let(::parseLocationType)
185
+
174
186
  if (currentLocationStyle == null) {
175
187
  currentLocationStyle = MyLocationStyle().apply {
176
- myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE)
177
188
  interval(2000)
178
189
  showMyLocation(true)
179
190
  }
@@ -182,21 +193,7 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
182
193
  val style = currentLocationStyle!!
183
194
 
184
195
  // 定位蓝点展现模式 (locationType) - Android 支持8种模式
185
- val locationType = config["locationType"] as? String
186
- if (locationType != null) {
187
- val locationTypeValue = when (locationType) {
188
- "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
189
- "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
190
- "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
191
- "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
192
- "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
193
- "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
194
- "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
195
- "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
196
- else -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE // 默认值
197
- }
198
- style.myLocationType(locationTypeValue)
199
- }
196
+ style.myLocationType(resolveLocationType(currentFollowUserLocation))
200
197
 
201
198
  // 是否显示定位蓝点 (showMyLocation) - Android 5.1.0+ 支持
202
199
  // 注意:这个属性在 iOS 中没有对应项,是 Android 特有的
@@ -141,8 +141,11 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
141
141
  * 设置最小聚合数量
142
142
  */
143
143
  fun setMinClusterSize(size: Int) {
144
- Log.d("ClusterView", "setMinClusterSize: $size")
145
- minClusterSize = size
144
+ val nextSize = size.coerceAtLeast(1)
145
+ if (minClusterSize != nextSize) {
146
+ minClusterSize = nextSize
147
+ styleChanged = true
148
+ }
146
149
  updateClusters()
147
150
  }
148
151