expo-gaode-map-navigation 2.0.12-next.0 → 2.0.13

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 (238) hide show
  1. package/README.md +296 -7
  2. package/android/build.gradle +12 -4
  3. package/android/src/main/AndroidManifest.xml +10 -1
  4. package/android/src/main/cpp/cluster_jni.cpp +56 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +49 -8
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
  8. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
  9. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +23 -17
  10. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
  11. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
  12. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
  13. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +37 -25
  14. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +6 -6
  15. package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
  16. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +103 -0
  17. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +1408 -394
  18. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +121 -1
  19. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +14 -28
  20. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/IndependentRouteListener.kt +28 -3
  21. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/RouteCalculateListener.kt +2 -2
  22. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/IndependentRouteManager.kt +114 -15
  23. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +24 -35
  24. package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +50 -36
  25. package/android/src/main/java/expo/modules/gaodemap/navigation/services/NavigationForegroundService.kt +661 -0
  26. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +21 -12
  27. package/android/src/main/res/drawable/ic_nav_notification_small.xml +10 -0
  28. package/android/src/main/res/drawable/landback_0.png +0 -0
  29. package/android/src/main/res/drawable/landback_1.png +0 -0
  30. package/android/src/main/res/drawable/landback_2.png +0 -0
  31. package/android/src/main/res/drawable/landback_3.png +0 -0
  32. package/android/src/main/res/drawable/landback_4.png +0 -0
  33. package/android/src/main/res/drawable/landback_5.png +0 -0
  34. package/android/src/main/res/drawable/landback_6.png +0 -0
  35. package/android/src/main/res/drawable/landback_7.png +0 -0
  36. package/android/src/main/res/drawable/landback_8.png +0 -0
  37. package/android/src/main/res/drawable/landback_9.png +0 -0
  38. package/android/src/main/res/drawable/landback_a.png +0 -0
  39. package/android/src/main/res/drawable/landback_b.png +0 -0
  40. package/android/src/main/res/drawable/landback_c.png +0 -0
  41. package/android/src/main/res/drawable/landback_d.png +0 -0
  42. package/android/src/main/res/drawable/landback_e.png +0 -0
  43. package/android/src/main/res/drawable/landback_f.png +0 -0
  44. package/android/src/main/res/drawable/landback_g.png +0 -0
  45. package/android/src/main/res/drawable/landback_h.png +0 -0
  46. package/android/src/main/res/drawable/landback_i.png +0 -0
  47. package/android/src/main/res/drawable/landback_j.png +0 -0
  48. package/android/src/main/res/drawable/landback_k.png +0 -0
  49. package/android/src/main/res/drawable/landback_l.png +0 -0
  50. package/android/src/main/res/drawable/landfront_0.png +0 -0
  51. package/android/src/main/res/drawable/landfront_00.png +0 -0
  52. package/android/src/main/res/drawable/landfront_1.png +0 -0
  53. package/android/src/main/res/drawable/landfront_11.png +0 -0
  54. package/android/src/main/res/drawable/landfront_20.png +0 -0
  55. package/android/src/main/res/drawable/landfront_21.png +0 -0
  56. package/android/src/main/res/drawable/landfront_22.png +0 -0
  57. package/android/src/main/res/drawable/landfront_3.png +0 -0
  58. package/android/src/main/res/drawable/landfront_33.png +0 -0
  59. package/android/src/main/res/drawable/landfront_40.png +0 -0
  60. package/android/src/main/res/drawable/landfront_43.png +0 -0
  61. package/android/src/main/res/drawable/landfront_44.png +0 -0
  62. package/android/src/main/res/drawable/landfront_5.png +0 -0
  63. package/android/src/main/res/drawable/landfront_55.png +0 -0
  64. package/android/src/main/res/drawable/landfront_61.png +0 -0
  65. package/android/src/main/res/drawable/landfront_63.png +0 -0
  66. package/android/src/main/res/drawable/landfront_66.png +0 -0
  67. package/android/src/main/res/drawable/landfront_70.png +0 -0
  68. package/android/src/main/res/drawable/landfront_71.png +0 -0
  69. package/android/src/main/res/drawable/landfront_73.png +0 -0
  70. package/android/src/main/res/drawable/landfront_77.png +0 -0
  71. package/android/src/main/res/drawable/landfront_8.png +0 -0
  72. package/android/src/main/res/drawable/landfront_88.png +0 -0
  73. package/android/src/main/res/drawable/landfront_90.png +0 -0
  74. package/android/src/main/res/drawable/landfront_95.png +0 -0
  75. package/android/src/main/res/drawable/landfront_99.png +0 -0
  76. package/android/src/main/res/drawable/landfront_a0.png +0 -0
  77. package/android/src/main/res/drawable/landfront_a8.png +0 -0
  78. package/android/src/main/res/drawable/landfront_aa.png +0 -0
  79. package/android/src/main/res/drawable/landfront_b1.png +0 -0
  80. package/android/src/main/res/drawable/landfront_b5.png +0 -0
  81. package/android/src/main/res/drawable/landfront_bb.png +0 -0
  82. package/android/src/main/res/drawable/landfront_c3.png +0 -0
  83. package/android/src/main/res/drawable/landfront_c8.png +0 -0
  84. package/android/src/main/res/drawable/landfront_cc.png +0 -0
  85. package/android/src/main/res/drawable/landfront_d.png +0 -0
  86. package/android/src/main/res/drawable/landfront_dd.png +0 -0
  87. package/android/src/main/res/drawable/landfront_e1.png +0 -0
  88. package/android/src/main/res/drawable/landfront_e5.png +0 -0
  89. package/android/src/main/res/drawable/landfront_ee.png +0 -0
  90. package/android/src/main/res/drawable/landfront_f0.png +0 -0
  91. package/android/src/main/res/drawable/landfront_f1.png +0 -0
  92. package/android/src/main/res/drawable/landfront_f5.png +0 -0
  93. package/android/src/main/res/drawable/landfront_ff.png +0 -0
  94. package/android/src/main/res/drawable/landfront_g3.png +0 -0
  95. package/android/src/main/res/drawable/landfront_g5.png +0 -0
  96. package/android/src/main/res/drawable/landfront_gg.png +0 -0
  97. package/android/src/main/res/drawable/landfront_h1.png +0 -0
  98. package/android/src/main/res/drawable/landfront_h3.png +0 -0
  99. package/android/src/main/res/drawable/landfront_h5.png +0 -0
  100. package/android/src/main/res/drawable/landfront_hh.png +0 -0
  101. package/android/src/main/res/drawable/landfront_i0.png +0 -0
  102. package/android/src/main/res/drawable/landfront_i3.png +0 -0
  103. package/android/src/main/res/drawable/landfront_i5.png +0 -0
  104. package/android/src/main/res/drawable/landfront_ii.png +0 -0
  105. package/android/src/main/res/drawable/landfront_j1.png +0 -0
  106. package/android/src/main/res/drawable/landfront_j8.png +0 -0
  107. package/android/src/main/res/drawable/landfront_jj.png +0 -0
  108. package/android/src/main/res/drawable/landfront_kk.png +0 -0
  109. package/android/src/main/res/drawable/landfront_ll.png +0 -0
  110. package/android/src/main/res/drawable/nav_notification_brand_icon.xml +16 -0
  111. package/android/src/main/res/drawable/navi_arrow_leftline.png +0 -0
  112. package/android/src/main/res/drawable/navi_lane_shape_bg_center.xml +5 -0
  113. package/android/src/main/res/drawable/navi_lane_shape_bg_left.xml +8 -0
  114. package/android/src/main/res/drawable/navi_lane_shape_bg_over.xml +6 -0
  115. package/android/src/main/res/drawable/navi_lane_shape_bg_right.xml +8 -0
  116. package/android/src/main/res/drawable-nodpi/nav_tracker_car.png +0 -0
  117. package/build/ExpoGaodeMapNaviView.d.ts +16 -0
  118. package/build/ExpoGaodeMapNaviView.d.ts.map +1 -1
  119. package/build/ExpoGaodeMapNaviView.js +74 -1
  120. package/build/ExpoGaodeMapNaviView.js.map +1 -1
  121. package/build/index.d.ts +56 -8
  122. package/build/index.d.ts.map +1 -1
  123. package/build/index.js +452 -10
  124. package/build/index.js.map +1 -1
  125. package/build/map/ExpoGaodeMapModule.d.ts +15 -13
  126. package/build/map/ExpoGaodeMapModule.d.ts.map +1 -1
  127. package/build/map/ExpoGaodeMapModule.js +31 -39
  128. package/build/map/ExpoGaodeMapModule.js.map +1 -1
  129. package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
  130. package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  131. package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
  132. package/build/map/ExpoGaodeMapView.d.ts +3 -4
  133. package/build/map/ExpoGaodeMapView.d.ts.map +1 -1
  134. package/build/map/ExpoGaodeMapView.js +28 -25
  135. package/build/map/ExpoGaodeMapView.js.map +1 -1
  136. package/build/map/components/overlays/Circle.d.ts.map +1 -1
  137. package/build/map/components/overlays/Circle.js +1 -30
  138. package/build/map/components/overlays/Circle.js.map +1 -1
  139. package/build/map/components/overlays/Cluster.d.ts.map +1 -1
  140. package/build/map/components/overlays/Cluster.js +1 -42
  141. package/build/map/components/overlays/Cluster.js.map +1 -1
  142. package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
  143. package/build/map/components/overlays/HeatMap.js +21 -21
  144. package/build/map/components/overlays/HeatMap.js.map +1 -1
  145. package/build/map/components/overlays/Marker.d.ts.map +1 -1
  146. package/build/map/components/overlays/Marker.js +76 -80
  147. package/build/map/components/overlays/Marker.js.map +1 -1
  148. package/build/map/components/overlays/Polygon.d.ts.map +1 -1
  149. package/build/map/components/overlays/Polygon.js +1 -25
  150. package/build/map/components/overlays/Polygon.js.map +1 -1
  151. package/build/map/components/overlays/Polyline.d.ts.map +1 -1
  152. package/build/map/components/overlays/Polyline.js +1 -31
  153. package/build/map/components/overlays/Polyline.js.map +1 -1
  154. package/build/map/index.d.ts +9 -2
  155. package/build/map/index.d.ts.map +1 -1
  156. package/build/map/index.js +9 -2
  157. package/build/map/index.js.map +1 -1
  158. package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  159. package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  160. package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
  161. package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
  162. package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  163. package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  164. package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
  165. package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
  166. package/build/map/search/index.d.ts +156 -0
  167. package/build/map/search/index.d.ts.map +1 -0
  168. package/build/map/search/index.js +171 -0
  169. package/build/map/search/index.js.map +1 -0
  170. package/build/map/types/index.d.ts +2 -2
  171. package/build/map/types/index.d.ts.map +1 -1
  172. package/build/map/types/index.js.map +1 -1
  173. package/build/map/types/map-view.types.d.ts +4 -2
  174. package/build/map/types/map-view.types.d.ts.map +1 -1
  175. package/build/map/types/map-view.types.js.map +1 -1
  176. package/build/map/types/native-module.types.d.ts +11 -12
  177. package/build/map/types/native-module.types.d.ts.map +1 -1
  178. package/build/map/types/native-module.types.js.map +1 -1
  179. package/build/map/types/overlays.types.d.ts +9 -14
  180. package/build/map/types/overlays.types.d.ts.map +1 -1
  181. package/build/map/types/overlays.types.js.map +1 -1
  182. package/build/map/types/route-playback.types.d.ts +16 -0
  183. package/build/map/types/route-playback.types.d.ts.map +1 -1
  184. package/build/map/types/route-playback.types.js.map +1 -1
  185. package/build/map/utils/ErrorHandler.js +11 -11
  186. package/build/map/utils/ErrorHandler.js.map +1 -1
  187. package/build/map/utils/OfflineMapManager.d.ts +4 -0
  188. package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
  189. package/build/map/utils/OfflineMapManager.js +6 -0
  190. package/build/map/utils/OfflineMapManager.js.map +1 -1
  191. package/build/types/coordinates.types.d.ts +3 -0
  192. package/build/types/coordinates.types.d.ts.map +1 -1
  193. package/build/types/coordinates.types.js.map +1 -1
  194. package/build/types/independent.types.d.ts +111 -12
  195. package/build/types/independent.types.d.ts.map +1 -1
  196. package/build/types/independent.types.js.map +1 -1
  197. package/build/types/native-module.types.d.ts +1 -1
  198. package/build/types/native-module.types.js.map +1 -1
  199. package/build/types/naviview.types.d.ts +304 -14
  200. package/build/types/naviview.types.d.ts.map +1 -1
  201. package/build/types/naviview.types.js.map +1 -1
  202. package/build/types/route.types.d.ts +12 -2
  203. package/build/types/route.types.d.ts.map +1 -1
  204. package/build/types/route.types.js.map +1 -1
  205. package/expo-module.config.json +4 -2
  206. package/ios/ExpoGaodeMapNaviView.swift +2331 -201
  207. package/ios/ExpoGaodeMapNaviViewModule.swift +109 -1
  208. package/ios/ExpoGaodeMapNavigation.podspec +2 -1
  209. package/ios/ExpoGaodeMapNavigationModule.swift +7 -5
  210. package/ios/managers/IndependentRouteManager.swift +90 -26
  211. package/ios/map/ExpoGaodeMapModule.swift +72 -21
  212. package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
  213. package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
  214. package/ios/map/ExpoGaodeMapView.swift +23 -5
  215. package/ios/map/GaodeMapPrivacyManager.swift +26 -18
  216. package/ios/map/cpp/GeometryEngine.cpp +112 -0
  217. package/ios/map/cpp/GeometryEngine.hpp +21 -0
  218. package/ios/map/modules/LocationManager.swift +37 -5
  219. package/ios/map/overlays/MarkerView.swift +11 -11
  220. package/ios/map/overlays/MarkerViewModule.swift +4 -4
  221. package/ios/map/overlays/PolylineView.swift +6 -12
  222. package/ios/map/utils/ClusterNative.h +8 -0
  223. package/ios/map/utils/ClusterNative.mm +27 -0
  224. package/ios/map/utils/PermissionManager.swift +115 -6
  225. package/ios/routes/drive/DriveTruckRouteCalculator.swift +165 -77
  226. package/ios/routes/walkride/WalkRideRouteCalculator.swift +127 -1
  227. package/ios/services/IndependentRouteService.swift +198 -39
  228. package/ios/services/NavigationLiveActivityAttributes.swift +48 -0
  229. package/ios/services/NavigationLiveActivityManager.swift +359 -0
  230. package/package.json +22 -7
  231. package/plugin/build/withGaodeMap.d.ts +8 -0
  232. package/plugin/build/withGaodeMap.js +60 -4
  233. package/scripts/check-expo-modules.js +68 -0
  234. package/shared/cpp/GeometryEngine.cpp +112 -0
  235. package/shared/cpp/GeometryEngine.hpp +21 -0
  236. package/widget-template/README.md +46 -0
  237. package/widget-template/ios/NavigationLiveActivityWidget.swift +367 -0
  238. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
