expo-gaode-map-navigation 2.0.10 → 2.0.11

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 (53) hide show
  1. package/README.md +54 -2
  2. package/android/build.gradle +4 -0
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviView.kt +501 -27
  5. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNaviViewModule.kt +35 -0
  6. package/android/src/main/java/expo/modules/gaodemap/navigation/ExpoGaodeMapNavigationModule.kt +10 -23
  7. package/android/src/main/java/expo/modules/gaodemap/navigation/listeners/IndependentRouteListener.kt +24 -0
  8. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/IndependentRouteManager.kt +24 -7
  9. package/android/src/main/java/expo/modules/gaodemap/navigation/routes/drive/DriveTruckRouteCalculator.kt +22 -35
  10. package/android/src/main/java/expo/modules/gaodemap/navigation/services/IndependentRouteService.kt +45 -35
  11. package/android/src/main/java/expo/modules/gaodemap/navigation/services/NavigationForegroundService.kt +661 -0
  12. package/android/src/main/java/expo/modules/gaodemap/navigation/utils/Converters.kt +2 -2
  13. package/android/src/main/res/drawable/ic_nav_notification_small.xml +10 -0
  14. package/android/src/main/res/drawable/nav_notification_brand_icon.xml +16 -0
  15. package/android/src/main/res/drawable/navi_lane_shape_bg_center.xml +4 -4
  16. package/android/src/main/res/drawable/navi_lane_shape_bg_left.xml +7 -7
  17. package/android/src/main/res/drawable/navi_lane_shape_bg_over.xml +5 -5
  18. package/android/src/main/res/drawable/navi_lane_shape_bg_right.xml +7 -7
  19. package/android/src/main/res/drawable-nodpi/nav_tracker_car.png +0 -0
  20. package/build/ExpoGaodeMapNaviView.d.ts +9 -1
  21. package/build/ExpoGaodeMapNaviView.d.ts.map +1 -1
  22. package/build/ExpoGaodeMapNaviView.js +39 -3
  23. package/build/ExpoGaodeMapNaviView.js.map +1 -1
  24. package/build/index.d.ts +32 -6
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +32 -6
  27. package/build/index.js.map +1 -1
  28. package/build/types/independent.types.d.ts +18 -3
  29. package/build/types/independent.types.d.ts.map +1 -1
  30. package/build/types/independent.types.js.map +1 -1
  31. package/build/types/naviview.types.d.ts +49 -3
  32. package/build/types/naviview.types.d.ts.map +1 -1
  33. package/build/types/naviview.types.js.map +1 -1
  34. package/build/types/route.types.d.ts +10 -2
  35. package/build/types/route.types.d.ts.map +1 -1
  36. package/build/types/route.types.js.map +1 -1
  37. package/ios/ExpoGaodeMapNaviView.swift +1526 -246
  38. package/ios/ExpoGaodeMapNaviViewModule.swift +22 -0
  39. package/ios/ExpoGaodeMapNavigationModule.swift +6 -4
  40. package/ios/managers/IndependentRouteManager.swift +89 -26
  41. package/ios/map/ExpoGaodeMapModule.swift +25 -11
  42. package/ios/map/modules/LocationManager.swift +10 -1
  43. package/ios/map/utils/PermissionManager.swift +104 -0
  44. package/ios/routes/drive/DriveTruckRouteCalculator.swift +157 -78
  45. package/ios/routes/walkride/WalkRideRouteCalculator.swift +97 -1
  46. package/ios/services/IndependentRouteService.swift +165 -32
  47. package/ios/services/NavigationLiveActivityAttributes.swift +48 -0
  48. package/ios/services/NavigationLiveActivityManager.swift +359 -0
  49. package/package.json +2 -1
  50. package/plugin/build/withGaodeMap.d.ts +8 -0
  51. package/plugin/build/withGaodeMap.js +48 -4
  52. package/widget-template/README.md +46 -0
  53. package/widget-template/ios/NavigationLiveActivityWidget.swift +367 -0
@@ -4,7 +4,11 @@ import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.graphics.Bitmap
6
6
  import android.graphics.BitmapFactory
7
+ import android.graphics.Canvas
7
8
  import android.graphics.Color
9
+ import android.graphics.Paint
10
+ import android.graphics.RectF
11
+ import android.graphics.Typeface
8
12
  import android.net.Uri
9
13
  import android.os.Bundle
10
14
  import android.os.Handler
