expo-gaode-map-navigation 2.0.8 → 2.0.9

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 (126) hide show
  1. package/README.md +233 -3
  2. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +4 -2
  3. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +931 -391
  4. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +86 -1
  5. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +4 -5
  6. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/IndependentRouteListener.kt +4 -3
  7. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/RouteCalculateListener.kt +2 -2
  8. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/IndependentRouteManager.kt +96 -14
  9. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +2 -0
  10. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +19 -10
  11. package/android/src/main/res/drawable/landback_0.png +0 -0
  12. package/android/src/main/res/drawable/landback_1.png +0 -0
  13. package/android/src/main/res/drawable/landback_2.png +0 -0
  14. package/android/src/main/res/drawable/landback_3.png +0 -0
  15. package/android/src/main/res/drawable/landback_4.png +0 -0
  16. package/android/src/main/res/drawable/landback_5.png +0 -0
  17. package/android/src/main/res/drawable/landback_6.png +0 -0
  18. package/android/src/main/res/drawable/landback_7.png +0 -0
  19. package/android/src/main/res/drawable/landback_8.png +0 -0
  20. package/android/src/main/res/drawable/landback_9.png +0 -0
  21. package/android/src/main/res/drawable/landback_a.png +0 -0
  22. package/android/src/main/res/drawable/landback_b.png +0 -0
  23. package/android/src/main/res/drawable/landback_c.png +0 -0
  24. package/android/src/main/res/drawable/landback_d.png +0 -0
  25. package/android/src/main/res/drawable/landback_e.png +0 -0
  26. package/android/src/main/res/drawable/landback_f.png +0 -0
  27. package/android/src/main/res/drawable/landback_g.png +0 -0
  28. package/android/src/main/res/drawable/landback_h.png +0 -0
  29. package/android/src/main/res/drawable/landback_i.png +0 -0
  30. package/android/src/main/res/drawable/landback_j.png +0 -0
  31. package/android/src/main/res/drawable/landback_k.png +0 -0
  32. package/android/src/main/res/drawable/landback_l.png +0 -0
  33. package/android/src/main/res/drawable/landfront_0.png +0 -0
  34. package/android/src/main/res/drawable/landfront_00.png +0 -0
  35. package/android/src/main/res/drawable/landfront_1.png +0 -0
  36. package/android/src/main/res/drawable/landfront_11.png +0 -0
  37. package/android/src/main/res/drawable/landfront_20.png +0 -0
  38. package/android/src/main/res/drawable/landfront_21.png +0 -0
  39. package/android/src/main/res/drawable/landfront_22.png +0 -0
  40. package/android/src/main/res/drawable/landfront_3.png +0 -0
  41. package/android/src/main/res/drawable/landfront_33.png +0 -0
  42. package/android/src/main/res/drawable/landfront_40.png +0 -0
  43. package/android/src/main/res/drawable/landfront_43.png +0 -0
  44. package/android/src/main/res/drawable/landfront_44.png +0 -0
  45. package/android/src/main/res/drawable/landfront_5.png +0 -0
  46. package/android/src/main/res/drawable/landfront_55.png +0 -0
  47. package/android/src/main/res/drawable/landfront_61.png +0 -0
  48. package/android/src/main/res/drawable/landfront_63.png +0 -0
  49. package/android/src/main/res/drawable/landfront_66.png +0 -0
  50. package/android/src/main/res/drawable/landfront_70.png +0 -0
  51. package/android/src/main/res/drawable/landfront_71.png +0 -0
  52. package/android/src/main/res/drawable/landfront_73.png +0 -0
  53. package/android/src/main/res/drawable/landfront_77.png +0 -0
  54. package/android/src/main/res/drawable/landfront_8.png +0 -0
  55. package/android/src/main/res/drawable/landfront_88.png +0 -0
  56. package/android/src/main/res/drawable/landfront_90.png +0 -0
  57. package/android/src/main/res/drawable/landfront_95.png +0 -0
  58. package/android/src/main/res/drawable/landfront_99.png +0 -0
  59. package/android/src/main/res/drawable/landfront_a0.png +0 -0
  60. package/android/src/main/res/drawable/landfront_a8.png +0 -0
  61. package/android/src/main/res/drawable/landfront_aa.png +0 -0
  62. package/android/src/main/res/drawable/landfront_b1.png +0 -0
  63. package/android/src/main/res/drawable/landfront_b5.png +0 -0
  64. package/android/src/main/res/drawable/landfront_bb.png +0 -0
  65. package/android/src/main/res/drawable/landfront_c3.png +0 -0
  66. package/android/src/main/res/drawable/landfront_c8.png +0 -0
  67. package/android/src/main/res/drawable/landfront_cc.png +0 -0
  68. package/android/src/main/res/drawable/landfront_d.png +0 -0
  69. package/android/src/main/res/drawable/landfront_dd.png +0 -0
  70. package/android/src/main/res/drawable/landfront_e1.png +0 -0
  71. package/android/src/main/res/drawable/landfront_e5.png +0 -0
  72. package/android/src/main/res/drawable/landfront_ee.png +0 -0
  73. package/android/src/main/res/drawable/landfront_f0.png +0 -0
  74. package/android/src/main/res/drawable/landfront_f1.png +0 -0
  75. package/android/src/main/res/drawable/landfront_f5.png +0 -0
  76. package/android/src/main/res/drawable/landfront_ff.png +0 -0
  77. package/android/src/main/res/drawable/landfront_g3.png +0 -0
  78. package/android/src/main/res/drawable/landfront_g5.png +0 -0
  79. package/android/src/main/res/drawable/landfront_gg.png +0 -0
  80. package/android/src/main/res/drawable/landfront_h1.png +0 -0
  81. package/android/src/main/res/drawable/landfront_h3.png +0 -0
  82. package/android/src/main/res/drawable/landfront_h5.png +0 -0
  83. package/android/src/main/res/drawable/landfront_hh.png +0 -0
  84. package/android/src/main/res/drawable/landfront_i0.png +0 -0
  85. package/android/src/main/res/drawable/landfront_i3.png +0 -0
  86. package/android/src/main/res/drawable/landfront_i5.png +0 -0
  87. package/android/src/main/res/drawable/landfront_ii.png +0 -0
  88. package/android/src/main/res/drawable/landfront_j1.png +0 -0
  89. package/android/src/main/res/drawable/landfront_j8.png +0 -0
  90. package/android/src/main/res/drawable/landfront_jj.png +0 -0
  91. package/android/src/main/res/drawable/landfront_kk.png +0 -0
  92. package/android/src/main/res/drawable/landfront_ll.png +0 -0
  93. package/android/src/main/res/drawable/navi_arrow_leftline.png +0 -0
  94. package/android/src/main/res/drawable/navi_lane_shape_bg_center.xml +5 -0
  95. package/android/src/main/res/drawable/navi_lane_shape_bg_left.xml +8 -0
  96. package/android/src/main/res/drawable/navi_lane_shape_bg_over.xml +6 -0
  97. package/android/src/main/res/drawable/navi_lane_shape_bg_right.xml +8 -0
  98. package/build/ExpoGaodeMapNaviView.d.ts +8 -0
  99. package/build/ExpoGaodeMapNaviView.d.ts.map +1 -1
  100. package/build/ExpoGaodeMapNaviView.js +38 -1
  101. package/build/ExpoGaodeMapNaviView.js.map +1 -1
  102. package/build/index.d.ts +8 -4
  103. package/build/index.d.ts.map +1 -1
  104. package/build/index.js +408 -4
  105. package/build/index.js.map +1 -1
  106. package/build/types/independent.types.d.ts +91 -0
  107. package/build/types/independent.types.d.ts.map +1 -1
  108. package/build/types/independent.types.js.map +1 -1
  109. package/build/types/naviview.types.d.ts +256 -12
  110. package/build/types/naviview.types.d.ts.map +1 -1
  111. package/build/types/naviview.types.js.map +1 -1
  112. package/build/types/route.types.d.ts +2 -0
  113. package/build/types/route.types.d.ts.map +1 -1
  114. package/build/types/route.types.js.map +1 -1
  115. package/ios/ExpoGaodeMapNaviView.swift +888 -66
  116. package/ios/ExpoGaodeMapNaviViewModule.swift +87 -1
  117. package/ios/ExpoGaodeMapNavigationModule.swift +1 -1
  118. package/ios/managers/IndependentRouteManager.swift +1 -0
  119. package/ios/map/ExpoGaodeMapModule.swift +9 -4
  120. package/ios/map/ExpoGaodeMapView.swift +13 -2
  121. package/ios/map/modules/LocationManager.swift +17 -0
  122. package/ios/map/utils/PermissionManager.swift +11 -6
  123. package/ios/routes/drive/DriveTruckRouteCalculator.swift +9 -0
  124. package/ios/routes/walkride/WalkRideRouteCalculator.swift +30 -0
  125. package/ios/services/IndependentRouteService.swift +25 -0
  126. package/package.json +5 -2