@@ -2,7 +2,17 @@ package expo.modules.gaodemap.navigation
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.graphics.Canvas
8
+ import android.graphics.Color
9
+ import android.graphics.Paint
10
+ import android.graphics.RectF
11
+ import android.graphics.Typeface
12
+ import android.net.Uri
5
13
  import android.os.Bundle
14
+ import android.os.Handler
15
+ import android.os.Looper
6
16
  import android.util.Log
7
17
  import android.view.View
8
18
  import android.view.ViewGroup
@@ -11,18 +21,60 @@ import com.amap.api.navi.AMapNaviListener
11
21
  import com.amap.api.navi.AMapNaviView
12
22
  import com.amap.api.navi.AMapNaviViewListener
13
23
  import com.amap.api.navi.AMapNaviViewOptions
24
+ import com.amap.api.maps.model.BitmapDescriptorFactory
25
+ import com.amap.api.maps.model.LatLng
26
+ import com.amap.api.maps.model.Marker
27
+ import com.amap.api.maps.model.MarkerOptions
14
28
  import com.amap.api.navi.enums.MapStyle
15
29
  import com.amap.api.navi.enums.NaviType
16
30
  import com.amap.api.navi.model.*
17
31
  import expo.modules.gaodemap.map.modules.SDKInitializer
32
+ import expo.modules.gaodemap.navigation.managers.IndependentRouteManager
33
+ import expo.modules.gaodemap.navigation.services.NavigationForegroundService
34
+ import expo.modules.gaodemap.navigation.services.NavigationNotificationSnapshot
18
35
  import expo.modules.kotlin.AppContext
19
36
  import expo.modules.kotlin.viewevent.EventDispatcher
20
37
  import expo.modules.kotlin.views.ExpoView
21
- import java.util.IdentityHashMap
38
+ import java.io.ByteArrayOutputStream
39
+ import java.io.File
40
+ import java.io.FileOutputStream
41
+ import java.security.MessageDigest
42
+ import java.util.Collections
43
+ import java.util.WeakHashMap
44
+ import java.net.URL
45
+ import kotlin.math.roundToInt
46
+
47
+ private data class NaviCustomWaypointMarkerModel(
48
+ val latitude: Double,
49
+ val longitude: Double,
50
+ val title: String,
51
+ val arrived: Boolean = false
52
+ )
22
53
 
23
54
  @SuppressLint("ViewConstructor")
24
55
  @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
25
56
  class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
57
+ companion object {
58
+ private val activeViews =
59
+ Collections.newSetFromMap(WeakHashMap<ExpoGaodeMapNaviView, Boolean>())
60
+
61
+ private fun snapshotActiveViews(): List<ExpoGaodeMapNaviView> =
62
+ synchronized(activeViews) { activeViews.toList() }
63
+
64
+ fun resumeActiveViews() {
65
+ snapshotActiveViews().forEach { it.onHostActivityForeground() }
66
+ }
67
+
68
+ fun pauseActiveViews() {
69
+ snapshotActiveViews().forEach { it.onHostActivityBackground() }
70
+ }
71
+
72
+ fun destroyActiveViews() {
73
+ snapshotActiveViews().forEach { it.onDestroy() }
74
+ }
75
+ }
76
+
77
+ private val independentRouteManager = IndependentRouteManager.shared
26
78
 
27
79
  // 事件派发器
28
80
  private val onNavigationReady by EventDispatcher()
@@ -38,6 +90,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
38
90
  private val onGpsStatusChanged by EventDispatcher()
39
91
  private val onNavigationInfoUpdate by EventDispatcher()
40
92
  private val onGpsSignalWeak by EventDispatcher()
93
+ private val onNavigationVisualStateUpdate by EventDispatcher()
94
+ private val onLaneInfoUpdate by EventDispatcher()
95
+ private val onTrafficStatusesUpdate by EventDispatcher()
41
96
 
42
97
  // Props - 使用 internal 避免自动生成 setter
43
98
  internal var showCamera: Boolean = true
@@ -50,16 +105,41 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
50
105
  internal var showUIElements: Boolean = true
51
106
  internal var androidTrafficBarEnabled: Boolean = true
52
107
  internal var isRouteListButtonShow: Boolean = true
53
- internal var isTrafficLayerEnabled: Boolean = true
108
+ internal var isTrafficButtonVisible: Boolean = true
54
109
  internal var autoChangeZoom : Boolean = true
55
110
  internal var autoLockCar: Boolean = true
56
- internal var isTrafficLine: Boolean = true
111
+ internal var isTrafficLineEnabled: Boolean = true
57
112
  internal var isRealCrossDisplayShow : Boolean = true
58
113
  internal var isNaviArrowVisible : Boolean = true
59
- internal var isAfterRouteAutoGray: Boolean = false
114
+ internal var isLaneInfoVisible: Boolean = true
115
+ internal var isModeCrossDisplayVisible: Boolean = true
116
+ internal var isEyrieCrossDisplayVisible: Boolean = true
117
+ internal var isSecondActionVisible: Boolean = true
118
+ internal var isBackupOverlayVisible: Boolean = true
119
+ internal var isAfterRouteAutoGray: Boolean = true
60
120
  internal var isVectorLineShow: Boolean = true
61
121
  internal var isNaviTravelView : Boolean = false
62
122
  internal var isCompassEnabled: Boolean = true
123
+ internal var isNaviStatusBarEnabled: Boolean = false
124
+ internal var lockZoomLevel: Int = 18
125
+ internal var lockTilt: Int = 35
126
+ internal var isEagleMapVisible: Boolean = false
127
+ internal var pointToCenterX: Double = 0.0
128
+ internal var pointToCenterY: Double = 0.0
129
+ internal var hideNativeTopInfoLayout: Boolean = false
130
+ internal var androidBackgroundNavigationNotificationEnabled: Boolean = false
131
+ internal var naviModeValue: Int = AMapNaviView.CAR_UP_MODE
132
+ internal var mapViewModeTypeValue: Int = 0
133
+ internal var carImageUri: String? = null
134
+ internal var carImageWidthDp: Double? = null
135
+ internal var carImageHeightDp: Double? = null
136
+ internal var fourCornersImageUri: String? = null
137
+ internal var startPointImageUri: String? = null
138
+ internal var wayPointImageUri: String? = null
139
+ internal var endPointImageUri: String? = null
140
+ private var customWaypointMarkers: List<NaviCustomWaypointMarkerModel> = emptyList()
141
+ private val renderedCustomWaypointMarkers = mutableListOf<Marker>()
142
+ private var activeIndependentRouteId: Int? = null
63
143
  private val naviView: AMapNaviView by lazy(LazyThreadSafetyMode.NONE) {
64
144
  AMapNaviView(context)
65
145
  }
@@ -68,15 +148,803 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
68
148
  private var endCoordinate: NaviLatLng? = null
69
149
 
70
150
  private var lastAppliedTopPaddingPx: Int? = null
151
+ private var isDestroyed = false
152
+ private var isCrossVisible = false
153
+ private var isModeCrossVisible = false
154
+ private var isLaneInfoCurrentlyVisible = false
155
+ private var currentRouteTotalLength: Int? = null
156
+ private var sourceCarBitmap: Bitmap? = null
157
+ private var customCarBitmap: Bitmap? = null
158
+ private var customFourCornersBitmap: Bitmap? = null
159
+ private var customStartPointBitmap: Bitmap? = null
160
+ private var customWayPointBitmap: Bitmap? = null
161
+ private var customEndPointBitmap: Bitmap? = null
162
+ private var routeMarkerShowStartEndVia: Boolean = true
163
+ private var routeMarkerShowFootFerry: Boolean = true
164
+ private var routeMarkerShowForbidden: Boolean = true
165
+ private var routeMarkerShowRouteStartIcon: Boolean = true
166
+ private var routeMarkerShowRouteEndIcon: Boolean = true
167
+ private var cachedTurnIconImageUri: String? = null
168
+ private var cachedTurnIconContentHash: String? = null
169
+ private var hasLoggedMissingTurnIconBitmapApi = false
170
+ private var hasLoggedMissingNaviStatusBarApi = false
171
+ private var hasLoggedMissingNaviModeApi = false
172
+ private var isHostActivityInBackground: Boolean = false
173
+ private var isNavigationRunning: Boolean = false
174
+ private var latestNavigationNotificationSnapshot: NavigationNotificationSnapshot? = null
175
+
176
+ private fun registerActiveView() {
177
+ synchronized(activeViews) {
178
+ activeViews.add(this)
179
+ }
180
+ }
181
+
182
+ private fun unregisterActiveView() {
183
+ synchronized(activeViews) {
184
+ activeViews.remove(this)
185
+ }
186
+ }
71
187
 