@@ -17,11 +21,17 @@ import com.amap.api.navi.AMapNaviListener
17
21
  import com.amap.api.navi.AMapNaviView
18
22
  import com.amap.api.navi.AMapNaviViewListener
19
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
20
28
  import com.amap.api.navi.enums.MapStyle
21
29
  import com.amap.api.navi.enums.NaviType
22
30
  import com.amap.api.navi.model.*
23
31
  import expo.modules.gaodemap.map.modules.SDKInitializer
24
32
  import expo.modules.gaodemap.navigation.managers.IndependentRouteManager
33
+ import expo.modules.gaodemap.navigation.services.NavigationForegroundService
34
+ import expo.modules.gaodemap.navigation.services.NavigationNotificationSnapshot
25
35
  import expo.modules.kotlin.AppContext
26
36
  import expo.modules.kotlin.viewevent.EventDispatcher
27
37
  import expo.modules.kotlin.views.ExpoView
@@ -32,6 +42,14 @@ import java.security.MessageDigest
32
42
  import java.util.Collections
33
43
  import java.util.WeakHashMap
34
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
+ )
35
53
 
36
54
  @SuppressLint("ViewConstructor")
37
55
  @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@@ -44,11 +62,11 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
44
62
  synchronized(activeViews) { activeViews.toList() }
45
63
 
46
64
  fun resumeActiveViews() {
47
- snapshotActiveViews().forEach { it.onResume() }
65
+ snapshotActiveViews().forEach { it.onHostActivityForeground() }
48
66
  }
49
67
 
50
68
  fun pauseActiveViews() {
51
- snapshotActiveViews().forEach { it.onPause() }
69
+ snapshotActiveViews().forEach { it.onHostActivityBackground() }
52
70
  }
53
71
 
54
72
  fun destroyActiveViews() {
@@ -98,7 +116,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
98
116
  internal var isEyrieCrossDisplayVisible: Boolean = true
99
117
  internal var isSecondActionVisible: Boolean = true
100
118
  internal var isBackupOverlayVisible: Boolean = true
101
- internal var isAfterRouteAutoGray: Boolean = false
119
+ internal var isAfterRouteAutoGray: Boolean = true
102
120
  internal var isVectorLineShow: Boolean = true
103
121
  internal var isNaviTravelView : Boolean = false
104
122
  internal var isCompassEnabled: Boolean = true
@@ -109,12 +127,19 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
109
127
  internal var pointToCenterX: Double = 0.0
110
128
  internal var pointToCenterY: Double = 0.0
111
129
  internal var hideNativeTopInfoLayout: Boolean = false
130
+ internal var androidBackgroundNavigationNotificationEnabled: Boolean = false
112
131
  internal var naviModeValue: Int = AMapNaviView.CAR_UP_MODE
132
+ internal var mapViewModeTypeValue: Int = 0
113
133
  internal var carImageUri: String? = null
134
+ internal var carImageWidthDp: Double? = null
135
+ internal var carImageHeightDp: Double? = null
114
136
  internal var fourCornersImageUri: String? = null
115
137
  internal var startPointImageUri: String? = null
116
138
  internal var wayPointImageUri: String? = null
117
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
118
143
  private val naviView: AMapNaviView by lazy(LazyThreadSafetyMode.NONE) {
119
144
  AMapNaviView(context)
120
145
  }
@@ -128,6 +153,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
128
153
  private var isModeCrossVisible = false
129
154
  private var isLaneInfoCurrentlyVisible = false
130
155
  private var currentRouteTotalLength: Int? = null
156
+ private var sourceCarBitmap: Bitmap? = null
131
157
  private var customCarBitmap: Bitmap? = null
132
158
  private var customFourCornersBitmap: Bitmap? = null
133
159
  private var customStartPointBitmap: Bitmap? = null
@@ -141,6 +167,11 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
141
167
  private var cachedTurnIconImageUri: String? = null
142
168
  private var cachedTurnIconContentHash: String? = null
143
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
144
175
 
145
176
  private fun registerActiveView() {
146
177
  synchronized(activeViews) {
@@ -154,6 +185,87 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
154
185
  }
155
186
  }
156
187
 
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
+ }
196
+
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
+
157
269
  private fun applyNaviStatusBarEnabledCompat(
158
270
  options: AMapNaviViewOptions,
159
271
  enabled: Boolean
@@ -165,10 +277,13 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
165
277
  )
166
278
  method.invoke(options, enabled)
