Introduction
This blog post documents step-by-step procedure to implement Mapbox navigation. You will need to get a developer account on Mapbox to obtain a required access token.
Create the map
Start a new “Empty Activity” project in Android Studio.
Add Compile Option in build.gradle
in (Module:app)
within dependencies
:
implementation 'com.mapbox.mapboxsdk:mapbox-android-navigation-ui:0.42.6'
Hover mouse over each implementation to let Android update the packages to the latest. Update by pressing alt-shift-return. Now, it would be good to change minSdkVersion
to 16 to cover more applicable users.
Now update build.gradle
(Project) file, adding below lines to allprojects>
block just below jcenter()
:
maven { url 'https://mapbox.bintray.com/mapbox' }
A pop-up should appear asking if you want to sync. Click ‘Sync now’ to sync the project.
In activity_main.xml
add the below code to bring in the Mapbox map widget:
<com.mapbox.mapboxsdk.maps.MapView
android:id="@+id/mapbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
If root view is a ConstraintLayout, then you may need to add the constraints.
Now we will add permissions to AndroidManifest.xml
:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
Add the ‘coarse’ and ‘fine’ location permissions and so that Mapbox SDK can detect your location and show it on the map. As for the ‘foreground service’ permission, this is needed when you want to start the turn by turn navigation.
Now we edit the MainActivity.kt
to access the Mapbox Map by adding the below line just above the setContentView
call:
Mapbox.getInstance(this, "Your Mapbox access token")
Then we can add this code just below the setContentView
line:
mapView = findViewById<MapView>(R.id.mapbox) mapView.getMapAsync(this)
The first line attached the MapView defined in activity_main.xml
to the variable mapView
. The second line connects the variable to the actual map. At this point, the top part of MainActivity
should look like this:
class MainActivity : AppCompatActivity(), OnMapReadyCallback, PermissionsListener { private lateinit var mapView: MapView private lateinit var map: MapboxMap private var permissionsManager: PermissionsManager = PermissionsManager(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Mapbox.getInstance(this, getString(R.string.access_token)) setContentView(R.layout.activity_main) mapView = findViewById(R.id.mapbox) mapView.getMapAsync(this) }
The lateinit
keyword means the variable will be initialized later. Also, we added a couple of interfaces that will need to be implemented. The OnMapReadyCallback
requires the onMapReady()
callback; and PermissionsListener
requires onExplainationNeeded()
and onPermissionResult()
.
The onMapReady()
callback can do something like this:
override fun onMapReady(mapboxMap: MapboxMap) { Log.i("Map", "is ready") this.map = mapboxMap mapboxMap.setStyle( Style.Builder().fromUri("mapbox://styles/mapbox/streets-v11") ) { // Map is set up and the style has loaded. Now you can add data or make other map adjustments enableLocationComponent(it) } }
The callback receives a map handle that is saved in the map
variable. Then the map style is set to one of several pre-defined styles Mapbox offers. When style is loaded, the enableLocationComponent(it)
is called, with the style passed In through (it)
. The Log.i()
is just a debug print statement.
The enableLocationComponent()
functions looks like this:
@SuppressWarnings("MissingPermission") private fun enableLocationComponent(loadedMapStyle: Style) { // Check if permissions are enabled and if not request if (PermissionsManager.areLocationPermissionsGranted(this)) { // Create and customize the LocationComponent's options val customLocationComponentOptions = LocationComponentOptions.builder(this) .trackingGesturesManagement(true) .accuracyColor(ContextCompat.getColor(this, R.color.colorGreen)) .build() val locationComponentActivationOptions = LocationComponentActivationOptions.builder(this, loadedMapStyle) .locationComponentOptions(customLocationComponentOptions) .build() // Get an instance of the LocationComponent and then adjust its settings map.locationComponent.apply { // Activate the LocationComponent with options activateLocationComponent(locationComponentActivationOptions) // Enable to make the LocationComponent visible isLocationComponentEnabled = true // Set the LocationComponent's camera mode cameraMode = CameraMode.TRACKING // Set the LocationComponent's render mode renderMode = RenderMode.COMPASS } // Setup camera position val location = map.locationComponent.lastKnownLocation if (location != null) { // Setup camera position val position = CameraPosition.Builder() .target(LatLng(location.latitude, location.longitude)) .zoom(15.0) // Sets the zoom .bearing(0.0) // Rotate the camera .tilt(0.0) // Set the camera tilt .build() // Creates a CameraPosition from the builder map.animateCamera( CameraUpdateFactory .newCameraPosition(position), 1 ) } } else { permissionsManager = PermissionsManager(this) permissionsManager.requestLocationPermissions(this) }
The outer if-else statement checks if location permission is granted. If granted, the map’s locationComponent
is setup (customization), then the camera is setup for initial animated zoom in of current location (the minimum animation duration is 1). If location permission is not granted, a PermissionManager
is spun up to request for the permission.
Now let’s bring in the activity lifecycle functions:
@SuppressWarnings("MissingPermission") override fun onStart() { super.onStart() mapView.onStart() } override fun onResume() { super.onResume() mapView.onResume() } override fun onPause() { super.onPause() mapView.onPause() } override fun onStop() { super.onStop() mapView.onStop() } override fun onDestroy() { super.onDestroy() mapbox.onDestroy() } override fun onLowMemory() { super.onLowMemory() mapView.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mapView.onSaveInstanceState(outState) }
Mapbox has its own lifecycle methods for managing an Android openGL lifecycle. You must call those methods directly from the containing activity. (Note: You need to add @SuppressWarnings("MissingPermission")
above onStart
and the other methods, because these methods require you to implement permission handling. However, you don’t need that here because Mapbox handles it for you.)
Now, build and run the app to see the result. Below example uses mapbox://styles/mapbox/satellite-streets-v11
style in onMapReady()
.

Add a button
Now we need to add a button to start navigation. Select activity_main.xml
from the project view to bring up the layout view. Add a Floating Button to the view. Constrain the button to the right edge and to the bottom edge. My button creates the following entry in activity_main.xml
:
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btnStartNavigation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="76dp" android:layout_marginRight="76dp" android:layout_marginBottom="56dp" android:clickable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/ic_navigation_white_24dp" />
Noticed the id I gave to the FloatingActiveButton
is btnStartNavigation
. You can change it to something else if you like. Now we can bind the button in code by adding the lines below:
// In the MainActivity class variable area: private lateinit var btn: FloatingActionButton ... // In onCreate(), below setContentView(...), add btn = findViewById(R.id.btnStartNavigation) btn.setOnClickListener { if (currentRoute != null) { val navigationLauncherOptions = NavigationLauncherOptions.builder() //1 .directionsRoute(currentRoute) //2 .shouldSimulateRoute(true) //3 .build() NavigationLauncher.startNavigation(this, navigationLauncherOptions) //4 } }
The above code not only binds the button, but also sets the click handler, which sets up and starts the navigation using currentRoute
, the route between current location, and a destination selected by the user by clicking on a map location. Next we will implement destination selection. The variable currentRoute
needs to be declared with other variables:
private var currentRoute: DirectionsRoute? = null
Now, build and run the app. However, at this point nothing will happen when clicking on the btnStartNavigation
, because currentRoute
is still null until we assign something to it.
Destination selection
Now let’s figure out how to create a valid route and assign it to currentRoute
. We desire to have the user select a destination by clicking on the map. For this we will need to add the MapboxMap.OnMapClickListener
interface to MainActivity
, like below:
class MainActivity : AppCompatActivity(), OnMapReadyCallback, PermissionsListener, MapboxMap.OnMapClickListener { ...
Then implement the below onMapClick
function:
override fun onMapClick(point: LatLng): Boolean { checkLocation() addPlaceMarker(point) originLocation?.run { val startPoint = Point.fromLngLat(longitude, latitude) val endPoint = Point.fromLngLat(point.longitude, point.latitude) getRoute(startPoint, endPoint) } return false }
When user clicks anywhere on the map, a destination marker is displayed along with a route from origin to destination. The checkLocation()
function updates the current location; addPlaceMarker(point)
adds a destination place marker on the map.
fun checkLocation() { if (originLocation == null) { map.locationComponent.lastKnownLocation?.run { originLocation = this } } } private fun addPlaceMarker(location: LatLng) { // Add symbol at specified lat/lon val symbol = symbolManager?.create( SymbolOptions() .withLatLng(location) .withIconImage("place-marker") .withIconSize(1.0f)) }
The symbolManager
used in addPlaceMarker()
needs to be initialized in onCreate()
after the map camera setup section:
// Setup the symbol manager object symbolManager = SymbolManager(mapView, map, loadedMapStyle) // add click listeners if desired symbolManager?.addClickListener { symbol -> } symbolManager?.addLongClickListener { symbol -> } // set non-data-driven properties, such as: symbolManager?.iconAllowOverlap = true symbolManager?.iconTranslate = arrayOf(-4f, 5f) symbolManager?.iconRotationAlignment = ICON_ROTATION_ALIGNMENT_VIEWPORT // setup marker val bm: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.map_marker_light) map.style?.addImage("place-marker", bm)
Notice that the symbol itself can be clicked and have listener functions. The last two lines of code prepares the place marker for display later.
Now the last thing to implement is getRoute()
:
private fun getRoute(originPoint: Point, endPoint: Point) { NavigationRoute.builder(this) //1 .accessToken(Mapbox.getAccessToken()!!) //2 .origin(originPoint) //3 .destination(endPoint) //4 .build() //5 .getRoute(object : Callback { //6 override fun onFailure(call: Call, t: Throwable) { Log.d("MainActivity", t.localizedMessage) } override fun onResponse(call: Call, response: Response) { if (navigationMapRoute != null) { navigationMapRoute?.updateRouteVisibilityTo(false) } else { navigationMapRoute = NavigationMapRoute(null, mapView, map) } currentRoute = response.body()?.routes()?.first() if (currentRoute != null) { navigationMapRoute?.addRoute(currentRoute) } btnStartNavigation.isEnabled = true } }) }
The function basically calls the NavigationRoute.builder()
with access token, origin and destination as arguments, and handles both success and failed results. If successful, the first route is added. Note: it is possible to check for alternative routes.
Now we need to make sure all the new variables introduced are declared in MainActivity
; the all the declared variables now look like this:
private lateinit var mapView: MapView private lateinit var map: MapboxMap private lateinit var btn: FloatingActionButton private var permissionsManager: PermissionsManager = PermissionsManager(this) private var currentRoute: DirectionsRoute? = null private var originLocation: Location? = null private var navigationMapRoute: NavigationMapRoute? = null private var symbolManager: SymbolManager? = null
Now, build and run, you should see something like this:
Click on another part of the map you should see this:
Click on the navigation button you so should see:
Get the project on Github
References
- http://fabcoding.com/mapbox-tutorial-map-geolocation-markers/
- https://docs.mapbox.com/api/maps/#styles
- https://github.com/mapbox/mapbox-android-demo/tree/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/location
- https://www.raywenderlich.com/378151-mapbox-tutorial-for-android-getting-started
- https://www.youtube.com/watch?v=aS__9RbCyHg
Kindly help…
You are probably running in “simulation” mode, or don’t have a good Wi-Fi connection. Source code is provided so look into it.
This project does not show accurate location.I am using it from Pakistan and it shows US.