@@ -2,7 +2,13 @@ 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.Color
8
+ import android.net.Uri
5
9
  import android.os.Bundle
10
+ import android.os.Handler
11
+ import android.os.Looper
6
12
  import android.util.Log
7
13
  import android.view.View
8
14
  import android.view.ViewGroup
@@ -15,14 +21,42 @@ import com.amap.api.navi.enums.MapStyle
15
21
  import com.amap.api.navi.enums.NaviType
16
22
  import com.amap.api.navi.model.*
17
23
  import expo.modules.gaodemap.map.modules.SDKInitializer
24
+ import expo.modules.gaodemap.navigation.managers.IndependentRouteManager
18
25
  import expo.modules.kotlin.AppContext
19
26
  import expo.modules.kotlin.viewevent.EventDispatcher
20
27
  import expo.modules.kotlin.views.ExpoView
21
- import java.util.IdentityHashMap
28
+ import java.io.ByteArrayOutputStream
29
+ import java.io.File
30
+ import java.io.FileOutputStream
31
+ import java.security.MessageDigest
32
+ import java.util.Collections
33
+ import java.util.WeakHashMap
34
+ import java.net.URL
22
35
 
23
36
  @SuppressLint("ViewConstructor")
24
37
  @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
25
38
  class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
39
+ companion object {
40
+ private val activeViews =
41
+ Collections.newSetFromMap(WeakHashMap<ExpoGaodeMapNaviView, Boolean>())
42
+
43
+ private fun snapshotActiveViews(): List<ExpoGaodeMapNaviView> =
44
+ synchronized(activeViews) { activeViews.toList() }
45
+
46
+ fun resumeActiveViews() {
47
+ snapshotActiveViews().forEach { it.onResume() }
48
+ }
49
+
50
+ fun pauseActiveViews() {
51
+ snapshotActiveViews().forEach { it.onPause() }
52
+ }
53
+
54
+ fun destroyActiveViews() {
55
+ snapshotActiveViews().forEach { it.onDestroy() }
56
+ }
57
+ }
58
+
59
+ private val independentRouteManager = IndependentRouteManager.shared
26
60
 
27
61
  // 事件派发器
28
62
  private val onNavigationReady by EventDispatcher()
@@ -38,6 +72,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
38
72
  private val onGpsStatusChanged by EventDispatcher()
39
73
  private val onNavigationInfoUpdate by EventDispatcher()
40
74
  private val onGpsSignalWeak by EventDispatcher()
75
+ private val onNavigationVisualStateUpdate by EventDispatcher()
76
+ private val onLaneInfoUpdate by EventDispatcher()
77
+ private val onTrafficStatusesUpdate by EventDispatcher()
41
78
 
42
79
  // Props - 使用 internal 避免自动生成 setter
43
80
  internal var showCamera: Boolean = true
@@ -50,16 +87,34 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
50
87
  internal var showUIElements: Boolean = true
51
88
  internal var androidTrafficBarEnabled: Boolean = true
52
89
  internal var isRouteListButtonShow: Boolean = true
53
- internal var isTrafficLayerEnabled: Boolean = true
90
+ internal var isTrafficButtonVisible: Boolean = true
54
91
  internal var autoChangeZoom : Boolean = true
55
92
  internal var autoLockCar: Boolean = true
56
- internal var isTrafficLine: Boolean = true
93
+ internal var isTrafficLineEnabled: Boolean = true
57
94
  internal var isRealCrossDisplayShow : Boolean = true
58
95
  internal var isNaviArrowVisible : Boolean = true
96
+ internal var isLaneInfoVisible: Boolean = true
97
+ internal var isModeCrossDisplayVisible: Boolean = true
98
+ internal var isEyrieCrossDisplayVisible: Boolean = true
99
+ internal var isSecondActionVisible: Boolean = true
100
+ internal var isBackupOverlayVisible: Boolean = true
59
101
  internal var isAfterRouteAutoGray: Boolean = false
60
102
  internal var isVectorLineShow: Boolean = true
61
103
  internal var isNaviTravelView : Boolean = false
62
104
  internal var isCompassEnabled: Boolean = true
105
+ internal var isNaviStatusBarEnabled: Boolean = false
106
+ internal var lockZoomLevel: Int = 18
107
+ internal var lockTilt: Int = 35
108
+ internal var isEagleMapVisible: Boolean = false
109
+ internal var pointToCenterX: Double = 0.0
110
+ internal var pointToCenterY: Double = 0.0
111
+ internal var hideNativeTopInfoLayout: Boolean = false
112
+ internal var naviModeValue: Int = AMapNaviView.CAR_UP_MODE
113
+ internal var carImageUri: String? = null
114
+ internal var fourCornersImageUri: String? = null
115
+ internal var startPointImageUri: String? = null
116
+ internal var wayPointImageUri: String? = null
117
+ internal var endPointImageUri: String? = null
63
118
  private val naviView: AMapNaviView by lazy(LazyThreadSafetyMode.NONE) {
64
119
  AMapNaviView(context)
65
120
  }
@@ -68,15 +123,550 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
68
123
  private var endCoordinate: NaviLatLng? = null
69
124
 
70
125
  private var lastAppliedTopPaddingPx: Int? = null