167
279
  } catch (_: NoSuchMethodException) {
168
- Log.w(
169
- "ExpoGaodeMapNaviView",
170
- "AMapNaviViewOptions#setNaviStatusBarEnabled is unavailable in the current AMap SDK; skip applying naviStatusBarEnabled"
171
- )
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
+ }
172
287
  } catch (error: Throwable) {
173
288
  Log.w(
174
289
  "ExpoGaodeMapNaviView",
@@ -188,20 +303,114 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
188
303
  Integer.TYPE
189
304
  )
190
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)
191
333
  } catch (_: NoSuchMethodException) {
192
334
  Log.w(
193
335
  "ExpoGaodeMapNaviView",
194
- "AMapNaviViewOptions#setNaviMode is unavailable in the current AMap SDK; skip applying naviMode on options"
336
+ "AMapNaviViewOptions#setAutoNaviViewNightMode is unavailable in the current AMap SDK; skip applying auto night mode"
195
337
  )
196
338
  } catch (error: Throwable) {
197
339
  Log.w(
198
340
  "ExpoGaodeMapNaviView",
199
- "Failed to apply naviMode compatibly",
341
+ "Failed to apply auto navi night mode compatibly",
200
342
  error
201
343
  )
202
344
  }
203
345
  }
204
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
+
205
414
  private fun createInitialViewOptions(): AMapNaviViewOptions {
206
415
  return AMapNaviViewOptions().also { options ->
207
416
  applyAllViewOptions(options)
@@ -247,7 +456,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
247
456
 
248
457
  options.isAfterRouteAutoGray = isAfterRouteAutoGray
249
458
  options.isSensorEnable = true
250
- options.isAutoNaviViewNightMode = false
459
+ applyMapViewModeTypeCompat(options, mapViewModeTypeValue)
251
460
  options.isEagleMapVisible = isEagleMapVisible
252
461
  applyCustomAnnotationBitmaps(options)
253
462
  }
@@ -318,6 +527,25 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
318
527
  }
319
528
  }
320
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
+
321
549
  private fun updateCustomAnnotationBitmap(
322
550
  uri: String?,
323
551
  getCurrentUri: () -> String?,
@@ -363,6 +591,43 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
363
591
  refreshNaviUILayout("commitViewOptions")
364
592
  }
365
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
+
366
631
  private fun refreshNaviUILayout(reason: String) {
367
632
  if (isDestroyed) {
368
633
  return
@@ -374,14 +639,23 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
374
639
  }
375
640
 
376
641
  updateTopInsetPadding()
377
- naviView.requestLayout()
378
- naviView.forceLayout()
379
- naviView.invalidate()
380
- naviView.postInvalidateOnAnimation()
381
- requestLayout()
382
- invalidate()
383
- postInvalidateOnAnimation()
384
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
+
385
659
  Log.d("ExpoGaodeMapNaviView", "refreshNaviUILayout: $reason")
386
660
  }
387
661
  }
@@ -397,8 +671,12 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
397
671
  return
398
672
  }
399
673
 
674
+ if (!hideNativeTopInfoLayout) {
675
+ return
676
+ }
677
+
400
678
  val queue = ArrayDeque<View>()
401
- val targetVisibility = if (hideNativeTopInfoLayout) View.GONE else View.VISIBLE
679
+ val targetVisibility = View.GONE
402
680
  queue.add(naviView)
403
681
 