72
- private var topInsetPx: Int = 0
73
- private var overlayHooked: Boolean = false
74
- private val overlayStates = IdentityHashMap<View, OverlayState>()
188
+ private fun buildNavigationNotificationSnapshot(naviInfo: NaviInfo?): NavigationNotificationSnapshot {
189
+ if (naviInfo == null) {
190
+ return latestNavigationNotificationSnapshot ?: NavigationNotificationSnapshot(
191
+ pathRetainDistance = currentRouteTotalLength,
192
+ routeTotalDistance = currentRouteTotalLength,
193
+ turnIconImageUri = cachedTurnIconImageUri
194
+ )
195
+ }
75
196
 
76
- private data class OverlayState(
77
- val translationY: Float,
78
- val paddingTop: Int
79
- )
197
+ return NavigationNotificationSnapshot(
198
+ currentRoadName = naviInfo.currentRoadName,
199
+ nextRoadName = naviInfo.nextRoadName,
200
+ pathRetainDistance = naviInfo.pathRetainDistance,
201
+ routeTotalDistance = currentRouteTotalLength,
202
+ pathRetainTime = naviInfo.pathRetainTime,
203
+ curStepRetainDistance = naviInfo.curStepRetainDistance,
204
+ iconType = naviInfo.iconType,
205
+ turnIconImageUri = cachedTurnIconImageUri
206
+ )
207
+ }
208
+
209
+ private fun shouldRunBackgroundNavigationNotification(): Boolean {
210
+ return androidBackgroundNavigationNotificationEnabled &&
211
+ isNavigationRunning &&
212
+ isHostActivityInBackground
213
+ }
214
+
215
+ private fun syncNavigationForegroundService(reason: String) {
216
+ val shouldRun = shouldRunBackgroundNavigationNotification()
217
+ Log.d(
218
+ "ExpoGaodeMapNaviView",
219
+ "syncNavigationForegroundService reason=$reason, shouldRun=$shouldRun, " +
220
+ "propEnabled=$androidBackgroundNavigationNotificationEnabled, " +
221
+ "isNavigationRunning=$isNavigationRunning, isHostActivityInBackground=$isHostActivityInBackground"
222
+ )
223
+ if (shouldRunBackgroundNavigationNotification()) {
224
+ val snapshot = latestNavigationNotificationSnapshot ?: buildNavigationNotificationSnapshot(null)
225
+ Log.d(
226
+ "ExpoGaodeMapNaviView",
227
+ "startOrUpdate notification snapshot: stepDistance=${snapshot.curStepRetainDistance}, " +
228
+ "remainDistance=${snapshot.pathRetainDistance}, routeTotal=${snapshot.routeTotalDistance}, " +
229
+ "remainTime=${snapshot.pathRetainTime}, " +
230
+ "nextRoad=${snapshot.nextRoadName}, turnIconUri=${snapshot.turnIconImageUri}"
231
+ )
232
+ NavigationForegroundService.startOrUpdate(context, snapshot)
233
+ Log.d("ExpoGaodeMapNaviView", "Navigation foreground notification enabled: $reason")
234
+ } else {
235
+ NavigationForegroundService.stop(context)
236
+ Log.d("ExpoGaodeMapNaviView", "Navigation foreground notification disabled: $reason")
237
+ }
238
+ }
239
+
240
+ private fun updateNavigationNotification(naviInfo: NaviInfo) {
241
+ latestNavigationNotificationSnapshot = buildNavigationNotificationSnapshot(naviInfo)
242
+ Log.d(
243
+ "ExpoGaodeMapNaviView",
244
+ "updateNavigationNotification: stepDistance=${naviInfo.curStepRetainDistance}, " +
245
+ "remainDistance=${naviInfo.pathRetainDistance}, routeTotal=${latestNavigationNotificationSnapshot?.routeTotalDistance}, " +
246
+ "remainTime=${naviInfo.pathRetainTime}, " +
247
+ "currentRoad=${naviInfo.currentRoadName}, nextRoad=${naviInfo.nextRoadName}, " +
248
+ "turnIconUri=${latestNavigationNotificationSnapshot?.turnIconImageUri}"
249
+ )
250
+ if (shouldRunBackgroundNavigationNotification()) {
251
+ NavigationForegroundService.startOrUpdate(context, latestNavigationNotificationSnapshot)
252
+ }
253
+ }
254
+
255
+ fun onHostActivityForeground() {
256
+ Log.d("ExpoGaodeMapNaviView", "onHostActivityForeground")
257
+ isHostActivityInBackground = false
258
+ onResume()
259
+ syncNavigationForegroundService("host_foreground")
260
+ }
261
+
262
+ fun onHostActivityBackground() {
263
+ Log.d("ExpoGaodeMapNaviView", "onHostActivityBackground")
264
+ isHostActivityInBackground = true
265
+ onPause()
266
+ syncNavigationForegroundService("host_background")
267
+ }
268
+
269
+ private fun applyNaviStatusBarEnabledCompat(
270
+ options: AMapNaviViewOptions,
271
+ enabled: Boolean
272
+ ) {
273
+ try {
274
+ val method = options.javaClass.getMethod(
275
+ "setNaviStatusBarEnabled",
276
+ java.lang.Boolean.TYPE
277
+ )
278
+ method.invoke(options, enabled)
279
+ } catch (_: NoSuchMethodException) {
280
+ if (!hasLoggedMissingNaviStatusBarApi) {
281
+ hasLoggedMissingNaviStatusBarApi = true
282
+ Log.w(
283
+ "ExpoGaodeMapNaviView",
284
+ "AMapNaviViewOptions#setNaviStatusBarEnabled is unavailable in the current AMap SDK; skip applying naviStatusBarEnabled"
285
+ )
286
+ }
287
+ } catch (error: Throwable) {
288
+ Log.w(
289
+ "ExpoGaodeMapNaviView",
290
+ "Failed to apply naviStatusBarEnabled compatibly",
291
+ error
292
+ )
293
+ }
294
+ }
295
+
296
+ private fun applyNaviModeCompat(
297
+ options: AMapNaviViewOptions,
298
+ mode: Int
299
+ ) {
300
+ try {
301
+ val method = options.javaClass.getMethod(
302
+ "setNaviMode",
303
+ Integer.TYPE
304
+ )
305
+ method.invoke(options, mode)
306
+ } catch (_: NoSuchMethodException) {
307
+ if (!hasLoggedMissingNaviModeApi) {
308
+ hasLoggedMissingNaviModeApi = true
309
+ Log.w(
310
+ "ExpoGaodeMapNaviView",
311
+ "AMapNaviViewOptions#setNaviMode is unavailable in the current AMap SDK; skip applying naviMode on options"
312
+ )
313
+ }
314
+ } catch (error: Throwable) {
315
+ Log.w(
316
+ "ExpoGaodeMapNaviView",
317
+ "Failed to apply naviMode compatibly",
318
+ error
319
+ )
320
+ }
321
+ }
322
+
323
+ private fun applyAutoNaviViewNightModeCompat(
324
+ options: AMapNaviViewOptions,
325
+ enabled: Boolean
326
+ ) {
327
+ try {
328
+ val method = options.javaClass.getMethod(
329
+ "setAutoNaviViewNightMode",
330
+ java.lang.Boolean.TYPE
331
+ )
332
+ method.invoke(options, enabled)
333
+ } catch (_: NoSuchMethodException) {
334
+ Log.w(
335
+ "ExpoGaodeMapNaviView",
336
+ "AMapNaviViewOptions#setAutoNaviViewNightMode is unavailable in the current AMap SDK; skip applying auto night mode"
337
+ )
338
+ } catch (error: Throwable) {
339
+ Log.w(
340
+ "ExpoGaodeMapNaviView",
341
+ "Failed to apply auto navi night mode compatibly",
342
+ error
343
+ )
344
+ }
345
+ }
346
+
347
+ private fun applyNaviNightCompat(
348
+ options: AMapNaviViewOptions,
349
+ enabled: Boolean
350
+ ) {
351
+ try {
352
+ val method = options.javaClass.getMethod(
353
+ "setNaviNight",
354
+ java.lang.Boolean.TYPE
355
+ )
356
+ method.invoke(options, enabled)
357
+ return
358
+ } catch (_: NoSuchMethodException) {
359
+ Log.w(
360
+ "ExpoGaodeMapNaviView",
361
+ "AMapNaviViewOptions#setNaviNight is unavailable in the current AMap SDK; fallback to setMapStyle"
362
+ )
363
+ } catch (error: Throwable) {
364
+ Log.w(
365
+ "ExpoGaodeMapNaviView",
366
+ "Failed to apply navi night compatibly, fallback to setMapStyle",
367
+ error
368
+ )
369
+ }
370
+
371
+ if (enabled) {
372
+ options.setMapStyle(MapStyle.NIGHT, null)
373
+ } else {
374
+ options.setMapStyle(MapStyle.DAY, null)
375
+ }
376
+ }
377
+
378
+ private fun applyMapViewModeTypeCompat(
379
+ options: AMapNaviViewOptions,
380
+ mode: Int
381
+ ) {
382
+ when (mode) {
383
+ 0 -> {
384
+ applyAutoNaviViewNightModeCompat(options, false)
385
+ applyNaviNightCompat(options, false)
386
+ }
387
+ 1 -> {
388
+ applyAutoNaviViewNightModeCompat(options, false)
389
+ applyNaviNightCompat(options, true)
390
+ }
391
+ 2 -> {
392
+ applyAutoNaviViewNightModeCompat(options, true)
393
+ }
394
+ 3 -> {
395
+ // Android custom map style requires style path API that is not exposed yet.
396
+ applyAutoNaviViewNightModeCompat(options, false)
397
+ applyNaviNightCompat(options, false)
398
+ Log.w(
399
+ "ExpoGaodeMapNaviView",
400
+ "mapViewModeType=3 (custom) requires custom map style path support on Android; fallback to day mode"
401
+ )
402
+ }
403
+ else -> {
404
+ applyAutoNaviViewNightModeCompat(options, false)
405
+ applyNaviNightCompat(options, false)
406
+ Log.w(
407
+ "ExpoGaodeMapNaviView",
408
+ "Unknown mapViewModeType=$mode; fallback to day mode"
409
+ )
410
+ }
411
+ }
412
+ }
413
+
414
+ private fun createInitialViewOptions(): AMapNaviViewOptions {
415
+ return AMapNaviViewOptions().also { options ->
416
+ applyAllViewOptions(options)
417
+ }
418
+ }
419
+
420
+ private fun applyAllViewOptions(options: AMapNaviViewOptions) {
421
+ options.isLayoutVisible = showUIElements
422
+ options.isSettingMenuEnabled = true
423
+ options.isCompassEnabled = isCompassEnabled
424
+ options.isTrafficBarEnabled = androidTrafficBarEnabled
425
+ options.isRouteListButtonShow = isRouteListButtonShow
426
+ applyNaviStatusBarEnabledCompat(options, isNaviStatusBarEnabled)
427
+
428
+ options.isTrafficLayerEnabled = isTrafficButtonVisible
429
+ options.isTrafficLine = isTrafficLineEnabled
430
+
431
+ options.isRealCrossDisplayShow = isRealCrossDisplayShow
432
+ options.setModeCrossDisplayShow(isModeCrossDisplayVisible)
433
+ options.isLaneInfoShow = isLaneInfoVisible
434
+ options.isEyrieCrossDisplay = isEyrieCrossDisplayVisible
435
+
436
+ options.isCameraBubbleShow = showCamera
437
+ options.isShowCameraDistance = true
438
+ options.isWidgetOverSpeedPulseEffective = true
439
+
440
+ options.isAutoDrawRoute = true
441
+ options.isNaviArrowVisible = isNaviArrowVisible
442
+ options.isSecondActionVisible = isSecondActionVisible
443
+ options.isDrawBackUpOverlay = isBackupOverlayVisible
444
+ applyLeaderLineSetting(options, isVectorLineShow)
445
+
446
+ options.isAutoLockCar = autoLockCar
447
+ options.lockMapDelayed = 5000L
448
+ options.isAutoDisplayOverview = false
449
+ options.isAutoChangeZoom = autoChangeZoom
450
+ options.zoom = lockZoomLevel.coerceIn(14, 18)
451
+ options.tilt = lockTilt.coerceIn(0, 60)
452
+ applyNaviModeCompat(options, naviModeValue)
453
+ if (pointToCenterX > 0.0 && pointToCenterY > 0.0) {
454
+ options.setPointToCenter(pointToCenterX, pointToCenterY)
455
+ }
456
+
457
+ options.isAfterRouteAutoGray = isAfterRouteAutoGray
458
+ options.isSensorEnable = true
459
+ applyMapViewModeTypeCompat(options, mapViewModeTypeValue)
460
+ options.isEagleMapVisible = isEagleMapVisible
461
+ applyCustomAnnotationBitmaps(options)
462
+ }
463
+
464
+ private fun applyBitmapOptionCompat(
465
+ options: AMapNaviViewOptions,
466
+ methodName: String,
467
+ bitmap: Bitmap?
468
+ ) {
469
+ try {
470
+ val method = options.javaClass.getMethod(methodName, Bitmap::class.java)
471
+ method.invoke(options, bitmap)
472
+ } catch (_: NoSuchMethodException) {
473
+ Log.w(
474
+ "ExpoGaodeMapNaviView",
475
+ "AMapNaviViewOptions#$methodName is unavailable in the current AMap SDK; skip applying custom bitmap"
476
+ )
477
+ } catch (error: Throwable) {
478
+ Log.w(
479
+ "ExpoGaodeMapNaviView",
480
+ "Failed to apply $methodName compatibly",
481
+ error
482
+ )
483
+ }
484
+ }
485
+
486
+ private fun applyCustomAnnotationBitmaps(options: AMapNaviViewOptions) {
487
+ applyBitmapOptionCompat(options, "setCarBitmap", customCarBitmap)
488
+ applyBitmapOptionCompat(options, "setFourCornersBitmap", customFourCornersBitmap)
489
+ applyBitmapOptionCompat(options, "setStartPointBitmap", customStartPointBitmap)
490
+ applyBitmapOptionCompat(options, "setWayPointBitmap", customWayPointBitmap)
491
+ applyBitmapOptionCompat(options, "setEndPointBitmap", customEndPointBitmap)
492
+ }
493
+
494
+ private fun refreshViewOptionsFromState(reason: String) {
495
+ if (isDestroyed) {
496
+ return
497
+ }
498
+ naviView.viewOptions = createInitialViewOptions()
499
+ refreshNaviUILayout(reason)
500
+ }
501
+
502
+ private fun loadBitmapFromSource(imagePath: String): Bitmap? {
503
+ return try {
504
+ when {
505
+ imagePath.startsWith("http://") || imagePath.startsWith("https://") -> {
506
+ URL(imagePath).openStream().use { BitmapFactory.decodeStream(it) }
507
+ }
508
+ imagePath.startsWith("file://") -> {
509
+ BitmapFactory.decodeFile(imagePath.substring(7))
510
+ }
511
+ else -> {
512
+ val fileName = imagePath.substringBeforeLast('.')
513
+ val resId = context.resources.getIdentifier(
514
+ fileName,
515
+ "drawable",
516
+ context.packageName
517
+ )
518
+ if (resId != 0) {
519
+ BitmapFactory.decodeResource(context.resources, resId)
520
+ } else {
521
+ BitmapFactory.decodeFile(imagePath)
522
+ }
523
+ }
524
+ }
525
+ } catch (_: Throwable) {
526
+ null
527
+ }
528
+ }
529
+
530
+ private fun resizeCarBitmapIfNeeded(source: Bitmap?): Bitmap? {
531
+ val rawBitmap = source ?: return null
532
+ val widthDp = carImageWidthDp?.takeIf { it > 0.0 }
533
+ val heightDp = carImageHeightDp?.takeIf { it > 0.0 }
534
+ if (widthDp == null || heightDp == null) {
535
+ return rawBitmap
536
+ }
537
+
538
+ val density = context.resources.displayMetrics.density
539
+ val widthPx = (widthDp * density).roundToInt().coerceAtLeast(1)
540
+ val heightPx = (heightDp * density).roundToInt().coerceAtLeast(1)
541
+
542
+ if (rawBitmap.width == widthPx && rawBitmap.height == heightPx) {
543
+ return rawBitmap
544
+ }
545
+
546
+ return Bitmap.createScaledBitmap(rawBitmap, widthPx, heightPx, true)
547
+ }
548
+
549
+ private fun updateCustomAnnotationBitmap(
550
+ uri: String?,
551
+ getCurrentUri: () -> String?,
552
+ setCurrentUri: (String?) -> Unit,
553
+ setBitmap: (Bitmap?) -> Unit,
554
+ reason: String
555
+ ) {
556
+ val normalizedUri = uri?.takeIf { it.isNotBlank() }
557
+ setCurrentUri(normalizedUri)
558
+
559
+ if (normalizedUri == null) {
560
+ setBitmap(null)
561
+ refreshViewOptionsFromState("clear-$reason")
562
+ return
563
+ }
564
+
565
+ Thread {
566
+ val bitmap = loadBitmapFromSource(normalizedUri)
567
+ Handler(Looper.getMainLooper()).post {
568
+ if (isDestroyed || getCurrentUri() != normalizedUri) {
569
+ return@post
570
+ }
571
+ setBitmap(bitmap)
572
+ refreshViewOptionsFromState("apply-$reason")
573
+ }
574
+ }.start()
575
+ }
576
+
577
+ private fun applyRouteMarkerVisibleFromState() {
578
+ naviView.setRouteMarkerVisible(
579
+ routeMarkerShowStartEndVia,
580
+ routeMarkerShowFootFerry,
581
+ routeMarkerShowForbidden,
582
+ routeMarkerShowRouteStartIcon,
583
+ routeMarkerShowRouteEndIcon
584
+ )
585
+ }
586
+
587
+ private fun commitViewOptions(mutator: (AMapNaviViewOptions) -> Unit) {
588
+ val options = naviView.viewOptions ?: AMapNaviViewOptions()
589
+ mutator(options)
590
+ naviView.viewOptions = options
591
+ refreshNaviUILayout("commitViewOptions")
592
+ }
593
+
594
+ private fun logCurrentNaviPathState(reason: String) {
595
+ val naviPath = aMapNavi?.naviPath
596
+ val waypointCount = naviPath?.wayPoint?.size ?: 0
597
+ val waypointIndexCount = naviPath?.wayPointIndex?.size ?: 0
598
+ Log.d(
599
+ "ExpoGaodeMapNaviView",
600
+ "pathState[$reason]: routeType=${naviPath?.routeType} length=${naviPath?.allLength} time=${naviPath?.allTime} labels=${naviPath?.labels} labelId=${naviPath?.labelId} waypointCount=$waypointCount waypointIndexCount=$waypointIndexCount start=${naviPath?.startPoint} end=${naviPath?.endPoint}"
601
+ )
602
+ }
603
+
604
+ private fun stabilizeIndependentRouteRendering(reason: String) {
605
+ val routeId = activeIndependentRouteId ?: return
606
+ val delays = longArrayOf(0L, 180L, 420L)
607
+ delays.forEach { delayMillis ->
608
+ naviView.postDelayed({
609
+ if (isDestroyed) {
610
+ return@postDelayed
611
+ }
612
+ try {
613
+ val selected = aMapNavi?.selectRouteId(routeId)
614
+ Log.d(
615
+ "ExpoGaodeMapNaviView",
616
+ "stabilizeIndependentRouteRendering[$reason/$delayMillis]: routeId=$routeId selected=$selected"
617
+ )
618
+ applyRouteMarkerVisibleFromState()
619
+ logCurrentNaviPathState("stabilize-$reason-$delayMillis")
620
+ } catch (error: Exception) {
621
+ Log.e(
622
+ "ExpoGaodeMapNaviView",
623
+ "Failed to stabilize independent route rendering: reason=$reason delay=$delayMillis routeId=$routeId",
624
+ error
625
+ )
626
+ }
627
+ }, delayMillis)
628
+ }
629
+ }
630
+
631
+ private fun refreshNaviUILayout(reason: String) {
632
+ if (isDestroyed) {
633
+ return
634
+ }
635
+
636
+ naviView.post {
637
+ if (isDestroyed) {
638
+ return@post
639
+ }
640
+
641
+ updateTopInsetPadding()
642
+ updateNativeTopInfoLayoutVisibility()
643
+
644
+ val needsAggressiveRefresh =
645
+ hideNativeTopInfoLayout ||
646
+ !showUIElements ||
647
+ androidStatusBarPaddingTopDp != null
648
+
649
+ if (needsAggressiveRefresh) {
650
+ naviView.requestLayout()
651
+ naviView.forceLayout()
652
+ naviView.invalidate()
653
+ naviView.postInvalidateOnAnimation()
654
+ requestLayout()
655
+ invalidate()
656
+ postInvalidateOnAnimation()
657
+ }
658
+
659
+ Log.d("ExpoGaodeMapNaviView", "refreshNaviUILayout: $reason")
660
+ }
661
+ }
662
+
663
+ private fun shouldHideNativeTopInfoView(view: View): Boolean {
664
+ val className = view.javaClass.name
665
+ return className.contains("BaseNaviInfoLayout") ||
666
+ className.contains("NaviInfoLayout_")
667
+ }
668
+
669
+ private fun updateNativeTopInfoLayoutVisibility() {
670
+ if (isDestroyed) {
671
+ return
672
+ }
673
+
674
+ if (!hideNativeTopInfoLayout) {
675
+ return
676
+ }
677
+
678
+ val queue = ArrayDeque<View>()
679
+ val targetVisibility = View.GONE
680
+ queue.add(naviView)
681
+
682
+ while (queue.isNotEmpty()) {
683
+ val current = queue.removeFirst()
684
+ if (current is ViewGroup) {
685
+ for (index in 0 until current.childCount) {
686
+ queue.add(current.getChildAt(index))
687
+ }
688
+ }
689
+
690
+ if (!shouldHideNativeTopInfoView(current)) {
691
+ continue
692
+ }
693
+
694
+ if (current.visibility != targetVisibility) {
695
+ current.visibility = targetVisibility
696
+ current.requestLayout()
697
+ current.invalidate()
698
+ }
699
+ }
700
+ }
701
+
702
+ private fun suppressNativeTopInfoLayoutFlash() {
703
+ if (!hideNativeTopInfoLayout || isDestroyed) {
704
+ return
705
+ }
706
+
707
+ val delays = longArrayOf(0L, 16L, 48L, 96L, 180L)
708
+ delays.forEach { delayMs ->
709
+ naviView.postDelayed({
710
+ if (!isDestroyed) {
711
+ updateNativeTopInfoLayoutVisibility()
712
+ }
713
+ }, delayMs)
714
+ }
715
+ }
716
+
717
+ private fun emitVisualStateUpdate() {
718
+ onNavigationVisualStateUpdate(
719
+ mapOf(
720
+ "isCrossVisible" to isCrossVisible,
721
+ "isModeCrossVisible" to isModeCrossVisible,
722
+ "isLaneInfoVisible" to isLaneInfoCurrentlyVisible
723
+ )
724
+ )
725
+ }
726
+
727
+ private fun extractLaneValues(raw: Any?): List<Int> {
728
+ return when (raw) {
729
+ is IntArray -> raw.map { it.toInt() }
730
+ is ByteArray -> raw.map { it.toInt() and 0xFF }
731
+ is ShortArray -> raw.map { it.toInt() }
732
+ is Array<*> -> raw.mapNotNull { (it as? Number)?.toInt() }
733
+ else -> emptyList()
734
+ }
735
+ }
736
+
737
+ private fun getLaneArrayValue(laneInfo: AMapLaneInfo, fieldName: String): List<Int> {
738
+ val reflectedValue = runCatching {
739
+ laneInfo.javaClass.getField(fieldName).get(laneInfo)
740
+ }.getOrElse {
741
+ runCatching {
742
+ val getterName = "get" + fieldName.replaceFirstChar { char ->
743
+ if (char.isLowerCase()) {
744
+ char.titlecase()
745
+ } else {
746
+ char.toString()
747
+ }
748
+ }
749
+ laneInfo.javaClass.getMethod(getterName).invoke(laneInfo)
750
+ }.getOrNull()
751
+ }
752
+
753
+ return extractLaneValues(reflectedValue)
754
+ }
755
+
756
+ private fun resolveLaneCount(laneInfo: AMapLaneInfo): Int {
757
+ val reflectedCount = runCatching {
758
+ laneInfo.javaClass.getField("laneCount").get(laneInfo) as? Number
759
+ }.getOrElse {
760
+ runCatching {
761
+ laneInfo.javaClass.getMethod("getLaneCount").invoke(laneInfo) as? Number
762
+ }.getOrNull()
763
+ }
764
+
765
+ return reflectedCount?.toInt()?.coerceAtLeast(0) ?: 0
766
+ }
767
+
768
+ private fun serializeLaneInfo(laneInfo: AMapLaneInfo?): Map<String, Any>? {
769
+ if (laneInfo == null) {
770
+ return null
771
+ }
772
+
773
+ val backgroundLane = getLaneArrayValue(laneInfo, "backgroundLane")
774
+ val frontLane = getLaneArrayValue(laneInfo, "frontLane")
775
+ val declaredCount = resolveLaneCount(laneInfo)
776
+ val sentinelIndex = backgroundLane.indexOf(255).takeIf { it >= 0 }
777
+
778
+ val resolvedCount = listOfNotNull(
779
+ sentinelIndex,
780
+ declaredCount.takeIf { it > 0 },
781
+ backgroundLane.size.takeIf { it > 0 },
782
+ frontLane.size.takeIf { it > 0 }
783
+ ).minOrNull() ?: 0
784
+
785
+ if (resolvedCount <= 0) {
786
+ return null
787
+ }
788
+
789
+ val normalizedBackground = (0 until resolvedCount).map { index ->
790
+ backgroundLane.getOrNull(index) ?: 255
791
+ }
792
+ val normalizedFront = (0 until resolvedCount).map { index ->
793
+ frontLane.getOrNull(index) ?: 255
794
+ }
795
+
796
+ return mapOf(
797
+ "laneCount" to resolvedCount,
798
+ "backgroundLane" to normalizedBackground,
799
+ "frontLane" to normalizedFront
800
+ )
801
+ }
802
+
803
+ private fun resolveNextTurnIconType(currentSegmentIndex: Int): Int? {
804
+ if (currentSegmentIndex < 0) {
805
+ return null
806
+ }
807
+
808
+ val steps = aMapNavi?.naviPath?.steps ?: return null
809
+ val nextSegmentIndex = currentSegmentIndex + 1
810
+ if (nextSegmentIndex !in steps.indices) {
811
+ return null
812
+ }
813
+
814
+ return try {
815
+ steps[nextSegmentIndex].iconType
816
+ } catch (_: Throwable) {
817
+ null
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Android 导航 SDK 仅透出当前转向图 bitmap,且不同版本方法可见性不完全一致,
823
+ * 这里走反射兼容,避免因为 SDK 小版本差异导致编译直接中断。
824
+ */
825
+ private fun extractTurnIconBitmap(naviInfo: NaviInfo): Bitmap? {
826
+ return try {
827
+ val method = naviInfo.javaClass.getMethod("getIconBitmap")
828
+ method.invoke(naviInfo) as? Bitmap
829
+ } catch (_: NoSuchMethodException) {
830
+ if (!hasLoggedMissingTurnIconBitmapApi) {
831
+ hasLoggedMissingTurnIconBitmapApi = true
832
+ Log.w(
833
+ "ExpoGaodeMapNaviView",
834
+ "NaviInfo#getIconBitmap is unavailable in the current AMap SDK; turnIconImage will be omitted on Android"
835
+ )
836
+ }
837
+ null
838
+ } catch (error: Throwable) {
839
+ Log.w("ExpoGaodeMapNaviView", "Failed to extract turn icon bitmap", error)
840
+ null
841
+ }
842
+ }
843
+
844
+ private fun bitmapToPngBytes(bitmap: Bitmap): ByteArray? {
845
+ return try {
846
+ val outputStream = ByteArrayOutputStream()
847
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
848
+ outputStream.toByteArray()
849
+ } catch (error: Throwable) {
850
+ Log.w("ExpoGaodeMapNaviView", "Failed to serialize turn icon bitmap", error)
851
+ null
852
+ }
853
+ }
854
+
855
+ private fun sha1Hex(bytes: ByteArray): String {
856
+ val digest = MessageDigest.getInstance("SHA-1").digest(bytes)
857
+ return digest.joinToString("") { "%02x".format(it) }
858
+ }
859
+
860
+ private fun cacheTurnIconBitmap(bytes: ByteArray, prefix: String, contentHash: String): String? {
861
+ return try {
862
+ val directory = File(context.cacheDir, "expo_gaode_map_navigation_icons")
863
+ if (!directory.exists()) {
864
+ directory.mkdirs()
865
+ }
866
+ val file = File(directory, "${prefix}_${contentHash}.png")
867
+ if (!file.exists()) {
868
+ FileOutputStream(file).use { stream ->
869
+ stream.write(bytes)
870
+ }
871
+ }
872
+ Uri.fromFile(file).toString()
873
+ } catch (error: Throwable) {
874
+ Log.w("ExpoGaodeMapNaviView", "Failed to cache turn icon bitmap", error)
875
+ null
876
+ }
877
+ }
878
+
879
+ private fun deleteCachedIconFile(uriString: String?) {
880
+ if (uriString.isNullOrBlank()) {
881
+ return
882
+ }
883
+ runCatching {
884
+ val path = Uri.parse(uriString).path ?: return
885
+ File(path).delete()
886
+ }
887
+ }
888
+
889
+ private fun updateCachedTurnIconImage(naviInfo: NaviInfo): String? {
890
+ val bitmap = extractTurnIconBitmap(naviInfo)
891
+ if (bitmap == null) {
892
+ deleteCachedIconFile(cachedTurnIconImageUri)
893
+ cachedTurnIconImageUri = null
894
+ cachedTurnIconContentHash = null
895
+ return null
896
+ }
897
+
898
+ val bytes = bitmapToPngBytes(bitmap) ?: return cachedTurnIconImageUri
899
+ val contentHash = sha1Hex(bytes)
900
+ if (contentHash == cachedTurnIconContentHash && !cachedTurnIconImageUri.isNullOrBlank()) {
901
+ return cachedTurnIconImageUri
902
+ }
903
+
904
+ val nextUri = cacheTurnIconBitmap(bytes, "turn_icon", contentHash) ?: return cachedTurnIconImageUri
905
+ val previousUri = cachedTurnIconImageUri
906
+ cachedTurnIconImageUri = nextUri
907
+ cachedTurnIconContentHash = contentHash
908
+ if (previousUri != nextUri) {
909
+ deleteCachedIconFile(previousUri)
910
+ }
911
+ return nextUri
912
+ }
913
+
914
+ private fun emitTrafficStatusesUpdate(retainDistance: Int? = null) {
915
+ val trafficStatuses = try {
916
+ aMapNavi?.getTrafficStatuses(0, 0)
917
+ } catch (_: Throwable) {
918
+ null
919
+ } ?: emptyList()
920
+
921
+ val totalLength = try {
922
+ aMapNavi?.naviPath?.allLength
923
+ } catch (_: Throwable) {
924
+ null
925
+ } ?: currentRouteTotalLength
926
+
927
+ currentRouteTotalLength = totalLength ?: currentRouteTotalLength
928
+
929
+ val payload = mutableMapOf<String, Any>(
930
+ "items" to trafficStatuses.map { status ->
931
+ mapOf(
932
+ "status" to status.status,
933
+ "length" to status.length
934
+ )
935
+ }
936
+ )
937
+
938
+ if ((totalLength ?: 0) > 0) {
939
+ payload["totalLength"] = totalLength as Int
940
+ }
941
+
942
+ if (retainDistance != null) {
943
+ payload["retainDistance"] = retainDistance
944
+ }
945
+
946
+ onTrafficStatusesUpdate(payload)
947
+ }
80
948
 
81
949
  // 导航监听器
82
950
  @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@@ -94,6 +962,10 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
94
962
 
95
963
  override fun onStartNavi(type: Int) {
96
964
  Log.d("ExpoGaodeMapNaviView", "导航开始: type=$type")
965
+ isNavigationRunning = true
966
+ syncNavigationForegroundService("on_start_navi")
967
+ logCurrentNaviPathState("onStartNavi")
968
+ stabilizeIndependentRouteRendering("onStartNavi")
97
969
  onNavigationStarted(mapOf(
98
970
  "type" to type,
99
971
  "isEmulator" to (type == 1)
@@ -101,7 +973,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
101
973
  }
102
974
 
103
975
  override fun onTrafficStatusUpdate() {
104
- // 交通状态更新
976
+ emitTrafficStatusesUpdate()
105
977
  }
106
978
 
107
979
  override fun onLocationChange(location: AMapNaviLocation?) {
@@ -135,10 +1007,18 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
135
1007
  }
136
1008
 
137
1009
  override fun onEndEmulatorNavi() {
1010
+ isNavigationRunning = false
1011
+ syncNavigationForegroundService("on_end_emulator_navi")
1012
+ resetCustomWaypointMarkerArrivalState()
1013
+ activeIndependentRouteId = null
138
1014
  onNavigationEnded(emptyMap())
139
1015
  }
140
1016
 
141
1017
  override fun onArriveDestination() {
1018
+ isNavigationRunning = false
1019
+ syncNavigationForegroundService("on_arrive_destination")
1020
+ resetCustomWaypointMarkerArrivalState()
1021
+ activeIndependentRouteId = null
142
1022
  onArriveDestination(emptyMap())
143
1023
  }
144
1024
 
@@ -156,6 +1036,14 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
156
1036
  // GPS 导航
157
1037
  aMapNavi?.startNavi(NaviType.GPS)
158
1038
  }
1039
+ Handler(Looper.getMainLooper()).post {
1040
+ try {
1041
+ applyRouteMarkerVisibleFromState()
1042
+ refreshCustomWaypointMarkers("calculate-route-success")
1043
+ } catch (e: Exception) {
1044
+ Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
1045
+ }
1046
+ }
159
1047
  }
160
1048
 
161
1049
  override fun onCalculateRouteSuccess(result: AMapCalcRouteResult?) {
@@ -172,9 +1060,19 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
172
1060
  // GPS 导航
173
1061
  aMapNavi?.startNavi(NaviType.GPS)
174
1062
  }
1063
+ Handler(Looper.getMainLooper()).post {
1064
+ try {
1065
+ applyRouteMarkerVisibleFromState()
1066
+ refreshCustomWaypointMarkers("calculate-route-success-result")
1067
+ } catch (e: Exception) {
1068
+ Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
1069
+ }
1070
+ }
175
1071
  }
176
1072
 
177
1073
  override fun onCalculateRouteFailure(errorCode: Int) {
1074
+ isNavigationRunning = false
1075
+ syncNavigationForegroundService("calculate_route_failure_code")
178
1076
  onRouteCalculated(mapOf(
179
1077
  "success" to false,
180
1078
  "errorCode" to errorCode
@@ -182,6 +1080,8 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
182
1080
  }
183
1081
 
184
1082
  override fun onCalculateRouteFailure(result: AMapCalcRouteResult?) {
1083
+ isNavigationRunning = false
1084
+ syncNavigationForegroundService("calculate_route_failure_result")
185
1085
  onRouteCalculated(mapOf(
186
1086
  "success" to false,
187
1087
  "errorInfo" to (result?.errorDescription ?: "Unknown error")
@@ -201,6 +1101,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
201
1101
  }
202
1102
 
203
1103
  override fun onArrivedWayPoint(wayPointIndex: Int) {
1104
+ markCustomWaypointArrived(wayPointIndex)
204
1105
  onWayPointArrived(mapOf(
205
1106
  "index" to wayPointIndex
206
1107
  ))
@@ -214,14 +1115,47 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
214
1115
 
215
1116
  override fun onNaviInfoUpdate(naviInfo: NaviInfo?) {
216
1117
  naviInfo?.let {
217
- onNavigationInfoUpdate(mapOf(
1118
+ val allLength = try {
1119
+ aMapNavi?.naviPath?.allLength
1120
+ } catch (_: Throwable) {
1121
+ null
1122
+ }
1123
+ val safeRetainDistance = it.pathRetainDistance.coerceAtLeast(0)
1124
+ currentRouteTotalLength = maxOf(
1125
+ currentRouteTotalLength ?: 0,
1126
+ allLength ?: 0,
1127
+ safeRetainDistance
1128
+ ).takeIf { total -> total > 0 } ?: currentRouteTotalLength
1129
+ val nextIconType = resolveNextTurnIconType(it.curStep)
1130
+ val turnIconImage = updateCachedTurnIconImage(it)
1131
+ val payload = mutableMapOf<String, Any>(
1132
+ "naviMode" to it.naviType,
218
1133
  "currentRoadName" to (it.currentRoadName ?: ""),
219
1134
  "nextRoadName" to (it.nextRoadName ?: ""),
220
1135
  "pathRetainDistance" to it.pathRetainDistance,
221
1136
  "pathRetainTime" to it.pathRetainTime,
222
1137
  "curStepRetainDistance" to it.curStepRetainDistance,
223
- "curStepRetainTime" to it.curStepRetainTime
224
- ))
1138
+ "curStepRetainTime" to it.curStepRetainTime,
1139
+ "currentSpeed" to it.currentSpeed,
1140
+ "iconType" to it.iconType,
1141
+ "iconDirection" to it.iconType,
1142
+ "currentSegmentIndex" to it.curStep,
1143
+ "currentLinkIndex" to it.curLink,
1144
+ "currentPointIndex" to it.curPoint,
1145
+ "routeRemainTrafficLightCount" to it.routeRemainLightCount,
1146
+ "driveDistance" to 0,
1147
+ "driveTime" to 0
1148
+ )
1149
+ if (nextIconType != null) {
1150
+ payload["nextIconType"] = nextIconType
1151
+ }
1152
+ if (!turnIconImage.isNullOrBlank()) {
1153
+ payload["turnIconImage"] = turnIconImage
1154
+ }
1155
+ onNavigationInfoUpdate(payload)
1156
+ updateNavigationNotification(it)
1157
+ emitTrafficStatusesUpdate(it.pathRetainDistance)
1158
+ refreshNaviUILayout("onNaviInfoUpdate")
225
1159
  }
226
1160
  }
227
1161
 
@@ -239,30 +1173,57 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
239
1173
 
240
1174
  override fun showCross(crossImg: AMapNaviCross?) {
241
1175
  // 显示路口放大图
1176
+ isCrossVisible = true
1177
+ emitVisualStateUpdate()
1178
+ suppressNativeTopInfoLayoutFlash()
1179
+ refreshNaviUILayout("showCross")
242
1180
  }
243
1181
 
244
1182
  override fun hideCross() {
245
1183
  // 隐藏路口放大图
1184
+ isCrossVisible = false
1185
+ emitVisualStateUpdate()
1186
+ suppressNativeTopInfoLayoutFlash()
1187
+ refreshNaviUILayout("hideCross")
246
1188
  }
247
1189
 
248
1190
  override fun showModeCross(modelCross: AMapModelCross?) {
249
1191
  // 显示路口3D模型
1192
+ isModeCrossVisible = true
1193
+ emitVisualStateUpdate()
1194
+ suppressNativeTopInfoLayoutFlash()
1195
+ refreshNaviUILayout("showModeCross")
250
1196
  }
251
1197
 
252
1198
  override fun hideModeCross() {
253
1199
  // 隐藏路口3D模型
1200
+ isModeCrossVisible = false
1201
+ emitVisualStateUpdate()
1202
+ suppressNativeTopInfoLayoutFlash()
1203
+ refreshNaviUILayout("hideModeCross")
254
1204
  }
255
1205
 
256
1206
  override fun showLaneInfo(laneInfo: AMapLaneInfo?) {
257
1207
  // 显示车道信息
1208
+ isLaneInfoCurrentlyVisible = true
1209
+ emitVisualStateUpdate()
1210
+ serializeLaneInfo(laneInfo)?.let { onLaneInfoUpdate(it) }
1211
+ refreshNaviUILayout("showLaneInfo")
258
1212
  }
259
1213
 
260
1214
  override fun showLaneInfo(laneInfos: Array<out AMapLaneInfo>?, laneBackgroundInfo: ByteArray?, laneRecommendedInfo: ByteArray?) {
261
1215
  // 显示车道信息(重载方法)
1216
+ isLaneInfoCurrentlyVisible = true
1217
+ emitVisualStateUpdate()
1218
+ serializeLaneInfo(laneInfos?.firstOrNull())?.let { onLaneInfoUpdate(it) }
1219
+ refreshNaviUILayout("showLaneInfoArray")
262
1220
  }
263
1221
 
264
1222
  override fun hideLaneInfo() {
265
1223
  // 隐藏车道信息
1224
+ isLaneInfoCurrentlyVisible = false
1225
+ emitVisualStateUpdate()
1226
+ refreshNaviUILayout("hideLaneInfo")
266
1227
  }
267
1228
 
268
1229
  override fun notifyParallelRoad(parallelRoadType: Int) {
@@ -306,12 +1267,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
306
1267
  SDKInitializer.restorePersistedState(appCtx)
307
1268
  if (!SDKInitializer.isPrivacyReady()) {
308
1269
  throw IllegalStateException(
309
- "隐私协议未完成确认,请先调用 setPrivacyConfig(或 setPrivacyShow/setPrivacyAgree)"
1270
+ "隐私协议未完成确认,请先调用 setPrivacyConfig"
310
1271
  )
311
1272
  }
312
1273
 
313
1274
  // 初始化导航视图
314
1275
  naviView.onCreate(Bundle())
1276
+ naviView.viewOptions = createInitialViewOptions()
315
1277
  naviView.layoutParams = android.widget.FrameLayout.LayoutParams(
316
1278
  android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
317
1279
  android.widget.FrameLayout.LayoutParams.MATCH_PARENT
@@ -323,8 +1285,6 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
323
1285
  naviView.clipChildren = false
324
1286
  naviView.clipToPadding = false
325
1287
 
326
- ensureOverlayInsetHook()
327
-
328
1288
  // 使用单例获取导航实例
329
1289
  aMapNavi = AMapNavi.getInstance(appCtx)
330
1290
  aMapNavi?.addAMapNaviListener(naviListener)
@@ -333,65 +1293,8 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
333
1293
  aMapNavi?.setUseInnerVoice(true, true) // 启用内置语音,并回调文字
334
1294
  //设置是否为骑步行视图
335
1295
  aMapNavi?.isNaviTravelView = isNaviTravelView
336
- // 设置导航视图选项 - 根据 AMapNaviViewOptions API
337
- val options = AMapNaviViewOptions()
338
-
339
- // === 基础界面控制 ===
340
- // 注意:isLayoutVisible 控制整个导航UI布局的显示
341
- // 设置为 true 将显示导航信息面板(包括距离、时间等)
342
- options.isLayoutVisible = showUIElements
343
- options.isSettingMenuEnabled = true // 显示设置菜单按钮
344
- options.isCompassEnabled = isCompassEnabled // 显示指南针
345
- options.isTrafficBarEnabled = androidTrafficBarEnabled // 显示路况条
346
- options.isRouteListButtonShow = isRouteListButtonShow // 显示路线全览按钮
347
-
348
- Log.d("ExpoGaodeMapNaviView", "导航UI配置: isLayoutVisible=true, 所有UI元素已启用")
349
-
350
- // === 地图图层 ===
351
- options.isTrafficLayerEnabled = isTrafficLayerEnabled // 显示交通路况图层
352
- options.isTrafficLine = isTrafficLine // 显示交通路况线
353
-
354
- // === 路口放大图和车道信息 ===
355
- options.isRealCrossDisplayShow = isRealCrossDisplayShow // 显示实景路口放大图
356
- options.setModeCrossDisplayShow(true) // 显示路口3D模型(使用方法而非属性)
357
- options.isLaneInfoShow = true // 显示车道信息
358
- options.isEyrieCrossDisplay = true // 显示鹰眼路口图
359
-
360
- // === 摄像头和电子眼 ===
361
- options.isCameraBubbleShow = showCamera // 显示摄像头气泡(已废弃但仍可用)
362
- options.isShowCameraDistance = true // 显示与摄像头的距离
363
- options.isWidgetOverSpeedPulseEffective = true // 超速脉冲效果
364
-
365
- // === 路线和导航箭头 ===
366
- options.isAutoDrawRoute = true // 自动绘制路线
367
- options.isNaviArrowVisible = isNaviArrowVisible // 显示导航箭头
368
- options.isSecondActionVisible = true // 显示辅助操作(如下个路口提示)
369
- options.isDrawBackUpOverlay = true // 绘制备用路线覆盖物
370
- if(isVectorLineShow)
371
- options.isLeaderLineEnabled
372
-
373
- // === 地图锁车和视角控制 ===
374
- options.isAutoLockCar = autoLockCar // 自动锁车
375
- options.lockMapDelayed = 5000L // 5秒后自动锁车(毫秒)
376
- options.isAutoDisplayOverview = false // 不自动显示全览
377
- options.isAutoChangeZoom = autoChangeZoom // 根据导航自动调整缩放级别
378
- options.zoom = 18 // 锁车时的缩放级别 (14-18)
379
- options.tilt = 35 // 锁车时的倾斜角度 (0-60)
380
-
381
- // === 已走路线处理 ===
382
- options.isAfterRouteAutoGray = isAfterRouteAutoGray // 走过的路线自动变灰
383
-
384
- // === 传感器和定位 ===
385
- options.isSensorEnable = true // 使用设备传感器
386
-
387
- // === 夜间模式(已废弃但保留兼容) ===
388
- // 建议使用 setMapStyle 方法设置地图样式
389
- options.isAutoNaviViewNightMode = false // 不自动切换夜间模式
390
-
391
- // === 鹰眼地图 ===
392
- options.isEagleMapVisible = false // 不显示鹰眼地图(小地图)
393
-
394
- naviView.viewOptions = options
1296
+ Log.d("ExpoGaodeMapNaviView", "导航UI配置初始化完成")
1297
+ registerActiveView()
395
1298
 
396
1299
  naviView.post { updateTopInsetPadding() }
397
1300
 
@@ -413,7 +1316,10 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
413
1316
  override fun onNextRoadClick() {}
414
1317
  override fun onScanViewButtonClick() {}
415
1318
  override fun onLockMap(isLock: Boolean) {}
416
- override fun onNaviViewLoaded() {}
1319
+ override fun onNaviViewLoaded() {
1320
+ updateNativeTopInfoLayoutVisibility()
1321
+ refreshNaviUILayout("onNaviViewLoaded")
1322
+ }
417
1323
  override fun onMapTypeChanged(mapType: Int) {}
418
1324
  })
419
1325
 
@@ -422,271 +1328,34 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
422
1328
  }
423
1329
  }
424
1330
 
425
- private fun getStatusBarHeightPx(): Int {
426
- return try {
427
- val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
428
- if (resourceId > 0) context.resources.getDimensionPixelSize(resourceId) else 0
429
- } catch (_: Exception) {
430
- 0
431
- }
432
- }
433
-
434
1331
  private fun dpToPx(dp: Double): Int {
435
1332
  val density = context.resources.displayMetrics.density
436
1333
  return (dp * density + 0.5).toInt()
437
1334
  }
438
1335
 
439
1336
  private fun updateTopInsetPadding() {
440
- val shouldApplyPadding = androidStatusBarPaddingTopDp != null
441
- val paddingTopPx = if (shouldApplyPadding) {
442
- androidStatusBarPaddingTopDp?.let { dpToPx(it) } ?: getStatusBarHeightPx()
443
- } else {
444
- //默认返回状态栏高度
445
- getStatusBarHeightPx()
446
- }
1337
+ val paddingTopPx = androidStatusBarPaddingTopDp?.let { dpToPx(it) } ?: 0
447
1338
 
448
1339
  if (lastAppliedTopPaddingPx == paddingTopPx) {
449
1340
  return
450
1341
  }
451
1342
 
452
1343
  lastAppliedTopPaddingPx = paddingTopPx
453
-
454
- topInsetPx = paddingTopPx
455
-
456
- naviView.setPadding(0, 0, 0, 0)
457
-
458
- applyTopInsetToOverlays(paddingTopPx)
459
- }
460
-
461
- private fun ensureOverlayInsetHook() {
462
- if (overlayHooked) {
463
- return
464
- }
465
-
466
- overlayHooked = true
467
- naviView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
468
- applyTopInsetToOverlays(topInsetPx)
469
- }
470
- }
471
-
472
- private fun applyTopInsetToOverlays(paddingTopPx: Int) {
473
- if (paddingTopPx <= 0) {
474
- if (overlayStates.isNotEmpty()) {
475
- val iterator = overlayStates.entries.iterator()
476
- while (iterator.hasNext()) {
477
- val entry = iterator.next()
478
- val view = entry.key
479
- val state = entry.value
480
- view.translationY = state.translationY
481
- view.setPadding(view.paddingLeft, state.paddingTop, view.paddingRight, view.paddingBottom)
482
- iterator.remove()
483
- }
484
- }
485
- return
486
- }
487
-
488
- val rawTargets = findTopOverlayTargets(naviView)
489
- val targets = filterTopLevelTargets(naviView, rawTargets)
490
- if (targets.isEmpty()) {
491
- val applied = applyTopPaddingToNaviUiLayer(paddingTopPx)
492
- if (!applied) {
493
- naviView.post { applyTopPaddingToNaviUiLayer(paddingTopPx) }
494
- }
495
- return
496
- }
497
-
498
- val targetSet = targets.toHashSet()
499
-
500
- val iterator = overlayStates.entries.iterator()
501
- while (iterator.hasNext()) {
502
- val entry = iterator.next()
503
- val view = entry.key
504
- if (!targetSet.contains(view)) {
505
- val state = entry.value
506
- view.translationY = state.translationY
507
- view.setPadding(view.paddingLeft, state.paddingTop, view.paddingRight, view.paddingBottom)
508
- iterator.remove()
509
- }
510
- }
511
-
512
- for (target in targets) {
513
- if (!overlayStates.containsKey(target)) {
514
- overlayStates[target] = OverlayState(
515
- translationY = target.translationY,
516
- paddingTop = target.paddingTop
517
- )
518
- }
519
- ensureNoClipChain(target)
520
- target.translationY = paddingTopPx.toFloat()
521
- }
522
- }
523
-
524
- private fun ensureNoClipChain(view: View) {
525
- var currentParent = view.parent
526
- while (currentParent is ViewGroup) {
527
- currentParent.clipChildren = false
528
- currentParent.clipToPadding = false
529
- if (currentParent === naviView) {
530
- break
531
- }
532
- currentParent = currentParent.parent
533
- }
534
- }
535
-
536
- private fun filterTopLevelTargets(root: ViewGroup, targets: List<View>): List<View> {
537
- if (targets.size <= 1) {
538
- return targets
539
- }
540
-
541
- val set = targets.toHashSet()
542
- val result = ArrayList<View>(targets.size)
543
- for (view in targets) {
544
- var parent = view.parent
545
- var hasAncestorInTargets = false
546
- while (parent is View) {
547
- if (parent === root) {
548
- break
549
- }
550
- if (set.contains(parent)) {
551
- hasAncestorInTargets = true
552
- break
553
- }
554
- parent = parent.parent
555
- }
556
- if (!hasAncestorInTargets) {
557
- result.add(view)
558
- }
559
- }
560
-
561
- return result
562
- }
563
-
564
- private fun findTopOverlayTargets(root: ViewGroup): List<View> {
565
- val result = ArrayList<View>()
566
- val parentHeight = root.height
567
- val parentWidth = root.width
568
- if (parentHeight <= 0 || parentWidth <= 0) {
569
- return result
570
- }
571
-
572
- val queue = ArrayDeque<View>()
573
- for (i in 0 until root.childCount) {
574
- queue.add(root.getChildAt(i))
575
- }
576
-
577
- while (queue.isNotEmpty()) {
578
- val view = queue.removeFirst()
579
- val group = view as? ViewGroup
580
- if (group != null) {
581
- for (i in 0 until group.childCount) {
582
- queue.add(group.getChildAt(i))
583
- }
584
- }
585
-
586
- if (!view.isShown) {
587
- continue
588
- }
589
-
590
- val name = view.javaClass.name
591
- if (name.contains("MapView", ignoreCase = true) ||
592
- name.contains("Texture", ignoreCase = true) ||
593
- name.contains("Surface", ignoreCase = true) ||
594
- name.contains("GLSurface", ignoreCase = true)
595
- ) {
596
- continue
597
- }
598
-
599
- if (view.height <= 0 || view.width <= 0) {
600
- continue
601
- }
602
-
603
- if (view.top > 1) {
604
- continue
605
- }
606
-
607
- if (view.height >= (parentHeight * 0.6f).toInt()) {
608
- continue
609
- }
610
-
611
- val wideEnough = view.width >= (parentWidth * 0.5f).toInt()
612
- val likelyUi = wideEnough && (view.isClickable || (view as? ViewGroup)?.childCount ?: 0 > 0)
613
- if (!likelyUi) {
614
- continue
615
- }
616
-
617
- result.add(view)
618
- }
619
-
620
- return result
621
- }
622
-
623
- private fun applyTopPaddingToNaviUiLayer(paddingTopPx: Int): Boolean {
624
- val uiRoot = findNaviUiRoot(naviView) ?: return false
625
-
626
- uiRoot.setPadding(uiRoot.paddingLeft, paddingTopPx, uiRoot.paddingRight, uiRoot.paddingBottom)
627
- uiRoot.clipToPadding = false
628
- return true
629
- }
630
-
631
- private fun findNaviUiRoot(root: ViewGroup): ViewGroup? {
632
- var best: ViewGroup? = null
633
- var bestScore = Int.MIN_VALUE
634
-
635
- val queue = ArrayDeque<View>()
636
- for (i in 0 until root.childCount) {
637
- queue.add(root.getChildAt(i))
638
- }
639
-
640
- while (queue.isNotEmpty()) {
641
- val view = queue.removeFirst()
642
- val group = view as? ViewGroup
643
- if (group != null) {
644
- val score = scoreAsUiRootCandidate(group)
645
- if (score > bestScore) {
646
- bestScore = score
647
- best = group
648
- }
649
- for (i in 0 until group.childCount) {
650
- queue.add(group.getChildAt(i))
651
- }
652
- }
1344
+ if (paddingTopPx > 0) {
1345
+ naviView.setPadding(0, paddingTopPx, 0, 0)
1346
+ } else {
1347
+ naviView.setPadding(0, 0, 0, 0)
653
1348
  }
654
-
655
- return if (bestScore > 0) best else null
1349
+ naviView.requestLayout()
656
1350
  }
657
1351
 
658
- private fun scoreAsUiRootCandidate(group: ViewGroup): Int {
659
- val name = group.javaClass.name
660
- if (name.contains("MapView", ignoreCase = true) ||
661
- name.contains("Texture", ignoreCase = true) ||
662
- name.contains("Surface", ignoreCase = true) ||
663
- name.contains("GLSurface", ignoreCase = true)
664
- ) {
665
- return Int.MIN_VALUE
666
- }
667
-
668
- var score = 0
669
-
670
- val lp = group.layoutParams
671
- if (lp != null) {
672
- if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT && lp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
673
- score += 4
674
- } else if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
675
- score += 2
676
- }
677
- }
678
-
679
- if (group.childCount >= 3) {
680
- score += 2
681
- } else if (group.childCount >= 1) {
682
- score += 1
683
- }
684
-
685
- if (name.contains("RelativeLayout", ignoreCase = true) || name.contains("FrameLayout", ignoreCase = true)) {
686
- score += 1
1352
+ private fun applyLeaderLineSetting(options: AMapNaviViewOptions, enabled: Boolean) {
1353
+ val color = if (enabled) {
1354
+ Color.argb(160, 48, 122, 246)
1355
+ } else {
1356
+ -1
687
1357
  }
688
-
689
- return score
1358
+ options.setLeaderLineEnabled(color)
690
1359
  }
691
1360
 
692
1361
 
@@ -698,50 +1367,50 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
698
1367
 
699
1368
  fun applyShowUIElements(visible: Boolean) {
700
1369
  showUIElements = visible
701
- val options = naviView.viewOptions
702
- options.isLayoutVisible = visible
703
- naviView.viewOptions = options
1370
+ commitViewOptions { options ->
1371
+ options.isLayoutVisible = visible
1372
+ }
704
1373
  }
705
1374
 
706
1375
  fun applyAndroidTrafficBarEnabled(enabled: Boolean) {
707
1376
  androidTrafficBarEnabled = enabled
708
- val options = naviView.viewOptions
709
- options.isTrafficBarEnabled = enabled
710
- naviView.viewOptions = options
1377
+ commitViewOptions { options ->
1378
+ options.isTrafficBarEnabled = enabled
1379
+ }
711
1380
  }
712
1381
 
713
1382
  fun applyShowTrafficButton(enabled: Boolean) {
714
- isTrafficLayerEnabled = enabled
715
- val options = naviView.viewOptions
716
- options.isTrafficLayerEnabled = enabled
717
- naviView.viewOptions = options
1383
+ isTrafficButtonVisible = enabled
1384
+ commitViewOptions { options ->
1385
+ options.isTrafficLayerEnabled = enabled
1386
+ }
718
1387
  }
719
1388
 
720
1389
  fun applyShowBrowseRouteButton(enabled: Boolean) {
721
1390
  isRouteListButtonShow = enabled
722
- val options = naviView.viewOptions
723
- options.isRouteListButtonShow = enabled
724
- naviView.viewOptions = options
1391
+ commitViewOptions { options ->
1392
+ options.isRouteListButtonShow = enabled
1393
+ }
725
1394
  }
726
1395
 
727
1396
  fun applyShowGreyAfterPass(enabled: Boolean){
728
1397
  isAfterRouteAutoGray = enabled
729
- val options = naviView.viewOptions
730
- options.isAfterRouteAutoGray = enabled
731
- naviView.viewOptions = options
1398
+ commitViewOptions { options ->
1399
+ options.isAfterRouteAutoGray = enabled
1400
+ }
732
1401
  }
733
1402
 
734
1403
  fun applyShowVectorline(enabled: Boolean){
735
1404
  isVectorLineShow = enabled
736
- val options = naviView.viewOptions
737
- if(enabled)
738
- options.isLeaderLineEnabled
739
- naviView.viewOptions = options
1405
+ commitViewOptions { options ->
1406
+ applyLeaderLineSetting(options, enabled)
1407
+ }
740
1408
  }
741
1409
 
742
1410
  fun startNavigation(startLat: Double, startLng: Double, endLat: Double, endLng: Double, promise: expo.modules.kotlin.Promise) {
743
1411
  Log.d("ExpoGaodeMapNaviView", "startNavigation: $startLat, $startLng, $endLat, $endLng, naviType: $naviType")
744
1412
  try {
1413
+ resetCustomWaypointMarkerArrivalState()
745
1414
  startCoordinate = NaviLatLng(startLat, startLng)
746
1415
  endCoordinate = NaviLatLng(endLat, endLng)
747
1416
 
@@ -778,9 +1447,47 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
778
1447
  }
779
1448
  }
780
1449
 
1450
+ fun startNavigationWithIndependentPath(
1451
+ token: Int,
1452
+ routeId: Int?,
1453
+ routeIndex: Int?,
1454
+ requestedNaviType: Int?,
1455
+ promise: expo.modules.kotlin.Promise
1456
+ ) {
1457
+ try {
1458
+ resetCustomWaypointMarkerArrivalState()
1459
+ val finalNaviType = requestedNaviType ?: naviType
1460
+ val result = independentRouteManager.start(context, token, finalNaviType, routeId, routeIndex)
1461
+ if (result.success) {
1462
+ activeIndependentRouteId = result.resolvedRouteId
1463
+ promise.resolve(
1464
+ mapOf(
1465
+ "success" to true,
1466
+ "message" to result.message,
1467
+ "token" to token,
1468
+ "naviType" to finalNaviType,
1469
+ "sdkNaviType" to result.sdkNaviType,
1470
+ "routeId" to result.resolvedRouteId,
1471
+ "pathCount" to result.pathCount,
1472
+ "mainPathIndex" to result.mainPathIndex
1473
+ )
1474
+ )
1475
+ } else {
1476
+ activeIndependentRouteId = null
1477
+ promise.reject("START_INDEPENDENT_NAVI_FAILED", result.message, null)
1478
+ }
1479
+ } catch (e: Exception) {
1480
+ activeIndependentRouteId = null
1481
+ promise.reject("START_INDEPENDENT_NAVI_ERROR", e.message, e)
1482
+ }
1483
+ }
1484
+
781
1485
  fun stopNavigation(promise: expo.modules.kotlin.Promise) {
782
1486
  try {
783
1487
  aMapNavi?.stopNavi()
1488
+ isNavigationRunning = false
1489
+ syncNavigationForegroundService("stop_navigation")
1490
+ resetCustomWaypointMarkerArrivalState()
784
1491
  promise.resolve(mapOf(
785
1492
  "success" to true,
786
1493
  "message" to "导航已停止"
@@ -793,10 +1500,15 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
793
1500
  // Prop setters - 使用不同的方法名避免与 var 属性的自动 setter 冲突
794
1501
  fun applyShowCamera(show: Boolean) {
795
1502
  showCamera = show
796
- // 摄像头显示设置
797
- val options = naviView.viewOptions
798
- options.isCameraBubbleShow = show
799
- naviView.viewOptions = options
1503
+ commitViewOptions { options ->
1504
+ options.isCameraBubbleShow = show
1505
+ }
1506
+ }
1507
+
1508
+ fun applyAndroidBackgroundNavigationNotificationEnabled(enabled: Boolean) {
1509
+ androidBackgroundNavigationNotificationEnabled = enabled
1510
+ Log.d("ExpoGaodeMapNaviView", "applyAndroidBackgroundNavigationNotificationEnabled=$enabled")
1511
+ syncNavigationForegroundService("prop_update")
800
1512
  }
801
1513
 
802
1514
  fun applyNaviType(type: Int) {
@@ -830,42 +1542,125 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
830
1542
 
831
1543
  fun applyAutoLockCar(enabled: Boolean) {
832
1544
  autoLockCar = enabled
833
- val options = naviView.viewOptions
834
- options.isAutoLockCar = enabled
835
- naviView.viewOptions = options
1545
+ commitViewOptions { options ->
1546
+ options.isAutoLockCar = enabled
1547
+ }
836
1548
  }
837
1549
 
838
1550
  fun applyAutoChangeZoom(enabled: Boolean) {
839
1551
  autoChangeZoom = enabled
840
- val options = naviView.viewOptions
841
- options.isAutoChangeZoom = enabled
842
- naviView.viewOptions = options
1552
+ commitViewOptions { options ->
1553
+ options.isAutoChangeZoom = enabled
1554
+ }
843
1555
  }
844
1556
 
845
1557
  fun applyTrafficLayerEnabled(enabled: Boolean) {
846
- isTrafficLine = enabled
847
- val options = naviView.viewOptions
848
- options.isTrafficLine = enabled
849
- naviView.viewOptions = options
1558
+ isTrafficLineEnabled = enabled
1559
+ commitViewOptions { options ->
1560
+ options.isTrafficLine = enabled
1561
+ }
850
1562
  }
851
1563
 
852
1564
  fun applyRealCrossDisplay(enabled: Boolean) {
853
1565
  isRealCrossDisplayShow = enabled
854
- val options = naviView.viewOptions
855
- options.isRealCrossDisplayShow = enabled
856
- naviView.viewOptions = options
1566
+ commitViewOptions { options ->
1567
+ options.isRealCrossDisplayShow = enabled
1568
+ }
1569
+ }
1570
+
1571
+ fun applyLaneInfoVisible(enabled: Boolean) {
1572
+ isLaneInfoVisible = enabled
1573
+ commitViewOptions { options ->
1574
+ options.isLaneInfoShow = enabled
1575
+ }
1576
+ }
1577
+
1578
+ fun applyModeCrossDisplay(enabled: Boolean) {
1579
+ isModeCrossDisplayVisible = enabled
1580
+ commitViewOptions { options ->
1581
+ options.setModeCrossDisplayShow(enabled)
1582
+ }
1583
+ }
1584
+
1585
+ fun applyEyrieCrossDisplay(enabled: Boolean) {
1586
+ isEyrieCrossDisplayVisible = enabled
1587
+ commitViewOptions { options ->
1588
+ options.isEyrieCrossDisplay = enabled
1589
+ }
1590
+ }
1591
+
1592
+ fun applySecondActionVisible(enabled: Boolean) {
1593
+ isSecondActionVisible = enabled
1594
+ commitViewOptions { options ->
1595
+ options.isSecondActionVisible = enabled
1596
+ }
1597
+ }
1598
+
1599
+ fun applyBackupOverlayVisible(enabled: Boolean) {
1600
+ isBackupOverlayVisible = enabled
1601
+ commitViewOptions { options ->
1602
+ options.isDrawBackUpOverlay = enabled
1603
+ }
857
1604
  }
858
1605
 
859
1606
  fun applyShowCompassEnabled(enabled: Boolean){
860
1607
  isCompassEnabled = enabled
861
- val options = naviView.viewOptions
862
- options.isCompassEnabled = enabled
863
- naviView.viewOptions = options
1608
+ commitViewOptions { options ->
1609
+ options.isCompassEnabled = enabled
1610
+ }
1611
+ }
1612
+
1613
+ fun applyNaviStatusBarEnabled(enabled: Boolean) {
1614
+ isNaviStatusBarEnabled = enabled
1615
+ commitViewOptions { options ->
1616
+ applyNaviStatusBarEnabledCompat(options, enabled)
1617
+ }
1618
+ }
1619
+
1620
+ fun applyLockZoom(level: Int) {
1621
+ lockZoomLevel = level.coerceIn(14, 18)
1622
+ commitViewOptions { options ->
1623
+ options.zoom = lockZoomLevel
1624
+ }
1625
+ }
1626
+
1627
+ fun applyLockTilt(tilt: Int) {
1628
+ lockTilt = tilt.coerceIn(0, 60)
1629
+ commitViewOptions { options ->
1630
+ options.tilt = lockTilt
1631
+ }
1632
+ }
1633
+
1634
+ fun applyEagleMapVisible(enabled: Boolean) {
1635
+ isEagleMapVisible = enabled
1636
+ commitViewOptions { options ->
1637
+ options.isEagleMapVisible = enabled
1638
+ }
1639
+ }
1640
+
1641
+ fun applyPointToCenter(x: Double, y: Double) {
1642
+ pointToCenterX = x
1643
+ pointToCenterY = y
1644
+ commitViewOptions { options ->
1645
+ if (x > 0.0 && y > 0.0) {
1646
+ options.setPointToCenter(x, y)
1647
+ }
1648
+ }
1649
+ }
1650
+
1651
+ fun applyHideNativeTopInfoLayout(hidden: Boolean) {
1652
+ hideNativeTopInfoLayout = hidden
1653
+ updateNativeTopInfoLayoutVisibility()
1654
+ refreshNaviUILayout("applyHideNativeTopInfoLayout")
864
1655
  }
865
1656
 
866
1657
 
867
1658
  fun applyNaviMode(mode: Int) {
868
- // 0: 车头朝上 1: 正北朝上
1659
+ naviModeValue = mode
1660
+ commitViewOptions { options ->
1661
+ applyNaviModeCompat(options, mode)
1662
+ }
1663
+ // 兼容旧版接口,保持与 options 一致
869
1664
  naviView.naviMode = mode
870
1665
  }
871
1666
 
@@ -875,21 +1670,26 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
875
1670
  }
876
1671
 
877
1672
  fun applyNightMode(enabled: Boolean) {
878
- // 夜间模式设置 - isNightMode 属性可能不存在
1673
+ mapViewModeTypeValue = if (enabled) 1 else 0
879
1674
  try {
880
- val options = naviView.viewOptions
881
- if(enabled){
882
- options.setMapStyle(MapStyle.NIGHT, null)
883
- }else{
884
- options.setMapStyle(MapStyle.DAY, null)
1675
+ commitViewOptions { options ->
1676
+ applyMapViewModeTypeCompat(options, mapViewModeTypeValue)
885
1677
  }
886
- // options.isNightMode = enabled // 该属性可能不存在
887
- // 可以通过其他方式设置夜间模式
888
- naviView.viewOptions = options
889
1678
  } catch (e: Exception) {
890
1679
  Log.e("ExpoGaodeMapNaviView", "Failed to set night mode", e)
891
1680
  }
892
1681
  }
1682
+
1683
+ fun applyMapViewModeType(mode: Int) {
1684
+ mapViewModeTypeValue = mode
1685
+ try {
1686
+ commitViewOptions { options ->
1687
+ applyMapViewModeTypeCompat(options, mode)
1688
+ }
1689
+ } catch (e: Exception) {
1690
+ Log.e("ExpoGaodeMapNaviView", "Failed to apply mapViewModeType=$mode", e)
1691
+ }
1692
+ }
893
1693
 
894
1694
  /**
895
1695
  * 设置是否显示自车和罗盘
@@ -905,6 +1705,200 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
905
1705
  }
906
1706
  }
907
1707
 
1708
+ fun applyCarImage(uri: String?) {
1709
+ updateCustomAnnotationBitmap(
1710
+ uri = uri,
1711
+ getCurrentUri = { carImageUri },
1712
+ setCurrentUri = { carImageUri = it },
1713
+ setBitmap = {
1714
+ sourceCarBitmap = it
1715
+ customCarBitmap = resizeCarBitmapIfNeeded(it)
1716
+ },
1717
+ reason = "carBitmap"
1718
+ )
1719
+ }
1720
+
1721
+ fun applyCarImageSize(widthDp: Double?, heightDp: Double?) {
1722
+ carImageWidthDp = widthDp
1723
+ carImageHeightDp = heightDp
1724
+ customCarBitmap = resizeCarBitmapIfNeeded(sourceCarBitmap)
1725
+ refreshViewOptionsFromState("apply-carBitmap-size")
1726
+ }
1727
+
1728
+ fun applyFourCornersImage(uri: String?) {
1729
+ updateCustomAnnotationBitmap(
1730
+ uri = uri,
1731
+ getCurrentUri = { fourCornersImageUri },
1732
+ setCurrentUri = { fourCornersImageUri = it },
1733
+ setBitmap = { customFourCornersBitmap = it },
1734
+ reason = "fourCornersBitmap"
1735
+ )
1736
+ }
1737
+
1738
+ fun applyStartPointImage(uri: String?) {
1739
+ updateCustomAnnotationBitmap(
1740
+ uri = uri,
1741
+ getCurrentUri = { startPointImageUri },
1742
+ setCurrentUri = { startPointImageUri = it },
1743
+ setBitmap = { customStartPointBitmap = it },
1744
+ reason = "startPointBitmap"
1745
+ )
1746
+ }
1747
+
1748
+ fun applyWayPointImage(uri: String?) {
1749
+ updateCustomAnnotationBitmap(
1750
+ uri = uri,
1751
+ getCurrentUri = { wayPointImageUri },
1752
+ setCurrentUri = { wayPointImageUri = it },
1753
+ setBitmap = { customWayPointBitmap = it },
1754
+ reason = "wayPointBitmap"
1755
+ )
1756
+ }
1757
+
1758
+ fun applyEndPointImage(uri: String?) {
1759
+ updateCustomAnnotationBitmap(
1760
+ uri = uri,
1761
+ getCurrentUri = { endPointImageUri },
1762
+ setCurrentUri = { endPointImageUri = it },
1763
+ setBitmap = { customEndPointBitmap = it },
1764
+ reason = "endPointBitmap"
1765
+ )
1766
+ }
1767
+
1768
+ fun applyCustomWaypointMarkers(markers: List<Map<String, Any?>>?) {
1769
+ customWaypointMarkers = markers?.mapNotNull { item ->
1770
+ val latitude = (item["latitude"] as? Number)?.toDouble() ?: return@mapNotNull null
1771
+ val longitude = (item["longitude"] as? Number)?.toDouble() ?: return@mapNotNull null
1772
+ val rawTitle = (item["title"] as? String)?.trim()
1773
+ NaviCustomWaypointMarkerModel(
1774
+ latitude = latitude,
1775
+ longitude = longitude,
1776
+ title = rawTitle?.takeIf { it.isNotEmpty() } ?: "途经"
1777
+ )
1778
+ } ?: emptyList()
1779
+ refreshCustomWaypointMarkers("apply-custom-waypoint-markers")
1780
+ }
1781
+
1782
+ private fun clearRenderedCustomWaypointMarkers() {
1783
+ renderedCustomWaypointMarkers.forEach { marker ->
1784
+ try {
1785
+ marker.remove()
1786
+ } catch (_: Throwable) {
1787
+ }
1788
+ }
1789
+ renderedCustomWaypointMarkers.clear()
1790
+ }
1791
+
1792
+ private fun refreshCustomWaypointMarkers(reason: String) {
1793
+ Handler(Looper.getMainLooper()).post {
1794
+ clearRenderedCustomWaypointMarkers()
1795
+ if (isDestroyed || customWaypointMarkers.isEmpty()) {
1796
+ return@post
1797
+ }
1798
+
1799
+ val map = try {
1800
+ naviView.map
1801
+ } catch (error: Throwable) {
1802
+ Log.w("ExpoGaodeMapNaviView", "Failed to access AMap for custom waypoint markers: $reason", error)
1803
+ null
1804
+ } ?: return@post
1805
+
1806
+ customWaypointMarkers.forEach { marker ->
1807
+ if (marker.arrived) {
1808
+ return@forEach
1809
+ }
1810
+
1811
+ val bitmap = createCustomWaypointBubbleBitmap(marker.title)
1812
+ val options = MarkerOptions()
1813
+ .position(LatLng(marker.latitude, marker.longitude))
1814
+ .anchor(0.5f, 1f)
1815
+ .zIndex(130f)
1816
+ .icon(BitmapDescriptorFactory.fromBitmap(bitmap))
1817
+ val renderedMarker = map.addMarker(options)
1818
+ if (renderedMarker != null) {
1819
+ renderedCustomWaypointMarkers.add(renderedMarker)
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+
1825
+ private fun resetCustomWaypointMarkerArrivalState() {
1826
+ customWaypointMarkers = customWaypointMarkers.map { marker ->
1827
+ marker.copy(arrived = false)
1828
+ }
1829
+ refreshCustomWaypointMarkers("reset-custom-waypoint-arrival-state")
1830
+ }
1831
+
1832
+ private fun markCustomWaypointArrived(rawIndex: Int) {
1833
+ if (customWaypointMarkers.isEmpty()) {
1834
+ return
1835
+ }
1836
+
1837
+ val resolvedIndex = when {
1838
+ rawIndex in customWaypointMarkers.indices -> rawIndex
1839
+ (rawIndex - 1) in customWaypointMarkers.indices -> rawIndex - 1
1840
+ else -> null
1841
+ } ?: return
1842
+
1843
+ customWaypointMarkers = customWaypointMarkers.mapIndexed { index, marker ->
1844
+ if (index == resolvedIndex) marker.copy(arrived = true) else marker
1845
+ }
1846
+ refreshCustomWaypointMarkers("arrived-waypoint-$rawIndex")
1847
+ }
1848
+
1849
+ private fun createCustomWaypointBubbleBitmap(title: String): Bitmap {
1850
+ val density = context.resources.displayMetrics.density
1851
+ val fontSize = 16f * density
1852
+ val horizontalPadding = 14f * density
1853
+ val bodyHeight = 34f * density
1854
+ val strokeWidth = 2.5f * density
1855
+
1856
+ val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1857
+ color = Color.WHITE
1858
+ textSize = fontSize
1859
+ typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
1860
+ textAlign = Paint.Align.CENTER
1861
+ }
1862
+ val bodyWidth = maxOf(
1863
+ 62f * density,
1864
+ textPaint.measureText(title) + horizontalPadding * 2
1865
+ )
1866
+ val width = bodyWidth.roundToInt()
1867
+ val height = (bodyHeight + 2f * density).roundToInt()
1868
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
1869
+ val canvas = Canvas(bitmap)
1870
+
1871
+ val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1872
+ color = Color.parseColor("#2F67FF")
1873
+ style = Paint.Style.FILL
1874
+ }
1875
+ val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1876
+ color = Color.WHITE
1877
+ style = Paint.Style.STROKE
1878
+ this.strokeWidth = strokeWidth
1879
+ }
1880
+ val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1881
+ color = Color.parseColor("#2D15357F")
1882
+ style = Paint.Style.FILL
1883
+ setShadowLayer(6f * density, 0f, 3f * density, Color.parseColor("#2D15357F"))
1884
+ }
1885
+
1886
+ val bodyRect = RectF(
1887
+ strokeWidth,
1888
+ strokeWidth,
1889
+ width - strokeWidth,
1890
+ bodyHeight
1891
+ )
1892
+ val cornerRadius = 17f * density
1893
+ canvas.drawRoundRect(bodyRect, cornerRadius, cornerRadius, shadowPaint)
1894
+ canvas.drawRoundRect(bodyRect, cornerRadius, cornerRadius, fillPaint)
1895
+ canvas.drawRoundRect(bodyRect, cornerRadius, cornerRadius, strokePaint)
1896
+
1897
+ val textY = bodyRect.centerY() - (textPaint.descent() + textPaint.ascent()) / 2f
1898
+ canvas.drawText(title, bodyRect.centerX(), textY, textPaint)
1899
+ return bitmap
1900
+ }
1901
+
908
1902
 
909
1903
  /**
910
1904
  * 设置是否显示交通信号灯
@@ -936,8 +1930,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
936
1930
  showRouteStartIcon: Boolean,
937
1931
  showRouteEndIcon: Boolean
938
1932
  ) {
1933
+ routeMarkerShowStartEndVia = showStartEndVia
1934
+ routeMarkerShowFootFerry = showFootFerry
1935
+ routeMarkerShowForbidden = showForbidden
1936
+ routeMarkerShowRouteStartIcon = showRouteStartIcon
1937
+ routeMarkerShowRouteEndIcon = showRouteEndIcon
939
1938
  try {
940
- naviView.setRouteMarkerVisible(showStartEndVia, showFootFerry, showForbidden, showRouteStartIcon, showRouteEndIcon)
1939
+ applyRouteMarkerVisibleFromState()
941
1940
  Log.d("ExpoGaodeMapNaviView", "Route marker visibility set - startEnd:$showStartEndVia, ferry:$showFootFerry, forbidden:$showForbidden, routeStart:$showRouteStartIcon, routeEnd:$showRouteEndIcon")
942
1941
  } catch (e: Exception) {
943
1942
  Log.e("ExpoGaodeMapNaviView", "Failed to set route marker visibility", e)
@@ -985,9 +1984,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
985
1984
  fun applyNaviArrowVisible(visible: Boolean) {
986
1985
  try {
987
1986
  isNaviArrowVisible = visible
988
- val options = naviView.viewOptions
989
- options.isNaviArrowVisible = visible
990
- naviView.viewOptions = options
1987
+ commitViewOptions { options ->
1988
+ options.isNaviArrowVisible = visible
1989
+ }
991
1990
  Log.d("ExpoGaodeMapNaviView", "Navi arrow visibility set to: $visible")
992
1991
  } catch (e: Exception) {
993
1992
  Log.e("ExpoGaodeMapNaviView", "Failed to set navi arrow visibility", e)
@@ -996,9 +1995,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
996
1995
 
997
1996
  override fun onAttachedToWindow() {
998
1997
  super.onAttachedToWindow()
1998
+ registerActiveView()
999
1999
  try {
1000
- naviView.onResume()
1001
- Log.d("ExpoGaodeMapNaviView", "NaviView resumed")
2000
+ onResume()
1002
2001
  } catch (e: Exception) {
1003
2002
  Log.e("ExpoGaodeMapNaviView", "Error resuming navi view", e)
1004
2003
  }
@@ -1007,42 +2006,57 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1007
2006
  override fun onDetachedFromWindow() {
1008
2007
  super.onDetachedFromWindow()
1009
2008
  try {
1010
- naviView.onPause()
1011
- naviView.onDestroy()
1012
-
1013
- // 停止语音播报
1014
- aMapNavi?.stopSpeak()
1015
-
1016
- // 移除监听器但保留 AMapNavi 实例(因为是单例)
1017
- aMapNavi?.removeAMapNaviListener(naviListener)
1018
-
1019
- Log.d("ExpoGaodeMapNaviView", "NaviView paused and destroyed")
2009
+ onPause()
1020
2010
  } catch (e: Exception) {
1021
- Log.e("ExpoGaodeMapNaviView", "Error destroying navi view", e)
2011
+ Log.e("ExpoGaodeMapNaviView", "Error pausing navi view", e)
1022
2012
  }
1023
2013
  }
1024
2014
 
1025
2015
  // 生命周期方法(供外部调用)
1026
2016
  fun onResume() {
2017
+ if (isDestroyed) {
2018
+ return
2019
+ }
1027
2020
  try {
1028
2021
  naviView.onResume()
2022
+ refreshNaviUILayout("onResume")
2023
+ Log.d("ExpoGaodeMapNaviView", "NaviView resumed")
1029
2024
  } catch (e: Exception) {
1030
2025
  Log.e("ExpoGaodeMapNaviView", "Error resuming navi view", e)
1031
2026
  }
1032
2027
  }
1033
2028
 
1034
2029
  fun onPause() {
2030
+ if (isDestroyed) {
2031
+ return
2032
+ }
1035
2033
  try {
1036
2034
  naviView.onPause()
2035
+ Log.d("ExpoGaodeMapNaviView", "NaviView paused")
1037
2036
  } catch (e: Exception) {
1038
2037
  Log.e("ExpoGaodeMapNaviView", "Error pausing navi view", e)
1039
2038
  }
1040
2039
  }
1041
2040
 
1042
2041
  fun onDestroy() {
2042
+ if (isDestroyed) {
2043
+ return
2044
+ }
2045
+ isDestroyed = true
2046
+ isNavigationRunning = false
2047
+ isHostActivityInBackground = false
2048
+ latestNavigationNotificationSnapshot = null
2049
+ NavigationForegroundService.stop(context)
2050
+ unregisterActiveView()
2051
+ clearRenderedCustomWaypointMarkers()
1043
2052
  try {
2053
+ naviView.onPause()
1044
2054
  naviView.onDestroy()
2055
+ aMapNavi?.stopSpeak()
1045
2056
  aMapNavi?.removeAMapNaviListener(naviListener)
2057
+ deleteCachedIconFile(cachedTurnIconImageUri)
2058
+ cachedTurnIconImageUri = null
2059
+ cachedTurnIconContentHash = null
1046
2060
  // AMapNavi 是单例,不需要手动 destroy
1047
2061
  } catch (e: Exception) {
1048
2062
  Log.e("ExpoGaodeMapNaviView", "Error destroying navi view", e)