126
+ private var isDestroyed = false
127
+ private var isCrossVisible = false
128
+ private var isModeCrossVisible = false
129
+ private var isLaneInfoCurrentlyVisible = false
130
+ private var currentRouteTotalLength: Int? = null
131
+ private var customCarBitmap: Bitmap? = null
132
+ private var customFourCornersBitmap: Bitmap? = null
133
+ private var customStartPointBitmap: Bitmap? = null
134
+ private var customWayPointBitmap: Bitmap? = null
135
+ private var customEndPointBitmap: Bitmap? = null
136
+ private var routeMarkerShowStartEndVia: Boolean = true
137
+ private var routeMarkerShowFootFerry: Boolean = true
138
+ private var routeMarkerShowForbidden: Boolean = true
139
+ private var routeMarkerShowRouteStartIcon: Boolean = true
140
+ private var routeMarkerShowRouteEndIcon: Boolean = true
141
+ private var cachedTurnIconImageUri: String? = null
142
+ private var cachedTurnIconContentHash: String? = null
143
+ private var hasLoggedMissingTurnIconBitmapApi = false
144
+
145
+ private fun registerActiveView() {
146
+ synchronized(activeViews) {
147
+ activeViews.add(this)
148
+ }
149
+ }
150
+
151
+ private fun unregisterActiveView() {
152
+ synchronized(activeViews) {
153
+ activeViews.remove(this)
154
+ }
155
+ }
156
+
157
+ private fun applyNaviStatusBarEnabledCompat(
158
+ options: AMapNaviViewOptions,
159
+ enabled: Boolean
160
+ ) {
161
+ try {
162
+ val method = options.javaClass.getMethod(
163
+ "setNaviStatusBarEnabled",
164
+ java.lang.Boolean.TYPE
165
+ )
166
+ method.invoke(options, enabled)
167
+ } catch (_: NoSuchMethodException) {
168
+ Log.w(
169
+ "ExpoGaodeMapNaviView",
170
+ "AMapNaviViewOptions#setNaviStatusBarEnabled is unavailable in the current AMap SDK; skip applying naviStatusBarEnabled"
171
+ )
172
+ } catch (error: Throwable) {
173
+ Log.w(
174
+ "ExpoGaodeMapNaviView",
175
+ "Failed to apply naviStatusBarEnabled compatibly",
176
+ error
177
+ )
178
+ }
179
+ }
180
+
181
+ private fun applyNaviModeCompat(
182
+ options: AMapNaviViewOptions,
183
+ mode: Int
184
+ ) {
185
+ try {
186
+ val method = options.javaClass.getMethod(
187
+ "setNaviMode",
188
+ Integer.TYPE
189
+ )
190
+ method.invoke(options, mode)
191
+ } catch (_: NoSuchMethodException) {
192
+ Log.w(
193
+ "ExpoGaodeMapNaviView",
194
+ "AMapNaviViewOptions#setNaviMode is unavailable in the current AMap SDK; skip applying naviMode on options"
195
+ )
196
+ } catch (error: Throwable) {
197
+ Log.w(
198
+ "ExpoGaodeMapNaviView",
199
+ "Failed to apply naviMode compatibly",
200
+ error
201
+ )
202
+ }
203
+ }
204
+
205
+ private fun createInitialViewOptions(): AMapNaviViewOptions {
206
+ return AMapNaviViewOptions().also { options ->
207
+ applyAllViewOptions(options)
208
+ }
209
+ }
210
+
211
+ private fun applyAllViewOptions(options: AMapNaviViewOptions) {
212
+ options.isLayoutVisible = showUIElements
213
+ options.isSettingMenuEnabled = true
214
+ options.isCompassEnabled = isCompassEnabled
215
+ options.isTrafficBarEnabled = androidTrafficBarEnabled
216
+ options.isRouteListButtonShow = isRouteListButtonShow
217
+ applyNaviStatusBarEnabledCompat(options, isNaviStatusBarEnabled)
218
+
219
+ options.isTrafficLayerEnabled = isTrafficButtonVisible
220
+ options.isTrafficLine = isTrafficLineEnabled
221
+
222
+ options.isRealCrossDisplayShow = isRealCrossDisplayShow
223
+ options.setModeCrossDisplayShow(isModeCrossDisplayVisible)
224
+ options.isLaneInfoShow = isLaneInfoVisible
225
+ options.isEyrieCrossDisplay = isEyrieCrossDisplayVisible
226
+
227
+ options.isCameraBubbleShow = showCamera
228
+ options.isShowCameraDistance = true
229
+ options.isWidgetOverSpeedPulseEffective = true
230
+
231
+ options.isAutoDrawRoute = true
232
+ options.isNaviArrowVisible = isNaviArrowVisible
233
+ options.isSecondActionVisible = isSecondActionVisible
234
+ options.isDrawBackUpOverlay = isBackupOverlayVisible
235
+ applyLeaderLineSetting(options, isVectorLineShow)
236
+
237
+ options.isAutoLockCar = autoLockCar
238
+ options.lockMapDelayed = 5000L
239
+ options.isAutoDisplayOverview = false
240
+ options.isAutoChangeZoom = autoChangeZoom
241
+ options.zoom = lockZoomLevel.coerceIn(14, 18)
242
+ options.tilt = lockTilt.coerceIn(0, 60)
243
+ applyNaviModeCompat(options, naviModeValue)
244
+ if (pointToCenterX > 0.0 && pointToCenterY > 0.0) {
245
+ options.setPointToCenter(pointToCenterX, pointToCenterY)
246
+ }
247
+
248
+ options.isAfterRouteAutoGray = isAfterRouteAutoGray
249
+ options.isSensorEnable = true
250
+ options.isAutoNaviViewNightMode = false
251
+ options.isEagleMapVisible = isEagleMapVisible
252
+ applyCustomAnnotationBitmaps(options)
253
+ }
254
+
255
+ private fun applyBitmapOptionCompat(
256
+ options: AMapNaviViewOptions,
257
+ methodName: String,
258
+ bitmap: Bitmap?
259
+ ) {
260
+ try {
261
+ val method = options.javaClass.getMethod(methodName, Bitmap::class.java)
262
+ method.invoke(options, bitmap)
263
+ } catch (_: NoSuchMethodException) {
264
+ Log.w(
265
+ "ExpoGaodeMapNaviView",
266
+ "AMapNaviViewOptions#$methodName is unavailable in the current AMap SDK; skip applying custom bitmap"
267
+ )
268
+ } catch (error: Throwable) {
269
+ Log.w(
270
+ "ExpoGaodeMapNaviView",
271
+ "Failed to apply $methodName compatibly",
272
+ error
273
+ )
274
+ }
275
+ }
276
+
277
+ private fun applyCustomAnnotationBitmaps(options: AMapNaviViewOptions) {
278
+ applyBitmapOptionCompat(options, "setCarBitmap", customCarBitmap)
279
+ applyBitmapOptionCompat(options, "setFourCornersBitmap", customFourCornersBitmap)
280
+ applyBitmapOptionCompat(options, "setStartPointBitmap", customStartPointBitmap)
281
+ applyBitmapOptionCompat(options, "setWayPointBitmap", customWayPointBitmap)
282
+ applyBitmapOptionCompat(options, "setEndPointBitmap", customEndPointBitmap)
283
+ }
284
+
285
+ private fun refreshViewOptionsFromState(reason: String) {
286
+ if (isDestroyed) {
287
+ return
288
+ }
289
+ naviView.viewOptions = createInitialViewOptions()
290
+ refreshNaviUILayout(reason)
291
+ }
292
+
293
+ private fun loadBitmapFromSource(imagePath: String): Bitmap? {
294
+ return try {
295
+ when {
296
+ imagePath.startsWith("http://") || imagePath.startsWith("https://") -> {
297
+ URL(imagePath).openStream().use { BitmapFactory.decodeStream(it) }
298
+ }
299
+ imagePath.startsWith("file://") -> {
300
+ BitmapFactory.decodeFile(imagePath.substring(7))
301
+ }
302
+ else -> {
303
+ val fileName = imagePath.substringBeforeLast('.')
304
+ val resId = context.resources.getIdentifier(
305
+ fileName,
306
+ "drawable",
307
+ context.packageName
308
+ )
309
+ if (resId != 0) {
310
+ BitmapFactory.decodeResource(context.resources, resId)
311
+ } else {
312
+ BitmapFactory.decodeFile(imagePath)
313
+ }
314
+ }
315
+ }
316
+ } catch (_: Throwable) {
317
+ null
318
+ }
319
+ }
320
+
321
+ private fun updateCustomAnnotationBitmap(
322
+ uri: String?,
323
+ getCurrentUri: () -> String?,
324
+ setCurrentUri: (String?) -> Unit,
325
+ setBitmap: (Bitmap?) -> Unit,
326
+ reason: String
327
+ ) {
328
+ val normalizedUri = uri?.takeIf { it.isNotBlank() }
329
+ setCurrentUri(normalizedUri)
330
+
331
+ if (normalizedUri == null) {
332
+ setBitmap(null)
333
+ refreshViewOptionsFromState("clear-$reason")
334
+ return
335
+ }
336
+
337
+ Thread {
338
+ val bitmap = loadBitmapFromSource(normalizedUri)
339
+ Handler(Looper.getMainLooper()).post {
340
+ if (isDestroyed || getCurrentUri() != normalizedUri) {
341
+ return@post
342
+ }
343
+ setBitmap(bitmap)
344
+ refreshViewOptionsFromState("apply-$reason")
345
+ }
346
+ }.start()
347
+ }
348
+
349
+ private fun applyRouteMarkerVisibleFromState() {
350
+ naviView.setRouteMarkerVisible(
351
+ routeMarkerShowStartEndVia,
352
+ routeMarkerShowFootFerry,
353
+ routeMarkerShowForbidden,
354
+ routeMarkerShowRouteStartIcon,
355
+ routeMarkerShowRouteEndIcon
356
+ )
357
+ }
358
+
359
+ private fun commitViewOptions(mutator: (AMapNaviViewOptions) -> Unit) {
360
+ val options = naviView.viewOptions ?: AMapNaviViewOptions()
361
+ mutator(options)
362
+ naviView.viewOptions = options
363
+ refreshNaviUILayout("commitViewOptions")
364
+ }
365
+
366
+ private fun refreshNaviUILayout(reason: String) {
367
+ if (isDestroyed) {
368
+ return
369
+ }
370
+
371
+ naviView.post {
372
+ if (isDestroyed) {
373
+ return@post
374
+ }
375
+
376
+ updateTopInsetPadding()
377
+ naviView.requestLayout()
378
+ naviView.forceLayout()
379
+ naviView.invalidate()
380
+ naviView.postInvalidateOnAnimation()
381
+ requestLayout()
382
+ invalidate()
383
+ postInvalidateOnAnimation()
384
+ updateNativeTopInfoLayoutVisibility()
385
+ Log.d("ExpoGaodeMapNaviView", "refreshNaviUILayout: $reason")
386
+ }
387
+ }
388
+
389
+ private fun shouldHideNativeTopInfoView(view: View): Boolean {
390
+ val className = view.javaClass.name
391
+ return className.contains("BaseNaviInfoLayout") ||
392
+ className.contains("NaviInfoLayout_")
393
+ }
394
+
395
+ private fun updateNativeTopInfoLayoutVisibility() {
396
+ if (isDestroyed) {
397
+ return
398
+ }
399
+
400
+ val queue = ArrayDeque<View>()
401
+ val targetVisibility = if (hideNativeTopInfoLayout) View.GONE else View.VISIBLE
402
+ queue.add(naviView)
403
+
404
+ while (queue.isNotEmpty()) {
405
+ val current = queue.removeFirst()
406
+ if (current is ViewGroup) {
407
+ for (index in 0 until current.childCount) {
408
+ queue.add(current.getChildAt(index))
409
+ }
410
+ }
411
+
412
+ if (!shouldHideNativeTopInfoView(current)) {
413
+ continue
414
+ }
415
+
416
+ if (current.visibility != targetVisibility) {
417
+ current.visibility = targetVisibility
418
+ current.requestLayout()
419
+ current.invalidate()
420
+ }
421
+ }
422
+ }
423
+
424
+ private fun suppressNativeTopInfoLayoutFlash() {
425
+ if (!hideNativeTopInfoLayout || isDestroyed) {
426
+ return
427
+ }
428
+
429
+ val delays = longArrayOf(0L, 16L, 48L, 96L, 180L)
430
+ delays.forEach { delayMs ->
431
+ naviView.postDelayed({
432
+ if (!isDestroyed) {
433
+ updateNativeTopInfoLayoutVisibility()
434
+ }
435
+ }, delayMs)
436
+ }
437
+ }
438
+
439
+ private fun emitVisualStateUpdate() {
440
+ onNavigationVisualStateUpdate(
441
+ mapOf(
442
+ "isCrossVisible" to isCrossVisible,
443
+ "isModeCrossVisible" to isModeCrossVisible,
444
+ "isLaneInfoVisible" to isLaneInfoCurrentlyVisible
445
+ )
446
+ )
447
+ }
448
+
449
+ private fun extractLaneValues(raw: Any?): List<Int> {
450
+ return when (raw) {
451
+ is IntArray -> raw.map { it.toInt() }
452
+ is ByteArray -> raw.map { it.toInt() and 0xFF }
453
+ is ShortArray -> raw.map { it.toInt() }
454
+ is Array<*> -> raw.mapNotNull { (it as? Number)?.toInt() }
455
+ else -> emptyList()
456
+ }
457
+ }
458
+
459
+ private fun getLaneArrayValue(laneInfo: AMapLaneInfo, fieldName: String): List<Int> {
460
+ val reflectedValue = runCatching {
461
+ laneInfo.javaClass.getField(fieldName).get(laneInfo)
462
+ }.getOrElse {
463
+ runCatching {
464
+ val getterName = "get" + fieldName.replaceFirstChar { char ->
465
+ if (char.isLowerCase()) {
466
+ char.titlecase()
467
+ } else {
468
+ char.toString()
469
+ }
470
+ }
471
+ laneInfo.javaClass.getMethod(getterName).invoke(laneInfo)
472
+ }.getOrNull()
473
+ }
474
+
475
+ return extractLaneValues(reflectedValue)
476
+ }
477
+
478
+ private fun resolveLaneCount(laneInfo: AMapLaneInfo): Int {
479
+ val reflectedCount = runCatching {
480
+ laneInfo.javaClass.getField("laneCount").get(laneInfo) as? Number
481
+ }.getOrElse {
482
+ runCatching {
483
+ laneInfo.javaClass.getMethod("getLaneCount").invoke(laneInfo) as? Number
484
+ }.getOrNull()
485
+ }
486
+
487
+ return reflectedCount?.toInt()?.coerceAtLeast(0) ?: 0
488
+ }
489
+
490
+ private fun serializeLaneInfo(laneInfo: AMapLaneInfo?): Map<String, Any>? {
491
+ if (laneInfo == null) {
492
+ return null
493
+ }
494
+
495
+ val backgroundLane = getLaneArrayValue(laneInfo, "backgroundLane")
496
+ val frontLane = getLaneArrayValue(laneInfo, "frontLane")
497
+ val declaredCount = resolveLaneCount(laneInfo)
498
+ val sentinelIndex = backgroundLane.indexOf(255).takeIf { it >= 0 }
499
+
500
+ val resolvedCount = listOfNotNull(
501
+ sentinelIndex,
502
+ declaredCount.takeIf { it > 0 },
503
+ backgroundLane.size.takeIf { it > 0 },
504
+ frontLane.size.takeIf { it > 0 }
505
+ ).minOrNull() ?: 0
506
+
507
+ if (resolvedCount <= 0) {
508
+ return null
509
+ }
510
+
511
+ val normalizedBackground = (0 until resolvedCount).map { index ->
512
+ backgroundLane.getOrNull(index) ?: 255
513
+ }
514
+ val normalizedFront = (0 until resolvedCount).map { index ->
515
+ frontLane.getOrNull(index) ?: 255
516
+ }
517
+
518
+ return mapOf(
519
+ "laneCount" to resolvedCount,
520
+ "backgroundLane" to normalizedBackground,
521
+ "frontLane" to normalizedFront
522
+ )
523
+ }
524
+
525
+ private fun resolveNextTurnIconType(currentSegmentIndex: Int): Int? {
526
+ if (currentSegmentIndex < 0) {
527
+ return null
528
+ }
529
+
530
+ val steps = aMapNavi?.naviPath?.steps ?: return null
531
+ val nextSegmentIndex = currentSegmentIndex + 1
532
+ if (nextSegmentIndex !in steps.indices) {
533
+ return null
534
+ }
535
+
536
+ return try {
537
+ steps[nextSegmentIndex].iconType
538
+ } catch (_: Throwable) {
539
+ null
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Android 导航 SDK 仅透出当前转向图 bitmap,且不同版本方法可见性不完全一致,
545
+ * 这里走反射兼容,避免因为 SDK 小版本差异导致编译直接中断。
546
+ */
547
+ private fun extractTurnIconBitmap(naviInfo: NaviInfo): Bitmap? {
548
+ return try {
549
+ val method = naviInfo.javaClass.getMethod("getIconBitmap")
550
+ method.invoke(naviInfo) as? Bitmap
551
+ } catch (_: NoSuchMethodException) {
552
+ if (!hasLoggedMissingTurnIconBitmapApi) {
553
+ hasLoggedMissingTurnIconBitmapApi = true
554
+ Log.w(
555
+ "ExpoGaodeMapNaviView",
556
+ "NaviInfo#getIconBitmap is unavailable in the current AMap SDK; turnIconImage will be omitted on Android"
557
+ )
558
+ }
559
+ null
560
+ } catch (error: Throwable) {
561
+ Log.w("ExpoGaodeMapNaviView", "Failed to extract turn icon bitmap", error)
562
+ null
563
+ }
564
+ }
565
+
566
+ private fun bitmapToPngBytes(bitmap: Bitmap): ByteArray? {
567
+ return try {
568
+ val outputStream = ByteArrayOutputStream()
569
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
570
+ outputStream.toByteArray()
571
+ } catch (error: Throwable) {
572
+ Log.w("ExpoGaodeMapNaviView", "Failed to serialize turn icon bitmap", error)
573
+ null
574
+ }
575
+ }
576
+
577
+ private fun sha1Hex(bytes: ByteArray): String {
578
+ val digest = MessageDigest.getInstance("SHA-1").digest(bytes)
579
+ return digest.joinToString("") { "%02x".format(it) }
580
+ }
581
+
582
+ private fun cacheTurnIconBitmap(bytes: ByteArray, prefix: String, contentHash: String): String? {
583
+ return try {
584
+ val directory = File(context.cacheDir, "expo_gaode_map_navigation_icons")
585
+ if (!directory.exists()) {
586
+ directory.mkdirs()
587
+ }
588
+ val file = File(directory, "${prefix}_${contentHash}.png")
589
+ if (!file.exists()) {
590
+ FileOutputStream(file).use { stream ->
591
+ stream.write(bytes)
592
+ }
593
+ }
594
+ Uri.fromFile(file).toString()
595
+ } catch (error: Throwable) {
596
+ Log.w("ExpoGaodeMapNaviView", "Failed to cache turn icon bitmap", error)
597
+ null
598
+ }
599
+ }
600
+
601
+ private fun deleteCachedIconFile(uriString: String?) {
602
+ if (uriString.isNullOrBlank()) {
603
+ return
604
+ }
605
+ runCatching {
606
+ val path = Uri.parse(uriString).path ?: return
607
+ File(path).delete()
608
+ }
609
+ }
610
+
611
+ private fun updateCachedTurnIconImage(naviInfo: NaviInfo): String? {
612
+ val bitmap = extractTurnIconBitmap(naviInfo)
613
+ if (bitmap == null) {
614
+ deleteCachedIconFile(cachedTurnIconImageUri)
615
+ cachedTurnIconImageUri = null
616
+ cachedTurnIconContentHash = null
617
+ return null
618
+ }
619
+
620
+ val bytes = bitmapToPngBytes(bitmap) ?: return cachedTurnIconImageUri
621
+ val contentHash = sha1Hex(bytes)
622
+ if (contentHash == cachedTurnIconContentHash && !cachedTurnIconImageUri.isNullOrBlank()) {
623
+ return cachedTurnIconImageUri
624
+ }
625
+
626
+ val nextUri = cacheTurnIconBitmap(bytes, "turn_icon", contentHash) ?: return cachedTurnIconImageUri
627
+ val previousUri = cachedTurnIconImageUri
628
+ cachedTurnIconImageUri = nextUri
629
+ cachedTurnIconContentHash = contentHash
630
+ if (previousUri != nextUri) {
631
+ deleteCachedIconFile(previousUri)
632
+ }
633
+ return nextUri
634
+ }
635
+
636
+ private fun emitTrafficStatusesUpdate(retainDistance: Int? = null) {
637
+ val trafficStatuses = try {
638
+ aMapNavi?.getTrafficStatuses(0, 0)
639
+ } catch (_: Throwable) {
640
+ null
641
+ } ?: emptyList()
642
+
643
+ val totalLength = try {
644
+ aMapNavi?.naviPath?.allLength
645
+ } catch (_: Throwable) {
646
+ null
647
+ } ?: currentRouteTotalLength
648
+
649
+ currentRouteTotalLength = totalLength ?: currentRouteTotalLength
650
+
651
+ val payload = mutableMapOf<String, Any>(
652
+ "items" to trafficStatuses.map { status ->
653
+ mapOf(
654
+ "status" to status.status,
655
+ "length" to status.length
656
+ )
657
+ }
658
+ )
659
+
660
+ if ((totalLength ?: 0) > 0) {
661
+ payload["totalLength"] = totalLength as Int
662
+ }
71
663
 
72
- private var topInsetPx: Int = 0
73
- private var overlayHooked: Boolean = false
74
- private val overlayStates = IdentityHashMap<View, OverlayState>()
664
+ if (retainDistance != null) {
665
+ payload["retainDistance"] = retainDistance
666
+ }
75
667
 
76
- private data class OverlayState(
77
- val translationY: Float,
78
- val paddingTop: Int
79
- )
668
+ onTrafficStatusesUpdate(payload)
669
+ }
80
670
 
81
671
  // 导航监听器
82
672
  @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@@ -101,7 +691,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
101
691
  }
102
692
 
103
693
  override fun onTrafficStatusUpdate() {
104
- // 交通状态更新
694
+ emitTrafficStatusesUpdate()
105
695
  }
106
696
 
107
697
  override fun onLocationChange(location: AMapNaviLocation?) {
@@ -156,6 +746,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
156
746
  // GPS 导航
157
747
  aMapNavi?.startNavi(NaviType.GPS)
158
748
  }
749
+ Handler(Looper.getMainLooper()).post {
750
+ try {
751
+ applyRouteMarkerVisibleFromState()
752
+ } catch (e: Exception) {
753
+ Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
754
+ }
755
+ }
159
756
  }