404
682
  while (queue.isNotEmpty()) {
@@ -684,6 +962,10 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
684
962
 
685
963
  override fun onStartNavi(type: Int) {
686
964
  Log.d("ExpoGaodeMapNaviView", "导航开始: type=$type")
965
+ isNavigationRunning = true
966
+ syncNavigationForegroundService("on_start_navi")
967
+ logCurrentNaviPathState("onStartNavi")
968
+ stabilizeIndependentRouteRendering("onStartNavi")
687
969
  onNavigationStarted(mapOf(
688
970
  "type" to type,
689
971
  "isEmulator" to (type == 1)
@@ -725,10 +1007,18 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
725
1007
  }
726
1008
 
727
1009
  override fun onEndEmulatorNavi() {
1010
+ isNavigationRunning = false
1011
+ syncNavigationForegroundService("on_end_emulator_navi")
1012
+ resetCustomWaypointMarkerArrivalState()
1013
+ activeIndependentRouteId = null
728
1014
  onNavigationEnded(emptyMap())
729
1015
  }
730
1016
 
731
1017
  override fun onArriveDestination() {
1018
+ isNavigationRunning = false
1019
+ syncNavigationForegroundService("on_arrive_destination")
1020
+ resetCustomWaypointMarkerArrivalState()
1021
+ activeIndependentRouteId = null
732
1022
  onArriveDestination(emptyMap())
733
1023
  }
734
1024
 
@@ -749,6 +1039,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
749
1039
  Handler(Looper.getMainLooper()).post {
750
1040
  try {
751
1041
  applyRouteMarkerVisibleFromState()
1042
+ refreshCustomWaypointMarkers("calculate-route-success")
752
1043
  } catch (e: Exception) {
753
1044
  Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
754
1045
  }
@@ -772,6 +1063,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
772
1063
  Handler(Looper.getMainLooper()).post {
773
1064
  try {
774
1065
  applyRouteMarkerVisibleFromState()
1066
+ refreshCustomWaypointMarkers("calculate-route-success-result")
775
1067
  } catch (e: Exception) {
776
1068
  Log.e("ExpoGaodeMapNaviView", "Failed to reapply route marker visibility after route success", e)
777
1069
  }
@@ -779,6 +1071,8 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
779
1071
  }
780
1072
 
781
1073
  override fun onCalculateRouteFailure(errorCode: Int) {
1074
+ isNavigationRunning = false
1075
+ syncNavigationForegroundService("calculate_route_failure_code")
782
1076
  onRouteCalculated(mapOf(
783
1077
  "success" to false,
784
1078
  "errorCode" to errorCode
@@ -786,6 +1080,8 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
786
1080
  }
787
1081
 
788
1082
  override fun onCalculateRouteFailure(result: AMapCalcRouteResult?) {
1083
+ isNavigationRunning = false
1084
+ syncNavigationForegroundService("calculate_route_failure_result")
789
1085
  onRouteCalculated(mapOf(
790
1086
  "success" to false,
791
1087
  "errorInfo" to (result?.errorDescription ?: "Unknown error")
@@ -805,6 +1101,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
805
1101
  }
806
1102
 
807
1103
  override fun onArrivedWayPoint(wayPointIndex: Int) {
1104
+ markCustomWaypointArrived(wayPointIndex)
808
1105
  onWayPointArrived(mapOf(
809
1106
  "index" to wayPointIndex
810
1107
  ))
@@ -818,11 +1115,17 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
818
1115
 
819
1116
  override fun onNaviInfoUpdate(naviInfo: NaviInfo?) {
820
1117
  naviInfo?.let {
821
- currentRouteTotalLength = try {
1118
+ val allLength = try {
822
1119
  aMapNavi?.naviPath?.allLength
823
1120
  } catch (_: Throwable) {
824
1121
  null
825
- } ?: currentRouteTotalLength
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
826
1129
  val nextIconType = resolveNextTurnIconType(it.curStep)
827
1130
  val turnIconImage = updateCachedTurnIconImage(it)
828
1131
  val payload = mutableMapOf<String, Any>(
@@ -850,6 +1153,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
850
1153
  payload["turnIconImage"] = turnIconImage
851
1154
  }
852
1155
  onNavigationInfoUpdate(payload)
1156
+ updateNavigationNotification(it)
853
1157
  emitTrafficStatusesUpdate(it.pathRetainDistance)
854
1158
  refreshNaviUILayout("onNaviInfoUpdate")
855
1159
  }
@@ -1106,6 +1410,7 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1106
1410
  fun startNavigation(startLat: Double, startLng: Double, endLat: Double, endLng: Double, promise: expo.modules.kotlin.Promise) {
1107
1411
  Log.d("ExpoGaodeMapNaviView", "startNavigation: $startLat, $startLng, $endLat, $endLng, naviType: $naviType")
1108
1412
  try {
1413
+ resetCustomWaypointMarkerArrivalState()
1109
1414
  startCoordinate = NaviLatLng(startLat, startLng)
1110
1415
  endCoordinate = NaviLatLng(endLat, endLng)
1111
1416
 
@@ -1150,9 +1455,11 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1150
1455
  promise: expo.modules.kotlin.Promise
1151
1456
  ) {
1152
1457
  try {
1458
+ resetCustomWaypointMarkerArrivalState()
1153
1459
  val finalNaviType = requestedNaviType ?: naviType
1154
1460
  val result = independentRouteManager.start(context, token, finalNaviType, routeId, routeIndex)
1155
1461
  if (result.success) {
1462
+ activeIndependentRouteId = result.resolvedRouteId
1156
1463
  promise.resolve(
1157
1464
  mapOf(
1158
1465
  "success" to true,
@@ -1166,9 +1473,11 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1166
1473
  )
1167
1474
  )
1168
1475
  } else {
1476
+ activeIndependentRouteId = null
1169
1477
  promise.reject("START_INDEPENDENT_NAVI_FAILED", result.message, null)
1170
1478
  }
1171
1479
  } catch (e: Exception) {
1480
+ activeIndependentRouteId = null
1172
1481
  promise.reject("START_INDEPENDENT_NAVI_ERROR", e.message, e)
1173
1482
  }
1174
1483
  }
@@ -1176,6 +1485,9 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1176
1485
  fun stopNavigation(promise: expo.modules.kotlin.Promise) {
1177
1486
  try {
1178
1487
  aMapNavi?.stopNavi()
1488
+ isNavigationRunning = false
1489
+ syncNavigationForegroundService("stop_navigation")
1490
+ resetCustomWaypointMarkerArrivalState()
1179
1491
  promise.resolve(mapOf(
1180
1492
  "success" to true,
1181
1493
  "message" to "导航已停止"
@@ -1193,6 +1505,12 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1193
1505
  }
1194
1506
  }
1195
1507
 
1508
+ fun applyAndroidBackgroundNavigationNotificationEnabled(enabled: Boolean) {
1509
+ androidBackgroundNavigationNotificationEnabled = enabled
1510
+ Log.d("ExpoGaodeMapNaviView", "applyAndroidBackgroundNavigationNotificationEnabled=$enabled")
1511
+ syncNavigationForegroundService("prop_update")
1512
+ }
1513
+
1196
1514
  fun applyNaviType(type: Int) {
1197
1515
  naviType = type
1198
1516
  // 导航类型会在 startNavigation 时使用
@@ -1352,19 +1670,26 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1352
1670
  }
1353
1671
 
1354
1672
  fun applyNightMode(enabled: Boolean) {
1355
- // 夜间模式设置 - isNightMode 属性可能不存在
1673
+ mapViewModeTypeValue = if (enabled) 1 else 0
1356
1674
  try {
1357
1675
  commitViewOptions { options ->
1358
- if(enabled){
1359
- options.setMapStyle(MapStyle.NIGHT, null)
1360
- }else{
1361
- options.setMapStyle(MapStyle.DAY, null)
1362
- }
1676
+ applyMapViewModeTypeCompat(options, mapViewModeTypeValue)
1363
1677
  }
1364
1678
  } catch (e: Exception) {
1365
1679
  Log.e("ExpoGaodeMapNaviView", "Failed to set night mode", e)
1366
1680
  }
1367
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
+ }
1368
1693
 
1369
1694
  /**
1370
1695
  * 设置是否显示自车和罗盘
@@ -1385,11 +1710,21 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1385
1710
  uri = uri,
1386
1711
  getCurrentUri = { carImageUri },
1387
1712
  setCurrentUri = { carImageUri = it },
1388
- setBitmap = { customCarBitmap = it },
1713
+ setBitmap = {
1714
+ sourceCarBitmap = it
1715
+ customCarBitmap = resizeCarBitmapIfNeeded(it)
1716
+ },
1389
1717
  reason = "carBitmap"
1390
1718
  )
1391
1719
  }
1392
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
+
1393
1728
  fun applyFourCornersImage(uri: String?) {
1394
1729
  updateCustomAnnotationBitmap(
1395
1730
  uri = uri,
@@ -1430,6 +1765,140 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1430
1765
  )
1431
1766
  }
1432
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
+
1433
1902
 
1434
1903
  /**
1435
1904
  * 设置是否显示交通信号灯
@@ -1574,7 +2043,12 @@ class ExpoGaodeMapNaviView(context: Context, appContext: AppContext) : ExpoView(
1574
2043
  return
1575
2044
  }
1576
2045
  isDestroyed = true
2046
+ isNavigationRunning = false
2047
+ isHostActivityInBackground = false
2048
+ latestNavigationNotificationSnapshot = null
2049
+ NavigationForegroundService.stop(context)
1577
2050
  unregisterActiveView()
2051
+ clearRenderedCustomWaypointMarkers()
1578
2052
  try {
1579
2053
  naviView.onPause()
1580
2054
  naviView.onDestroy()