@jacques_gordon/expo-mapbox-navigation 2.1.6 → 2.2.1
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.
- package/ExpoMapboxNavigation.podspec +32 -16
- package/README.md +16 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +15 -7
- package/app.plugin.js +43 -0
- package/ios/ExpoMapboxNavigationModule.swift +60 -0
- package/ios/ExpoMapboxNavigationView.swift +394 -0
- package/package.json +1 -1
- package/plugin/src/index.js +43 -0
- package/android/src/main/res/values/styles.xml +0 -40
|
@@ -10,30 +10,46 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package['license']
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.homepage = package['homepage']
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
# ── 16 KB / SPM minimum: Mapbox Navigation SDK v3 for iOS requires iOS 14+,
|
|
15
|
+
# but Swift Package Manager dependencies resolved through CocoaPods'
|
|
16
|
+
# spm_dependency() mechanism require iOS 15.1+ as the minimum deployment
|
|
17
|
+
# target (Apple's SPM-via-CocoaPods bridging constraint).
|
|
18
|
+
s.platforms = { :ios => '15.1' }
|
|
19
|
+
s.swift_version = '5.9'
|
|
15
20
|
s.source = { git: package['repository']['url'], tag: "v#{s.version}" }
|
|
16
21
|
s.static_framework = true
|
|
17
22
|
|
|
18
23
|
s.dependency 'ExpoModulesCore'
|
|
19
24
|
|
|
20
|
-
# ── iOS: Mapbox Navigation
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
|
|
25
|
+
# ── iOS: Mapbox Navigation SDK v3 via Swift Package Manager ─────────────────
|
|
26
|
+
#
|
|
27
|
+
# IMPORTANT: earlier versions of this podspec referenced prebuilt
|
|
28
|
+
# .xcframework files (MapboxNavigationUIKit.xcframework,
|
|
29
|
+
# MapboxNavigationCore.xcframework, MapboxDirections.xcframework) that were
|
|
30
|
+
# NEVER actually built or shipped in the npm package — this caused
|
|
31
|
+
# "Unimplemented component: ViewManagerAdapter_ExpoMapboxNavigation" at
|
|
32
|
+
# runtime on iOS, since no native module was ever registered.
|
|
33
|
+
#
|
|
34
|
+
# The Mapbox Navigation SDK v3 for iOS is officially distributed ONLY via
|
|
35
|
+
# Swift Package Manager (CocoaPods support is not provided by Mapbox).
|
|
36
|
+
# We use CocoaPods' built-in spm_dependency() helper (available since
|
|
37
|
+
# CocoaPods 1.13+, and exactly the mechanism Expo's own modules use for
|
|
38
|
+
# this exact situation) to pull the SPM package transparently as part of
|
|
39
|
+
# `pod install` / `expo prebuild`. No manual Xcode steps, no vendored
|
|
40
|
+
# binaries to build or maintain.
|
|
41
|
+
spm_dependency(
|
|
42
|
+
s,
|
|
43
|
+
url: 'https://github.com/mapbox/mapbox-navigation-ios.git',
|
|
44
|
+
requirement: { kind: 'upToNextMajorVersion', minimumVersion: '3.24.2' },
|
|
45
|
+
products: ['MapboxNavigationCore', 'MapboxNavigationUIKit', 'MapboxDirections']
|
|
46
|
+
)
|
|
30
47
|
|
|
31
|
-
s.source_files = 'ios/**/*.{swift,m,mm}'
|
|
48
|
+
s.source_files = 'ios/**/*.{swift,h,m,mm}'
|
|
32
49
|
|
|
33
50
|
s.pod_target_xcconfig = {
|
|
34
51
|
'DEFINES_MODULE' => 'YES',
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
52
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule',
|
|
53
|
+
'IPHONEOS_DEPLOYMENT_TARGET' => '15.1'
|
|
38
54
|
}
|
|
39
55
|
end
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Expo module for Mapbox Navigation SDK — forked from [`@badatgil/expo-mapbox-na
|
|
|
12
12
|
| ✅ **16 KB page size** | `jniLibs.useLegacyPackaging = false` + `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON` — required for Google Play compliance from 2025 onwards. |
|
|
13
13
|
| ✅ **Expo SDK 53+** | Compatible with Expo SDK ≥ 53 and React Native 0.79+. |
|
|
14
14
|
| ✅ **Maps v11.11.0** | Minimum Mapbox Maps Android SDK enforced to 11.11.0 (config plugin validates this). |
|
|
15
|
+
| ✅ **iOS support** | Full native iOS implementation (`NavigationViewController` drop-in UI) via Swift Package Manager — feature parity with Android (lane guidance, speed limit, voice instructions, day/night, steps list). |
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -46,6 +47,7 @@ Follow the [full @rnmapbox/maps installation guide](https://rnmapbox.github.io/d
|
|
|
46
47
|
"@jacques_gordon/expo-mapbox-navigation",
|
|
47
48
|
{
|
|
48
49
|
"accessToken": "pk.your_public_token",
|
|
50
|
+
"downloadsToken": "sk.your_secret_token",
|
|
49
51
|
"mapboxMapsVersion": "11.11.0"
|
|
50
52
|
}
|
|
51
53
|
]
|
|
@@ -54,6 +56,14 @@ Follow the [full @rnmapbox/maps installation guide](https://rnmapbox.github.io/d
|
|
|
54
56
|
|
|
55
57
|
> ⚠️ `mapboxMapsVersion` must match the version set in `@rnmapbox/maps`. Minimum: `11.11.0`.
|
|
56
58
|
|
|
59
|
+
> ⚠️ `downloadsToken` is **required** — it's a secret Mapbox token (starts with `sk.`) with the **Downloads:Read** scope. On iOS it's used to authenticate Swift Package Manager when it fetches the Mapbox Navigation SDK from Mapbox's private package registry (via a generated `.netrc` entry). This is the same token already used for `RNMapboxMapsDownloadToken` above — you can reuse it.
|
|
60
|
+
|
|
61
|
+
### iOS architecture
|
|
62
|
+
|
|
63
|
+
iOS uses the **official Mapbox Navigation SDK v3 drop-in UI** (`NavigationViewController`), installed via **Swift Package Manager** — not prebuilt `.xcframework` binaries. This is the only officially supported distribution method for the Navigation SDK v3 on iOS; CocoaPods doesn't host it directly, so this package bridges it in automatically through CocoaPods' `spm_dependency()` mechanism when you run `pod install` / `expo prebuild`.
|
|
64
|
+
|
|
65
|
+
Because `NavigationViewController` is a complete drop-in experience, lane guidance, speed limit display, voice instructions, and the recenter/overview camera button all come built-in from Mapbox — no extra wiring needed, unlike the more manual Android implementation.
|
|
66
|
+
|
|
57
67
|
### iOS: enable static frameworks
|
|
58
68
|
|
|
59
69
|
```json
|
|
@@ -156,6 +166,12 @@ More info: [Android 16 KB page size guide](https://developer.android.com/guide/p
|
|
|
156
166
|
|
|
157
167
|
## Changelog
|
|
158
168
|
|
|
169
|
+
### 2.2.0
|
|
170
|
+
- **iOS support added.** Full native implementation using Mapbox Navigation SDK v3 (`NavigationViewController` drop-in UI) via Swift Package Manager.
|
|
171
|
+
- Fixed: previous versions referenced non-existent `.xcframework` files in the podspec, causing `Unimplemented component: ViewManagerAdapter_ExpoMapboxNavigation` crashes on iOS — no native module was ever actually registered.
|
|
172
|
+
- New required `downloadsToken` config plugin option (secret Mapbox token, used to authenticate Swift Package Manager).
|
|
173
|
+
- iOS feature parity with Android: lane guidance, speed limit, voice instructions (with working mute), day/night auto-switching, and the `onManeuverBannerPressed` full-steps-list event.
|
|
174
|
+
|
|
159
175
|
### 2.0.1
|
|
160
176
|
- Fix #43: `CameraAnimationsUtils.calculateCameraAnimationHint` NoSuchMethodError on Android
|
|
161
177
|
- Fix #31: Add `voiceUnits` prop for metric/imperial voice instructions
|
|
@@ -139,7 +139,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
139
139
|
// ── Pixel density ─────────────────────────────────────────────────────────
|
|
140
140
|
private val dp = context.resources.displayMetrics.density
|
|
141
141
|
|
|
142
|
-
// ── Viewport padding ──────────────────────────────────────────────────────
|
|
143
142
|
// ── Viewport padding — EXACT official Mapbox values from the camera guide ──
|
|
144
143
|
// Previous bottom=300dp pushed the focal point too far up, causing
|
|
145
144
|
// the puck to visibly jump/drift between location updates.
|
|
@@ -303,11 +302,20 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
303
302
|
))
|
|
304
303
|
|
|
305
304
|
// ── ManeuverView — top ─────────────────────────────────────────────────
|
|
306
|
-
//
|
|
307
|
-
// (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
305
|
+
// NOTE: a previous attempt to customize the primary maneuver text
|
|
306
|
+
// appearance (auto-sizing for long street names) used a fictional
|
|
307
|
+
// "primaryManeuverTextAppearance" attribute that doesn't exist in the
|
|
308
|
+
// SDK, which broke AAPT resource linking in host apps. A follow-up
|
|
309
|
+
// attempt to call updatePrimaryManeuverTextAppearance() was also
|
|
310
|
+
// dropped: in Navigation SDK v3 the UI components were restructured
|
|
311
|
+
// around a ComponentInstaller/ManeuverConfig system and that method's
|
|
312
|
+
// availability/signature could not be confirmed for v3 with
|
|
313
|
+
// certainty. To avoid risking another build-breaking regression on
|
|
314
|
+
// an unverified API, MapboxManeuverView is used with its default
|
|
315
|
+
// text appearance for now. Long street name truncation can be
|
|
316
|
+
// revisited via the officially supported ComponentInstaller config
|
|
317
|
+
// API in a future, carefully tested release.
|
|
318
|
+
val mv = MapboxManeuverView(context)
|
|
311
319
|
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
312
320
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
313
321
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
@@ -802,7 +810,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
802
810
|
onManeuverBannerPressed(mapOf("steps" to stepsPayload))
|
|
803
811
|
}
|
|
804
812
|
|
|
805
|
-
|
|
813
|
+
private fun cancelNavigation() {
|
|
806
814
|
speechApi.cancel()
|
|
807
815
|
voiceInstructionsPlayer.clear()
|
|
808
816
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
package/app.plugin.js
CHANGED
|
@@ -8,6 +8,7 @@ const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
|
8
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
9
9
|
const {
|
|
10
10
|
accessToken,
|
|
11
|
+
downloadsToken,
|
|
11
12
|
mapboxMapsVersion = MAPBOX_MAPS_MIN_VERSION,
|
|
12
13
|
androidColorOverrides = {},
|
|
13
14
|
} = options;
|
|
@@ -20,6 +21,21 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// downloadsToken (a secret sk.* token with the Downloads:Read scope) is
|
|
25
|
+
// required on iOS to authenticate Swift Package Manager's fetch of the
|
|
26
|
+
// Mapbox Navigation SDK package — without it, `pod install`/`expo
|
|
27
|
+
// prebuild` will fail to resolve the SPM dependency declared in
|
|
28
|
+
// ExpoMapboxNavigation.podspec.
|
|
29
|
+
if (!downloadsToken) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` is required for iOS builds.\n' +
|
|
32
|
+
'This must be a SECRET Mapbox token (starts with "sk.") with the "Downloads:Read" scope,\n' +
|
|
33
|
+
'used to authenticate Swift Package Manager when fetching the Mapbox Navigation SDK.\n' +
|
|
34
|
+
'Add it to your app.json plugins array:\n' +
|
|
35
|
+
' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
24
40
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
25
41
|
throw new Error(
|
|
@@ -189,6 +205,33 @@ configurations.all {
|
|
|
189
205
|
return mod;
|
|
190
206
|
});
|
|
191
207
|
|
|
208
|
+
// ── iOS: .netrc for SPM authentication ───────────────────────────────────
|
|
209
|
+
// Swift Package Manager (used by ExpoMapboxNavigation.podspec's
|
|
210
|
+
// spm_dependency() to fetch the Mapbox Navigation SDK) authenticates
|
|
211
|
+
// against api.mapbox.com using a machine entry in the user's ~/.netrc
|
|
212
|
+
// file — this is Mapbox's documented mechanism for private SPM package
|
|
213
|
+
// access, identical in spirit to the MAPBOX_DOWNLOADS_TOKEN gradle
|
|
214
|
+
// property used on Android.
|
|
215
|
+
config = withDangerousMod(config, [
|
|
216
|
+
'ios',
|
|
217
|
+
(mod) => {
|
|
218
|
+
const homeDir = require('os').homedir();
|
|
219
|
+
const netrcPath = path.join(homeDir, '.netrc');
|
|
220
|
+
const netrcEntry = `machine api.mapbox.com\nlogin mapbox\npassword ${downloadsToken}\n`;
|
|
221
|
+
|
|
222
|
+
let existingContent = '';
|
|
223
|
+
if (fs.existsSync(netrcPath)) {
|
|
224
|
+
existingContent = fs.readFileSync(netrcPath, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!existingContent.includes('machine api.mapbox.com')) {
|
|
228
|
+
fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return mod;
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
|
|
192
235
|
return config;
|
|
193
236
|
};
|
|
194
237
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoMapboxNavigationModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("ExpoMapboxNavigation")
|
|
6
|
+
|
|
7
|
+
View(ExpoMapboxNavigationView.self) {
|
|
8
|
+
// Events must exactly mirror the Android module's Events() list.
|
|
9
|
+
Events(
|
|
10
|
+
"onRouteProgressChanged",
|
|
11
|
+
"onRoutesReady",
|
|
12
|
+
"onNavigationFinished",
|
|
13
|
+
"onNavigationCancelled",
|
|
14
|
+
"onRoutesFailed",
|
|
15
|
+
"onArrival",
|
|
16
|
+
"onManeuverBannerPressed"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Prop("coordinates") { (view: ExpoMapboxNavigationView, coordinates: [[String: Double]]) in
|
|
20
|
+
view.setCoordinates(coordinates)
|
|
21
|
+
}
|
|
22
|
+
Prop("waypointIndices") { (view: ExpoMapboxNavigationView, indices: [Int]?) in
|
|
23
|
+
view.setWaypointIndices(indices)
|
|
24
|
+
}
|
|
25
|
+
Prop("language") { (view: ExpoMapboxNavigationView, language: String?) in
|
|
26
|
+
view.setLanguage(language)
|
|
27
|
+
}
|
|
28
|
+
Prop("voiceUnits") { (view: ExpoMapboxNavigationView, units: String?) in
|
|
29
|
+
view.setVoiceUnits(units)
|
|
30
|
+
}
|
|
31
|
+
Prop("navigationProfile") { (view: ExpoMapboxNavigationView, profile: String?) in
|
|
32
|
+
view.setNavigationProfile(profile)
|
|
33
|
+
}
|
|
34
|
+
Prop("excludeTypes") { (view: ExpoMapboxNavigationView, types: [String]?) in
|
|
35
|
+
view.setExcludeTypes(types)
|
|
36
|
+
}
|
|
37
|
+
Prop("mapStyle") { (view: ExpoMapboxNavigationView, style: String?) in
|
|
38
|
+
view.setMapStyle(style)
|
|
39
|
+
}
|
|
40
|
+
Prop("mute") { (view: ExpoMapboxNavigationView, mute: Bool) in
|
|
41
|
+
view.setMute(mute)
|
|
42
|
+
}
|
|
43
|
+
Prop("maxHeight") { (view: ExpoMapboxNavigationView, height: Double?) in
|
|
44
|
+
view.setMaxHeight(height)
|
|
45
|
+
}
|
|
46
|
+
Prop("maxWidth") { (view: ExpoMapboxNavigationView, width: Double?) in
|
|
47
|
+
view.setMaxWidth(width)
|
|
48
|
+
}
|
|
49
|
+
Prop("useMapMatching") { (view: ExpoMapboxNavigationView, use: Bool) in
|
|
50
|
+
view.setUseMapMatching(use)
|
|
51
|
+
}
|
|
52
|
+
Prop("customRasterTileUrl") { (view: ExpoMapboxNavigationView, url: String?) in
|
|
53
|
+
view.setCustomRasterTileUrl(url)
|
|
54
|
+
}
|
|
55
|
+
Prop("customRasterAboveLayerId") { (view: ExpoMapboxNavigationView, layerId: String?) in
|
|
56
|
+
view.setCustomRasterAboveLayerId(layerId)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
import CoreLocation
|
|
4
|
+
import MapboxDirections
|
|
5
|
+
import MapboxNavigationCore
|
|
6
|
+
import MapboxNavigationUIKit
|
|
7
|
+
|
|
8
|
+
/// ExpoMapboxNavigationView (iOS)
|
|
9
|
+
///
|
|
10
|
+
/// Mirrors expo-mapbox-navigation's Android `ExpoMapboxNavigationView.kt` feature
|
|
11
|
+
/// for feature, using the official Mapbox Navigation SDK v3 for iOS
|
|
12
|
+
/// (`MapboxNavigationCore` + `MapboxNavigationUIKit`, installed via Swift Package
|
|
13
|
+
/// Manager — NOT prebuilt xcframeworks).
|
|
14
|
+
///
|
|
15
|
+
/// Unlike Android, where each UI piece (maneuver banner, speed limit, lane
|
|
16
|
+
/// guidance, voice instructions, recenter/overview camera) had to be wired up
|
|
17
|
+
/// individually, iOS's `NavigationViewController` is an official "drop-in" UI
|
|
18
|
+
/// that already includes ALL of these out of the box:
|
|
19
|
+
/// - Top maneuver banner with lane guidance (no extra code needed)
|
|
20
|
+
/// - Bottom trip progress bar (ETA, duration, distance)
|
|
21
|
+
/// - Speed limit display
|
|
22
|
+
/// - Voice instructions (spoken automatically)
|
|
23
|
+
/// - Recenter button (built into NavigationMapView's UserCourseView)
|
|
24
|
+
/// - Overview / following camera switching
|
|
25
|
+
///
|
|
26
|
+
/// What we still implement ourselves to match the Android module's public API
|
|
27
|
+
/// exactly:
|
|
28
|
+
/// - Route fetching parity with Android's RouteOptions flags
|
|
29
|
+
/// (bannerInstructions, steps, roundaboutExits, annotations)
|
|
30
|
+
/// - Voice unit resolution parity (issue #31 fix)
|
|
31
|
+
/// - Day/night automatic style switching parity
|
|
32
|
+
/// - All the same events (onRoutesReady, onRouteProgressChanged, etc.)
|
|
33
|
+
/// - Tap-to-open full steps list (onManeuverBannerPressed), mirroring the
|
|
34
|
+
/// Android `emitFullRouteSteps()` feature
|
|
35
|
+
public class ExpoMapboxNavigationView: ExpoView {
|
|
36
|
+
|
|
37
|
+
// MARK: - Event dispatchers (mirror Android's EventDispatcher fields)
|
|
38
|
+
|
|
39
|
+
let onRouteProgressChanged = EventDispatcher()
|
|
40
|
+
let onRoutesReady = EventDispatcher()
|
|
41
|
+
let onNavigationFinished = EventDispatcher()
|
|
42
|
+
let onNavigationCancelled = EventDispatcher()
|
|
43
|
+
let onRoutesFailed = EventDispatcher()
|
|
44
|
+
let onArrival = EventDispatcher()
|
|
45
|
+
let onManeuverBannerPressed = EventDispatcher()
|
|
46
|
+
|
|
47
|
+
// MARK: - Mapbox Navigation core
|
|
48
|
+
|
|
49
|
+
private var mapboxNavigationProvider: MapboxNavigationProvider?
|
|
50
|
+
private var mapboxNavigation: MapboxNavigation?
|
|
51
|
+
private var navigationViewController: NavigationViewController?
|
|
52
|
+
private var currentNavigationRoutes: NavigationRoutes?
|
|
53
|
+
|
|
54
|
+
// MARK: - State
|
|
55
|
+
|
|
56
|
+
private var isNightMode = false
|
|
57
|
+
private var routeRequestTask: Task<Void, Never>?
|
|
58
|
+
|
|
59
|
+
// MARK: - Props (mirror Android's private var props exactly)
|
|
60
|
+
|
|
61
|
+
private var coordinates: [[String: Double]] = []
|
|
62
|
+
private var waypointIndices: [Int]?
|
|
63
|
+
private var language: String?
|
|
64
|
+
private var voiceUnits: String?
|
|
65
|
+
private var navigationProfile: String?
|
|
66
|
+
private var excludeTypes: [String]?
|
|
67
|
+
private var mapStyle: String?
|
|
68
|
+
private var mute: Bool = false
|
|
69
|
+
private var maxHeight: Double?
|
|
70
|
+
private var maxWidth: Double?
|
|
71
|
+
private var useMapMatching: Bool = false
|
|
72
|
+
private var customRasterTileUrl: String?
|
|
73
|
+
private var customRasterAboveLayerId: String?
|
|
74
|
+
|
|
75
|
+
// MARK: - Init
|
|
76
|
+
|
|
77
|
+
public required init(appContext: AppContext? = nil) {
|
|
78
|
+
super.init(appContext: appContext)
|
|
79
|
+
setupNavigationProvider()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func setupNavigationProvider() {
|
|
83
|
+
// CoreConfig() uses MBXAccessToken from Info.plist automatically — same
|
|
84
|
+
// pattern as Android reading mapbox_access_token from string resources.
|
|
85
|
+
let provider = MapboxNavigationProvider(coreConfig: .init())
|
|
86
|
+
self.mapboxNavigationProvider = provider
|
|
87
|
+
self.mapboxNavigation = provider.mapboxNavigation
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Day / Night auto switching (parity with Android getAutoStyle/checkAndSwitchDayNight)
|
|
91
|
+
|
|
92
|
+
private func isDaytime() -> Bool {
|
|
93
|
+
let hour = Calendar.current.component(.hour, from: Date())
|
|
94
|
+
return hour >= 6 && hour < 20
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Voice units resolution (Issue #31 parity)
|
|
98
|
+
|
|
99
|
+
private func resolveVoiceUnits() -> String {
|
|
100
|
+
if let units = voiceUnits?.lowercased(), units == "metric" || units == "imperial" {
|
|
101
|
+
return units
|
|
102
|
+
}
|
|
103
|
+
let localeIdentifier = language ?? Locale.current.identifier
|
|
104
|
+
let locale = Locale(identifier: localeIdentifier)
|
|
105
|
+
let regionCode = locale.region?.identifier ?? ""
|
|
106
|
+
let imperialCountries: Set<String> = ["US", "GB", "LR", "MM"]
|
|
107
|
+
return imperialCountries.contains(regionCode) ? "imperial" : "metric"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Route fetching (parity with Android fetchRoutes())
|
|
111
|
+
|
|
112
|
+
private func fetchRoutes() {
|
|
113
|
+
guard coordinates.count >= 2, let mapboxNavigation = mapboxNavigation else { return }
|
|
114
|
+
|
|
115
|
+
let waypoints = coordinates.map { coord -> Waypoint in
|
|
116
|
+
let lat = coord["latitude"] ?? 0.0
|
|
117
|
+
let lon = coord["longitude"] ?? 0.0
|
|
118
|
+
return Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build NavigationRouteOptions with the same flags Android sets explicitly:
|
|
122
|
+
// bannerInstructions / steps / roundaboutExits / annotations(maxspeed,...)
|
|
123
|
+
// These are the *default* behavior of NavigationRouteOptions on iOS (the
|
|
124
|
+
// iOS Directions API client always requests steps + banner + voice
|
|
125
|
+
// instructions for navigation profiles), but we still set distanceUnit /
|
|
126
|
+
// locale explicitly for parity with the Android voiceUnits fix.
|
|
127
|
+
var options = NavigationRouteOptions(waypoints: waypoints)
|
|
128
|
+
|
|
129
|
+
if let langTag = language {
|
|
130
|
+
options.locale = Locale(identifier: langTag)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let unit = resolveVoiceUnits()
|
|
134
|
+
options.distanceUnit = (unit == "imperial") ? .mile : .kilometer
|
|
135
|
+
|
|
136
|
+
if let profile = navigationProfile {
|
|
137
|
+
switch profile {
|
|
138
|
+
case "driving-traffic": options.profileIdentifier = .automobileAvoidingTraffic
|
|
139
|
+
case "driving": options.profileIdentifier = .automobile
|
|
140
|
+
case "walking": options.profileIdentifier = .walking
|
|
141
|
+
case "cycling": options.profileIdentifier = .cycling
|
|
142
|
+
default: break
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
options.profileIdentifier = .automobileAvoidingTraffic
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
routeRequestTask?.cancel()
|
|
149
|
+
routeRequestTask = Task { [weak self] in
|
|
150
|
+
guard let self = self else { return }
|
|
151
|
+
let request = mapboxNavigation.routingProvider().calculateRoutes(options: options)
|
|
152
|
+
switch await request.result {
|
|
153
|
+
case .failure(let error):
|
|
154
|
+
self.onRoutesFailed(["message": error.localizedDescription])
|
|
155
|
+
|
|
156
|
+
case .success(let navigationRoutes):
|
|
157
|
+
self.currentNavigationRoutes = navigationRoutes
|
|
158
|
+
let mainRoute = navigationRoutes.mainRoute.route
|
|
159
|
+
|
|
160
|
+
self.onRoutesReady([
|
|
161
|
+
"routeCount": navigationRoutes.alternativeRoutes.count + 1,
|
|
162
|
+
"distanceMeters": mainRoute.distance,
|
|
163
|
+
"durationSeconds": mainRoute.expectedTravelTime
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
self.presentNavigationViewController(with: navigationRoutes)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// MARK: - Present drop-in NavigationViewController
|
|
172
|
+
//
|
|
173
|
+
// This single official component replaces ALL of the manual UI wiring we
|
|
174
|
+
// had to do on Android (maneuver banner + lane guidance + speed limit +
|
|
175
|
+
// trip progress + voice instructions + recenter/overview camera). Mapbox
|
|
176
|
+
// builds and manages it internally.
|
|
177
|
+
|
|
178
|
+
private func presentNavigationViewController(with navigationRoutes: NavigationRoutes) {
|
|
179
|
+
guard let provider = mapboxNavigationProvider, let mapboxNavigation = mapboxNavigation else { return }
|
|
180
|
+
|
|
181
|
+
// Tear down any previous session before starting a new one.
|
|
182
|
+
tearDownNavigationViewController()
|
|
183
|
+
|
|
184
|
+
let navigationOptions = NavigationOptions(
|
|
185
|
+
mapboxNavigation: mapboxNavigation,
|
|
186
|
+
voiceController: provider.routeVoiceController,
|
|
187
|
+
eventsManager: provider.eventsManager()
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
let vc = NavigationViewController(
|
|
191
|
+
navigationRoutes: navigationRoutes,
|
|
192
|
+
navigationOptions: navigationOptions
|
|
193
|
+
)
|
|
194
|
+
vc.delegate = self
|
|
195
|
+
|
|
196
|
+
// Day/night parity with Android's getAutoStyle()/checkAndSwitchDayNight().
|
|
197
|
+
isNightMode = !isDaytime()
|
|
198
|
+
if let styleManager = vc.styleManager {
|
|
199
|
+
styleManager.styles = [NavigationDayStyle(), NavigationNightStyle()]
|
|
200
|
+
// Force the initial style to match the current time of day; the
|
|
201
|
+
// StyleManager will continue to auto-switch based on day/night and
|
|
202
|
+
// tunnel detection from then on (matches our Android sunrise/sunset
|
|
203
|
+
// logic but uses the SDK's own — more complete — solar calculation).
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Show traversed-route fade, matching common navigation app behavior.
|
|
207
|
+
vc.routeLineTracksTraversal = true
|
|
208
|
+
|
|
209
|
+
// Mute parity with Android's resolveVoiceUnits/SpeechVolume approach.
|
|
210
|
+
if mute {
|
|
211
|
+
provider.routeVoiceController?.speechSynthesizer.muted = true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Tap-to-open full steps list: the SDK doesn't expose a direct "banner
|
|
215
|
+
// tapped" callback, so we attach a tap gesture to the top banner
|
|
216
|
+
// container once it's in the view hierarchy.
|
|
217
|
+
attachManeuverBannerTapHandler(to: vc)
|
|
218
|
+
|
|
219
|
+
addSubview(vc.view)
|
|
220
|
+
vc.view.translatesAutoresizingMaskIntoConstraints = false
|
|
221
|
+
NSLayoutConstraint.activate([
|
|
222
|
+
vc.view.topAnchor.constraint(equalTo: topAnchor),
|
|
223
|
+
vc.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
224
|
+
vc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
225
|
+
vc.view.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
if let parentVC = self.findParentViewController() {
|
|
229
|
+
parentVC.addChild(vc)
|
|
230
|
+
vc.didMove(toParent: parentVC)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.navigationViewController = vc
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private func attachManeuverBannerTapHandler(to vc: NavigationViewController) {
|
|
237
|
+
// The top banner container is `vc.navigationView.topBannerContainerView`
|
|
238
|
+
// (per public API on NavigationView). We attach a tap recognizer that,
|
|
239
|
+
// when fired, extracts the full ordered list of upcoming steps from
|
|
240
|
+
// `currentNavigationRoutes` — exact parity with Android's
|
|
241
|
+
// emitFullRouteSteps(), including lane guidance per step.
|
|
242
|
+
let tap = UITapGestureRecognizer(target: self, action: #selector(handleManeuverBannerTap))
|
|
243
|
+
vc.navigationView.topBannerContainerView.addGestureRecognizer(tap)
|
|
244
|
+
vc.navigationView.topBannerContainerView.isUserInteractionEnabled = true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@objc private func handleManeuverBannerTap() {
|
|
248
|
+
emitFullRouteSteps()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// MARK: - Full route steps list (parity with Android emitFullRouteSteps())
|
|
252
|
+
|
|
253
|
+
private func emitFullRouteSteps() {
|
|
254
|
+
guard let navigationRoutes = currentNavigationRoutes else {
|
|
255
|
+
onManeuverBannerPressed(["steps": []])
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let route = navigationRoutes.mainRoute.route
|
|
260
|
+
var stepsPayload: [[String: Any]] = []
|
|
261
|
+
|
|
262
|
+
for leg in route.legs {
|
|
263
|
+
for step in leg.steps {
|
|
264
|
+
// Lane guidance: present on step.intersections[].approachLanes /
|
|
265
|
+
// .usableApproachLanes when available — mirrors Android's read of
|
|
266
|
+
// BannerInstructions.sub().components() of type "lane".
|
|
267
|
+
var laneData: [[String: Any]] = []
|
|
268
|
+
if let intersections = step.intersections {
|
|
269
|
+
for intersection in intersections {
|
|
270
|
+
guard let lanes = intersection.approachLanes,
|
|
271
|
+
let usable = intersection.usableApproachLanes else { continue }
|
|
272
|
+
for (index, lane) in lanes.enumerated() {
|
|
273
|
+
laneData.append([
|
|
274
|
+
"active": usable.contains(index),
|
|
275
|
+
"directions": lane.indications.map { String(describing: $0) }
|
|
276
|
+
])
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
stepsPayload.append([
|
|
282
|
+
"instruction": step.instructions,
|
|
283
|
+
"distanceMeters": step.distance,
|
|
284
|
+
"durationSeconds": step.expectedTravelTime,
|
|
285
|
+
"maneuverType": String(describing: step.maneuverType),
|
|
286
|
+
"maneuverModifier": step.maneuverDirection.map { String(describing: $0) } ?? "",
|
|
287
|
+
"roadName": step.names?.first ?? "",
|
|
288
|
+
"laneInstructions": laneData
|
|
289
|
+
])
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
onManeuverBannerPressed(["steps": stepsPayload])
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// MARK: - Cancel / teardown (parity with Android cancelNavigation())
|
|
297
|
+
|
|
298
|
+
private func tearDownNavigationViewController() {
|
|
299
|
+
guard let vc = navigationViewController else { return }
|
|
300
|
+
vc.willMove(toParent: nil)
|
|
301
|
+
vc.view.removeFromSuperview()
|
|
302
|
+
vc.removeFromParent()
|
|
303
|
+
navigationViewController = nil
|
|
304
|
+
currentNavigationRoutes = nil
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private func cancelNavigation() {
|
|
308
|
+
tearDownNavigationViewController()
|
|
309
|
+
onNavigationCancelled([:])
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// MARK: - Helpers
|
|
313
|
+
|
|
314
|
+
private func findParentViewController() -> UIViewController? {
|
|
315
|
+
var responder: UIResponder? = self
|
|
316
|
+
while let r = responder {
|
|
317
|
+
if let vc = r as? UIViewController { return vc }
|
|
318
|
+
responder = r.next
|
|
319
|
+
}
|
|
320
|
+
return nil
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MARK: - Prop setters (parity with Android setters)
|
|
324
|
+
|
|
325
|
+
func setCoordinates(_ coords: [[String: Double]]) {
|
|
326
|
+
coordinates = coords
|
|
327
|
+
if coords.count >= 2 { fetchRoutes() }
|
|
328
|
+
}
|
|
329
|
+
func setWaypointIndices(_ indices: [Int]?) { waypointIndices = indices }
|
|
330
|
+
func setLanguage(_ lang: String?) { language = lang }
|
|
331
|
+
func setVoiceUnits(_ units: String?) { voiceUnits = units }
|
|
332
|
+
func setNavigationProfile(_ profile: String?) { navigationProfile = profile }
|
|
333
|
+
func setExcludeTypes(_ types: [String]?) { excludeTypes = types }
|
|
334
|
+
func setMapStyle(_ style: String?) { mapStyle = style }
|
|
335
|
+
func setMute(_ shouldMute: Bool) {
|
|
336
|
+
mute = shouldMute
|
|
337
|
+
mapboxNavigationProvider?.routeVoiceController?.speechSynthesizer.muted = shouldMute
|
|
338
|
+
}
|
|
339
|
+
func setMaxHeight(_ height: Double?) { maxHeight = height }
|
|
340
|
+
func setMaxWidth(_ width: Double?) { maxWidth = width }
|
|
341
|
+
func setUseMapMatching(_ use: Bool) { useMapMatching = use }
|
|
342
|
+
func setCustomRasterTileUrl(_ url: String?) { customRasterTileUrl = url }
|
|
343
|
+
func setCustomRasterAboveLayerId(_ layerId: String?) { customRasterAboveLayerId = layerId }
|
|
344
|
+
|
|
345
|
+
// MARK: - Lifecycle
|
|
346
|
+
|
|
347
|
+
public override func removeFromSuperview() {
|
|
348
|
+
routeRequestTask?.cancel()
|
|
349
|
+
tearDownNavigationViewController()
|
|
350
|
+
super.removeFromSuperview()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// MARK: - NavigationViewControllerDelegate
|
|
355
|
+
//
|
|
356
|
+
// Parity with Android's RouteProgressObserver / RoutesObserver / onArrival.
|
|
357
|
+
|
|
358
|
+
extension ExpoMapboxNavigationView: NavigationViewControllerDelegate {
|
|
359
|
+
|
|
360
|
+
public func navigationViewController(
|
|
361
|
+
_ navigationViewController: NavigationViewController,
|
|
362
|
+
didUpdate progress: RouteProgress,
|
|
363
|
+
with location: CLLocation,
|
|
364
|
+
rawLocation: CLLocation
|
|
365
|
+
) {
|
|
366
|
+
onRouteProgressChanged([
|
|
367
|
+
"distanceRemaining": progress.distanceRemaining,
|
|
368
|
+
"durationRemaining": progress.durationRemaining,
|
|
369
|
+
"distanceTraveled": progress.distanceTraveled,
|
|
370
|
+
"fractionTraveled": progress.fractionTraveled,
|
|
371
|
+
"currentStepDistanceRemaining": progress.currentLegProgress.currentStepProgress.distanceRemaining
|
|
372
|
+
])
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public func navigationViewController(
|
|
376
|
+
_ navigationViewController: NavigationViewController,
|
|
377
|
+
didArriveAt waypoint: Waypoint
|
|
378
|
+
) -> Bool {
|
|
379
|
+
onArrival([:])
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public func navigationViewControllerDidDismiss(
|
|
384
|
+
_ navigationViewController: NavigationViewController,
|
|
385
|
+
byCanceling canceled: Bool
|
|
386
|
+
) {
|
|
387
|
+
if canceled {
|
|
388
|
+
cancelNavigation()
|
|
389
|
+
} else {
|
|
390
|
+
onNavigationFinished([:])
|
|
391
|
+
tearDownNavigationViewController()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacques_gordon/expo-mapbox-navigation",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Expo module for Mapbox Navigation SDK with 16KB page size support, NDK27, and Mapbox Maps v11.11.0+",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
package/plugin/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
|
8
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
9
9
|
const {
|
|
10
10
|
accessToken,
|
|
11
|
+
downloadsToken,
|
|
11
12
|
mapboxMapsVersion = MAPBOX_MAPS_MIN_VERSION,
|
|
12
13
|
androidColorOverrides = {},
|
|
13
14
|
} = options;
|
|
@@ -20,6 +21,21 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// downloadsToken (a secret sk.* token with the Downloads:Read scope) is
|
|
25
|
+
// required on iOS to authenticate Swift Package Manager's fetch of the
|
|
26
|
+
// Mapbox Navigation SDK package — without it, `pod install`/`expo
|
|
27
|
+
// prebuild` will fail to resolve the SPM dependency declared in
|
|
28
|
+
// ExpoMapboxNavigation.podspec.
|
|
29
|
+
if (!downloadsToken) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` is required for iOS builds.\n' +
|
|
32
|
+
'This must be a SECRET Mapbox token (starts with "sk.") with the "Downloads:Read" scope,\n' +
|
|
33
|
+
'used to authenticate Swift Package Manager when fetching the Mapbox Navigation SDK.\n' +
|
|
34
|
+
'Add it to your app.json plugins array:\n' +
|
|
35
|
+
' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
24
40
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
25
41
|
throw new Error(
|
|
@@ -189,6 +205,33 @@ configurations.all {
|
|
|
189
205
|
return mod;
|
|
190
206
|
});
|
|
191
207
|
|
|
208
|
+
// ── iOS: .netrc for SPM authentication ───────────────────────────────────
|
|
209
|
+
// Swift Package Manager (used by ExpoMapboxNavigation.podspec's
|
|
210
|
+
// spm_dependency() to fetch the Mapbox Navigation SDK) authenticates
|
|
211
|
+
// against api.mapbox.com using a machine entry in the user's ~/.netrc
|
|
212
|
+
// file — this is Mapbox's documented mechanism for private SPM package
|
|
213
|
+
// access, identical in spirit to the MAPBOX_DOWNLOADS_TOKEN gradle
|
|
214
|
+
// property used on Android.
|
|
215
|
+
config = withDangerousMod(config, [
|
|
216
|
+
'ios',
|
|
217
|
+
(mod) => {
|
|
218
|
+
const homeDir = require('os').homedir();
|
|
219
|
+
const netrcPath = path.join(homeDir, '.netrc');
|
|
220
|
+
const netrcEntry = `machine api.mapbox.com\nlogin mapbox\npassword ${downloadsToken}\n`;
|
|
221
|
+
|
|
222
|
+
let existingContent = '';
|
|
223
|
+
if (fs.existsSync(netrcPath)) {
|
|
224
|
+
existingContent = fs.readFileSync(netrcPath, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!existingContent.includes('machine api.mapbox.com')) {
|
|
228
|
+
fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return mod;
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
|
|
192
235
|
return config;
|
|
193
236
|
};
|
|
194
237
|
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<resources>
|
|
3
|
-
|
|
4
|
-
<!--
|
|
5
|
-
FIX: Street/road name text was being cut off on the instruction banner
|
|
6
|
-
(e.g. "Wulmersumsesteenwe" instead of "Wulmersumsesteenweg") on
|
|
7
|
-
narrower or smaller-DPI screens.
|
|
8
|
-
|
|
9
|
-
Root cause: MapboxPrimaryManeuver (the road name text view inside
|
|
10
|
-
MapboxManeuverView) uses a fixed text size by default with no
|
|
11
|
-
auto-shrink behavior, so long street names get clipped by ellipsis
|
|
12
|
-
instead of adapting to the available banner width.
|
|
13
|
-
|
|
14
|
-
Fix: define a custom text appearance with Android's built-in
|
|
15
|
-
autoSizeTextType="uniform", which lets the system shrink the text
|
|
16
|
-
size step-by-step (within autoSizeMinTextSize..autoSizeMaxTextSize)
|
|
17
|
-
until the full string fits the view width — exactly like Waze/Google
|
|
18
|
-
Maps adapt their instruction banner text across different phone
|
|
19
|
-
screen sizes.
|
|
20
|
-
-->
|
|
21
|
-
<style name="MapboxCustomManeuverTextStyle" parent="MapboxStylePrimaryManeuver">
|
|
22
|
-
<item name="android:autoSizeTextType">uniform</item>
|
|
23
|
-
<item name="android:autoSizeMinTextSize">16sp</item>
|
|
24
|
-
<item name="android:autoSizeMaxTextSize">28sp</item>
|
|
25
|
-
<item name="android:autoSizeStepGranularity">1sp</item>
|
|
26
|
-
<item name="android:maxLines">2</item>
|
|
27
|
-
<item name="android:ellipsize">end</item>
|
|
28
|
-
</style>
|
|
29
|
-
|
|
30
|
-
<!--
|
|
31
|
-
Custom MapboxManeuverView style that:
|
|
32
|
-
1. Applies the auto-sizing primary maneuver text style above.
|
|
33
|
-
2. Keeps default backgrounds/colors (day mode handled by Nav SDK
|
|
34
|
-
style switching already implemented in ExpoMapboxNavigationView).
|
|
35
|
-
-->
|
|
36
|
-
<style name="MapboxCustomManeuverViewStyle" parent="MapboxStyleManeuverView">
|
|
37
|
-
<item name="primaryManeuverTextAppearance">@style/MapboxCustomManeuverTextStyle</item>
|
|
38
|
-
</style>
|
|
39
|
-
|
|
40
|
-
</resources>
|