160
757
 
161
758
  override fun onCalculateRouteSuccess(result: AMapCalcRouteResult?) {
@@ -172,6 +769,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
172
769
  // GPS 导航
173
770
  aMapNavi?.startNavi(NaviType.GPS)
174
771
  }
772
+ Handler(Looper.getMainLooper()).post {
773
+ try {
774
+ applyRouteMarkerVisibleFromState()
775
+ } catch (e: Exception) {
776
+ Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
777
+ }
778
+ }
175
779
  }
176
780
 
177
781
  override fun onCalculateRouteFailure(errorCode: Int) {
@@ -214,14 +818,40 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
214
818
 
215
819
  override fun onNaviInfoUpdate(naviInfo: NaviInfo?) {
216
820
  naviInfo?.let {
217
- onNavigationInfoUpdate(mapOf(
821
+ currentRouteTotalLength = try {
822
+ aMapNavi?.naviPath?.allLength
823
+ } catch (_: Throwable) {
824
+ null
825
+ } ?: currentRouteTotalLength
826
+ val nextIconType = resolveNextTurnIconType(it.curStep)
827
+ val turnIconImage = updateCachedTurnIconImage(it)
828
+ val payload = mutableMapOf<String, Any>(
829
+ "naviMode" to it.naviType,
218
830
  "currentRoadName" to (it.currentRoadName ?: ""),
219
831
  "nextRoadName" to (it.nextRoadName ?: ""),
220
832
  "pathRetainDistance" to it.pathRetainDistance,
221
833
  "pathRetainTime" to it.pathRetainTime,
222
834
  "curStepRetainDistance" to it.curStepRetainDistance,
223
- "curStepRetainTime" to it.curStepRetainTime
224
- ))
835
+ "curStepRetainTime" to it.curStepRetainTime,
836
+ "currentSpeed" to it.currentSpeed,
837
+ "iconType" to it.iconType,
838
+ "iconDirection" to it.iconType,
839
+ "currentSegmentIndex" to it.curStep,
840
+ "currentLinkIndex" to it.curLink,
841
+ "currentPointIndex" to it.curPoint,
842
+ "routeRemainTrafficLightCount" to it.routeRemainLightCount,
843
+ "driveDistance" to 0,
844
+ "driveTime" to 0
845
+ )
846
+ if (nextIconType != null) {
847
+ payload["nextIconType"] = nextIconType
848
+ }
849
+ if (!turnIconImage.isNullOrBlank()) {
850
+ payload["turnIconImage"] = turnIconImage
851
+ }
852
+ onNavigationInfoUpdate(payload)
853
+ emitTrafficStatusesUpdate(it.pathRetainDistance)
854
+ refreshNaviUILayout("onNaviInfoUpdate")
225
855
  }
226
856
  }
227
857
 
@@ -239,30 +869,57 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
239
869
 
240
870
  override fun showCross(crossImg: AMapNaviCross?) {
241
871
  // 显示路口放大图
872
+ isCrossVisible = true
873
+ emitVisualStateUpdate()
874
+ suppressNativeTopInfoLayoutFlash()
875
+ refreshNaviUILayout("showCross")
242
876
  }
243
877
 
244
878
  override fun hideCross() {
245
879
  // 隐藏路口放大图
880
+ isCrossVisible = false
881
+ emitVisualStateUpdate()
882
+ suppressNativeTopInfoLayoutFlash()
883
+ refreshNaviUILayout("hideCross")
246
884
  }
247
885
 
248
886
  override fun showModeCross(modelCross: AMapModelCross?) {
249
887
  // 显示路口3D模型
888
+ isModeCrossVisible = true
889
+ emitVisualStateUpdate()
890
+ suppressNativeTopInfoLayoutFlash()
891
+ refreshNaviUILayout("showModeCross")
250
892
  }
251
893
 
252
894
  override fun hideModeCross() {
253
895
  // 隐藏路口3D模型
896
+ isModeCrossVisible = false
897
+ emitVisualStateUpdate()
898
+ suppressNativeTopInfoLayoutFlash()
899
+ refreshNaviUILayout("hideModeCross")
254
900
  }
255
901
 
256
902
  override fun showLaneInfo(laneInfo: AMapLaneInfo?) {
257
903
  // 显示车道信息
904
+ isLaneInfoCurrentlyVisible = true
905
+ emitVisualStateUpdate()
906
+ serializeLaneInfo(laneInfo)?.let { onLaneInfoUpdate(it) }
907
+ refreshNaviUILayout("showLaneInfo")
258
908
  }
259
909
 
260
910
  override fun showLaneInfo(laneInfos: Array<out AMapLaneInfo>?, laneBackgroundInfo: ByteArray?, laneRecommendedInfo: ByteArray?) {
261
911
  // 显示车道信息(重载方法)
912
+ isLaneInfoCurrentlyVisible = true
913
+ emitVisualStateUpdate()
914
+ serializeLaneInfo(laneInfos?.firstOrNull())?.let { onLaneInfoUpdate(it) }
915
+ refreshNaviUILayout("showLaneInfoArray")
262
916
  }
263
917
 
264
918
  override fun hideLaneInfo() {
265
919
  // 隐藏车道信息
920
+ isLaneInfoCurrentlyVisible = false
921
+ emitVisualStateUpdate()
922
+ refreshNaviUILayout("hideLaneInfo")
266
923
  }
267
924
 
268
925
  override fun notifyParallelRoad(parallelRoadType: Int) {
@@ -312,6 +969,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
312
969
 
313
970
  // 初始化导航视图
314
971
  naviView.onCreate(Bundle())
972
+ naviView.viewOptions = createInitialViewOptions()
315
973
  naviView.layoutParams = android.widget.FrameLayout.LayoutParams(
316
974
  android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
317
975
  android.widget.FrameLayout.LayoutParams.MATCH_PARENT
@@ -323,8 +981,6 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
323
981
  naviView.clipChildren = false
324
982
  naviView.clipToPadding = false
325
983
 
326
- ensureOverlayInsetHook()
327
-
328
984
  // 使用单例获取导航实例
329
985
  aMapNavi = AMapNavi.getInstance(appCtx)
330
986
  aMapNavi?.addAMapNaviListener(naviListener)
@@ -333,65 +989,8 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
333
989
  aMapNavi?.setUseInnerVoice(true, true) // 启用内置语音,并回调文字
334
990
  //设置是否为骑步行视图
335
991
  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
992
+ Log.d("ExpoGaodeMapNaviView", "导航UI配置初始化完成")
993
+ registerActiveView()
395
994
 
396
995
  naviView.post { updateTopInsetPadding() }
397
996
 
@@ -413,7 +1012,10 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
413
1012
  override fun onNextRoadClick() {}
414
1013
  override fun onScanViewButtonClick() {}
415
1014
  override fun onLockMap(isLock: Boolean) {}
416
- override fun onNaviViewLoaded() {}
1015
+ override fun onNaviViewLoaded() {
1016
+ updateNativeTopInfoLayoutVisibility()
1017
+ refreshNaviUILayout("onNaviViewLoaded")
1018
+ }
417
1019
  override fun onMapTypeChanged(mapType: Int) {}
418
1020
  })
419
1021
 
@@ -422,271 +1024,34 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
422
1024
  }
423
1025
  }
424
1026
 
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
1027
  private fun dpToPx(dp: Double): Int {
435
1028
  val density = context.resources.displayMetrics.density
436
1029
  return (dp * density + 0.5).toInt()
437
1030
  }
438
1031
 
439
1032
  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
- }
1033
+ val paddingTopPx = androidStatusBarPaddingTopDp?.let { dpToPx(it) } ?: 0
447
1034
 
448
1035
  if (lastAppliedTopPaddingPx == paddingTopPx) {
449
1036
  return
450
1037
  }
451
1038
 
452
1039
  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
- }
1040
+ if (paddingTopPx > 0) {
1041
+ naviView.setPadding(0, paddingTopPx, 0, 0)
1042
+ } else {
1043
+ naviView.setPadding(0, 0, 0, 0)
653
1044
  }
654
-
655
- return if (bestScore > 0) best else null
1045
+ naviView.requestLayout()
656
1046
  }
657
1047
 
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
1048
+ private fun applyLeaderLineSetting(options: AMapNaviViewOptions, enabled: Boolean) {
1049
+ val color = if (enabled) {
1050
+ Color.argb(160, 48, 122, 246)
1051
+ } else {
1052
+ -1
687
1053
  }
688
-
689
- return score
1054
+ options.setLeaderLineEnabled(color)
690
1055
  }
691
1056
 
692
1057
 
@@ -698,45 +1063,44 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
698
1063
 
699
1064
  fun applyShowUIElements(visible: Boolean) {
700
1065
  showUIElements = visible
701
- val options = naviView.viewOptions
702
- options.isLayoutVisible = visible
703
- naviView.viewOptions = options
1066
+ commitViewOptions { options ->
1067
+ options.isLayoutVisible = visible
1068
+ }
704
1069
  }
705
1070
 
706
1071
  fun applyAndroidTrafficBarEnabled(enabled: Boolean) {
707
1072
  androidTrafficBarEnabled = enabled
708
- val options = naviView.viewOptions
709
- options.isTrafficBarEnabled = enabled
710
- naviView.viewOptions = options
1073
+ commitViewOptions { options ->
1074
+ options.isTrafficBarEnabled = enabled
1075
+ }
711
1076
  }
712
1077
 
713
1078
  fun applyShowTrafficButton(enabled: Boolean) {
714
- isTrafficLayerEnabled = enabled
715
- val options = naviView.viewOptions
716
- options.isTrafficLayerEnabled = enabled
717
- naviView.viewOptions = options
1079
+ isTrafficButtonVisible = enabled
1080
+ commitViewOptions { options ->
1081
+ options.isTrafficLayerEnabled = enabled
1082
+ }
718
1083
  }
719
1084
 
720
1085
  fun applyShowBrowseRouteButton(enabled: Boolean) {
721
1086
  isRouteListButtonShow = enabled
722
- val options = naviView.viewOptions
723
- options.isRouteListButtonShow = enabled
724
- naviView.viewOptions = options
1087
+ commitViewOptions { options ->
1088
+ options.isRouteListButtonShow = enabled
1089
+ }
725
1090
  }
726
1091
 
727
1092
  fun applyShowGreyAfterPass(enabled: Boolean){
728
1093
  isAfterRouteAutoGray = enabled
729
- val options = naviView.viewOptions
730
- options.isAfterRouteAutoGray = enabled
731
- naviView.viewOptions = options
1094
+ commitViewOptions { options ->
1095
+ options.isAfterRouteAutoGray = enabled
1096
+ }
732
1097
  }
733
1098
 
734
1099
  fun applyShowVectorline(enabled: Boolean){
735
1100
  isVectorLineShow = enabled
736
- val options = naviView.viewOptions
737
- if(enabled)
738
- options.isLeaderLineEnabled
739
- naviView.viewOptions = options
1101
+ commitViewOptions { options ->
1102
+ applyLeaderLineSetting(options, enabled)
1103
+ }
740
1104
  }
741
1105
 
742
1106
  fun startNavigation(startLat: Double, startLng: Double, endLat: Double, endLng: Double, promise: expo.modules.kotlin.Promise) {
@@ -778,6 +1142,37 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
778
1142
  }
779
1143
  }
780
1144
 
1145
+ fun startNavigationWithIndependentPath(
1146
+ token: Int,
1147
+ routeId: Int?,
1148
+ routeIndex: Int?,
1149
+ requestedNaviType: Int?,
1150
+ promise: expo.modules.kotlin.Promise
1151
+ ) {
1152
+ try {
1153
+ val finalNaviType = requestedNaviType ?: naviType
1154
+ val result = independentRouteManager.start(context, token, finalNaviType, routeId, routeIndex)
1155
+ if (result.success) {
1156
+ promise.resolve(
1157
+ mapOf(
1158
+ "success" to true,
1159
+ "message" to result.message,
1160
+ "token" to token,
1161
+ "naviType" to finalNaviType,
1162
+ "sdkNaviType" to result.sdkNaviType,
1163
+ "routeId" to result.resolvedRouteId,
1164
+ "pathCount" to result.pathCount,
1165
+ "mainPathIndex" to result.mainPathIndex
1166
+ )
1167
+ )
1168
+ } else {
1169
+ promise.reject("START_INDEPENDENT_NAVI_FAILED", result.message, null)
1170
+ }
1171
+ } catch (e: Exception) {
1172
+ promise.reject("START_INDEPENDENT_NAVI_ERROR", e.message, e)
1173
+ }
1174
+ }
1175
+
781
1176
  fun stopNavigation(promise: expo.modules.kotlin.Promise) {
782
1177
  try {
783
1178
  aMapNavi?.stopNavi()
@@ -793,10 +1188,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
793
1188
  // Prop setters - 使用不同的方法名避免与 var 属性的自动 setter 冲突
794
1189
  fun applyShowCamera(show: Boolean) {
795
1190
  showCamera = show
796
- // 摄像头显示设置
797
- val options = naviView.viewOptions
798
- options.isCameraBubbleShow = show
799
- naviView.viewOptions = options
1191
+ commitViewOptions { options ->
1192
+ options.isCameraBubbleShow = show
1193
+ }
800
1194
  }
801
1195
 
802
1196
  fun applyNaviType(type: Int) {
@@ -830,42 +1224,125 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
830
1224
 
831
1225
  fun applyAutoLockCar(enabled: Boolean) {
832
1226
  autoLockCar = enabled
833
- val options = naviView.viewOptions
834
- options.isAutoLockCar = enabled
835
- naviView.viewOptions = options
1227
+ commitViewOptions { options ->
1228
+ options.isAutoLockCar = enabled
1229
+ }
836
1230
  }
837
1231
 
838
1232
  fun applyAutoChangeZoom(enabled: Boolean) {
839
1233
  autoChangeZoom = enabled
840
- val options = naviView.viewOptions
841
- options.isAutoChangeZoom = enabled
842
- naviView.viewOptions = options
1234
+ commitViewOptions { options ->
1235
+ options.isAutoChangeZoom = enabled
1236
+ }
843
1237
  }
844
1238
 
845
1239
  fun applyTrafficLayerEnabled(enabled: Boolean) {
846
- isTrafficLine = enabled
847
- val options = naviView.viewOptions
848
- options.isTrafficLine = enabled
849
- naviView.viewOptions = options
1240
+ isTrafficLineEnabled = enabled
1241
+ commitViewOptions { options ->
1242
+ options.isTrafficLine = enabled
1243
+ }
850
1244
  }
851
1245
 
852
1246
  fun applyRealCrossDisplay(enabled: Boolean) {
853
1247
  isRealCrossDisplayShow = enabled
854
- val options = naviView.viewOptions
855
- options.isRealCrossDisplayShow = enabled
856
- naviView.viewOptions = options
1248
+ commitViewOptions { options ->
1249
+ options.isRealCrossDisplayShow = enabled
1250
+ }
1251
+ }
1252
+
1253
+ fun applyLaneInfoVisible(enabled: Boolean) {
1254
+ isLaneInfoVisible = enabled
1255
+ commitViewOptions { options ->
1256
+ options.isLaneInfoShow = enabled
1257
+ }
1258
+ }
1259
+
1260
+ fun applyModeCrossDisplay(enabled: Boolean) {
1261
+ isModeCrossDisplayVisible = enabled
1262
+ commitViewOptions { options ->
1263
+ options.setModeCrossDisplayShow(enabled)
1264
+ }
1265
+ }
1266
+
1267
+ fun applyEyrieCrossDisplay(enabled: Boolean) {
1268
+ isEyrieCrossDisplayVisible = enabled
1269
+ commitViewOptions { options ->
1270
+ options.isEyrieCrossDisplay = enabled
1271
+ }
1272
+ }
1273
+
1274
+ fun applySecondActionVisible(enabled: Boolean) {
1275
+ isSecondActionVisible = enabled
1276
+ commitViewOptions { options ->
1277
+ options.isSecondActionVisible = enabled
1278
+ }
1279
+ }
1280
+
1281
+ fun applyBackupOverlayVisible(enabled: Boolean) {
1282
+ isBackupOverlayVisible = enabled
1283
+ commitViewOptions { options ->
1284
+ options.isDrawBackUpOverlay = enabled
1285
+ }
857
1286
  }
858
1287
 
859
1288
  fun applyShowCompassEnabled(enabled: Boolean){
860
1289
  isCompassEnabled = enabled
861
- val options = naviView.viewOptions
862
- options.isCompassEnabled = enabled
863
- naviView.viewOptions = options
1290
+ commitViewOptions { options ->
1291
+ options.isCompassEnabled = enabled
1292
+ }
1293
+ }
1294
+
1295
+ fun applyNaviStatusBarEnabled(enabled: Boolean) {
1296
+ isNaviStatusBarEnabled = enabled
1297
+ commitViewOptions { options ->
1298
+ applyNaviStatusBarEnabledCompat(options, enabled)
1299
+ }
1300
+ }
1301
+
1302
+ fun applyLockZoom(level: Int) {
1303
+ lockZoomLevel = level.coerceIn(14, 18)
1304
+ commitViewOptions { options ->
1305
+ options.zoom = lockZoomLevel
1306
+ }
1307
+ }
1308
+
1309
+ fun applyLockTilt(tilt: Int) {
1310
+ lockTilt = tilt.coerceIn(0, 60)
1311
+ commitViewOptions { options ->
1312
+ options.tilt = lockTilt
1313
+ }
1314
+ }
1315
+
1316
+ fun applyEagleMapVisible(enabled: Boolean) {
1317
+ isEagleMapVisible = enabled
1318
+ commitViewOptions { options ->
1319
+ options.isEagleMapVisible = enabled
1320
+ }
1321
+ }
1322
+
1323
+ fun applyPointToCenter(x: Double, y: Double) {
1324
+ pointToCenterX = x
1325
+ pointToCenterY = y
1326
+ commitViewOptions { options ->
1327
+ if (x > 0.0 && y > 0.0) {
1328
+ options.setPointToCenter(x, y)
1329
+ }
1330
+ }
1331
+ }
1332
+
1333
+ fun applyHideNativeTopInfoLayout(hidden: Boolean) {
1334
+ hideNativeTopInfoLayout = hidden
1335
+ updateNativeTopInfoLayoutVisibility()
1336
+ refreshNaviUILayout("applyHideNativeTopInfoLayout")
864
1337
  }
865
1338
 
866
1339
 
867
1340
  fun applyNaviMode(mode: Int) {
868
- // 0: 车头朝上 1: 正北朝上
1341
+ naviModeValue = mode
1342
+ commitViewOptions { options ->
1343
+ applyNaviModeCompat(options, mode)
1344
+ }
1345
+ // 兼容旧版接口,保持与 options 一致
869
1346
  naviView.naviMode = mode
870
1347
  }
871
1348
 
@@ -877,15 +1354,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
877
1354
  fun applyNightMode(enabled: Boolean) {
878
1355
  // 夜间模式设置 - isNightMode 属性可能不存在
879
1356
  try {
880
- val options = naviView.viewOptions
881
- if(enabled){
882
- options.setMapStyle(MapStyle.NIGHT, null)
883
- }else{
884
- options.setMapStyle(MapStyle.DAY, null)
1357
+ commitViewOptions { options ->
1358
+ if(enabled){
1359
+ options.setMapStyle(MapStyle.NIGHT, null)
1360
+ }else{
1361
+ options.setMapStyle(MapStyle.DAY, null)
1362
+ }
885
1363
  }
886
- // options.isNightMode = enabled // 该属性可能不存在
887
- // 可以通过其他方式设置夜间模式
888
- naviView.viewOptions = options
889
1364
  } catch (e: Exception) {
890
1365
  Log.e("ExpoGaodeMapNaviView", "Failed to set night mode", e)
891
1366
  }
@@ -905,6 +1380,56 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
905
1380
  }
906
1381
  }
907
1382
 
1383
+ fun applyCarImage(uri: String?) {
1384
+ updateCustomAnnotationBitmap(
1385
+ uri = uri,
1386
+ getCurrentUri = { carImageUri },
1387
+ setCurrentUri = { carImageUri = it },
1388
+ setBitmap = { customCarBitmap = it },
1389
+ reason = "carBitmap"
1390
+ )
1391
+ }
1392
+
1393
+ fun applyFourCornersImage(uri: String?) {
1394
+ updateCustomAnnotationBitmap(
1395
+ uri = uri,
1396
+ getCurrentUri = { fourCornersImageUri },
1397
+ setCurrentUri = { fourCornersImageUri = it },
1398
+ setBitmap = { customFourCornersBitmap = it },
1399
+ reason = "fourCornersBitmap"
1400
+ )
1401
+ }
1402
+
1403
+ fun applyStartPointImage(uri: String?) {
1404
+ updateCustomAnnotationBitmap(
1405
+ uri = uri,
1406
+ getCurrentUri = { startPointImageUri },
1407
+ setCurrentUri = { startPointImageUri = it },
1408
+ setBitmap = { customStartPointBitmap = it },
1409
+ reason = "startPointBitmap"
1410
+ )
1411
+ }
1412
+
1413
+ fun applyWayPointImage(uri: String?) {
1414
+ updateCustomAnnotationBitmap(
1415
+ uri = uri,
1416
+ getCurrentUri = { wayPointImageUri },
1417
+ setCurrentUri = { wayPointImageUri = it },
1418
+ setBitmap = { customWayPointBitmap = it },
1419
+ reason = "wayPointBitmap"
1420
+ )
1421
+ }
1422
+
1423
+ fun applyEndPointImage(uri: String?) {
1424
+ updateCustomAnnotationBitmap(
1425
+ uri = uri,
1426
+ getCurrentUri = { endPointImageUri },
1427
+ setCurrentUri = { endPointImageUri = it },
1428
+ setBitmap = { customEndPointBitmap = it },
1429
+ reason = "endPointBitmap"
1430
+ )
1431
+ }
1432
+
908
1433
 
909
1434
  /**
910
1435
  * 设置是否显示交通信号灯
@@ -936,8 +1461,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
936
1461
  showRouteStartIcon: Boolean,
937
1462
  showRouteEndIcon: Boolean
938
1463
  ) {
1464
+ routeMarkerShowStartEndVia = showStartEndVia
1465
+ routeMarkerShowFootFerry = showFootFerry
1466
+ routeMarkerShowForbidden = showForbidden
1467
+ routeMarkerShowRouteStartIcon = showRouteStartIcon
1468
+ routeMarkerShowRouteEndIcon = showRouteEndIcon
939
1469
  try {
940
- naviView.setRouteMarkerVisible(showStartEndVia, showFootFerry, showForbidden, showRouteStartIcon, showRouteEndIcon)
1470
+ applyRouteMarkerVisibleFromState()
941
1471
  Log.d("ExpoGaodeMapNaviView", "Route marker visibility set - startEnd:$showStartEndVia, ferry:$showFootFerry, forbidden:$showForbidden, routeStart:$showRouteStartIcon, routeEnd:$showRouteEndIcon")
942
1472
  } catch (e: Exception) {
943
1473
  Log.e("ExpoGaodeMapNaviView", "Failed to set route marker visibility", e)
@@ -985,9 +1515,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
985
1515
  fun applyNaviArrowVisible(visible: Boolean) {
986
1516
  try {
987
1517
  isNaviArrowVisible = visible
988
- val options = naviView.viewOptions
989
- options.isNaviArrowVisible = visible
990
- naviView.viewOptions = options
1518
+ commitViewOptions { options ->
1519
+ options.isNaviArrowVisible = visible
1520
+ }
991
1521
  Log.d("ExpoGaodeMapNaviView", "Navi arrow visibility set to: $visible")
992
1522
  } catch (e: Exception) {
993
1523
  Log.e("ExpoGaodeMapNaviView", "Failed to set navi arrow visibility", e)
@@ -996,9 +1526,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
996
1526
 
997
1527
  override fun onAttachedToWindow() {
998
1528
  super.onAttachedToWindow()
1529
+ registerActiveView()
999
1530
  try {
1000
- naviView.onResume()
1001
- Log.d("ExpoGaodeMapNaviView", "NaviView resumed")
1531
+ onResume()
1002
1532
  } catch (e: Exception) {
1003
1533
  Log.e("ExpoGaodeMapNaviView", "Error resuming navi view", e)
1004
1534
  }
@@ -1007,42 +1537,52 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1007
1537
  override fun onDetachedFromWindow() {
1008
1538
  super.onDetachedFromWindow()
1009
1539
  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")
1540
+ onPause()
1020
1541
  } catch (e: Exception) {
1021
- Log.e("ExpoGaodeMapNaviView", "Error destroying navi view", e)
1542
+ Log.e("ExpoGaodeMapNaviView", "Error pausing navi view", e)
1022
1543
  }
1023
1544
  }
1024
1545
 
1025
1546
  // 生命周期方法(供外部调用)
1026
1547
  fun onResume() {
1548
+ if (isDestroyed) {
1549
+ return
1550
+ }
1027
1551
  try {
1028
1552
  naviView.onResume()
1553
+ refreshNaviUILayout("onResume")
1554
+ Log.d("ExpoGaodeMapNaviView", "NaviView resumed")
1029
1555
  } catch (e: Exception) {
1030
1556
  Log.e("ExpoGaodeMapNaviView", "Error resuming navi view", e)
1031
1557
  }
1032
1558
  }
1033
1559
 
1034
1560
  fun onPause() {
1561
+ if (isDestroyed) {
1562
+ return
1563
+ }
1035
1564
  try {
1036
1565
  naviView.onPause()
1566
+ Log.d("ExpoGaodeMapNaviView", "NaviView paused")
1037
1567
  } catch (e: Exception) {
1038
1568
  Log.e("ExpoGaodeMapNaviView", "Error pausing navi view", e)
1039
1569
  }
1040
1570
  }
1041
1571
 
1042
1572
  fun onDestroy() {
1573
+ if (isDestroyed) {
1574
+ return
1575
+ }
1576
+ isDestroyed = true
1577
+ unregisterActiveView()
1043
1578
  try {
1579
+ naviView.onPause()
1044
1580
  naviView.onDestroy()
1581
+ aMapNavi?.stopSpeak()
1045
1582
  aMapNavi?.removeAMapNaviListener(naviListener)
1583
+ deleteCachedIconFile(cachedTurnIconImageUri)
1584
+ cachedTurnIconImageUri = null
1585
+ cachedTurnIconContentHash = null
1046
1586
  // AMapNavi 是单例,不需要手动 destroy
1047
1587
  } catch (e: Exception) {
1048
1588
  Log.e("ExpoGaodeMapNaviView", "Error destroying navi view", e)