Beta
Navigation SDK for Android v2
All docsNavigation SDK for Android v2ExamplesImplement a basic turn-by-turn navigation experience

Implement a basic turn-by-turn navigation experience

Warning

There is a known issue with running this Navigation SDK v2 example on Android emulated devices. While our team works to correct this issue, please use a physical Android device.

Use Navigation SDK's UI components to build a custom turn-by-turn navigation UI. Long press on the map to select a destination to navigate toward.

Assets

This example uses two images:

  • mapbox_ic_start_navigation.xml to be used to start navigating
  • mapbox_ic_end_navigation.xml to be used to stop navigating

Download the images and add them to the drawable folder in your Android Studio project:

Download stop image

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.mapbox.maps.MapView
android:id="@+id/mapView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<androidx.cardview.widget.CardView
android:id="@+id/tripProgressCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardUseCompatPadding="false"
app:cardElevation="@dimen/mapbox_dimen_8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
>
<com.mapbox.navigation.ui.tripprogress.view.MapboxTripProgressView
android:id="@+id/tripProgressView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/stop"
android:layout_width="@dimen/mapbox_dimen_48dp"
android:layout_height="@dimen/mapbox_dimen_48dp"
android:layout_marginEnd="@dimen/mapbox_dimen_12dp"
android:layout_gravity="end|center_vertical"
app:srcCompat="@drawable/mapbox_ic_end_navigation"
/>
</androidx.cardview.widget.CardView>
<com.mapbox.navigation.ui.maneuver.view.MapboxManeuverView
android:id="@+id/maneuverView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/mapbox_dimen_16dp"
android:background="@color/colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:fabSize="auto"
android:src="@drawable/mapbox_ic_start_navigation"
android:visibility="gone"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.content.res.Resources
import android.location.Location
import android.os.Bundle
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mapbox.android.core.location.LocationEngineProvider
import com.mapbox.android.core.permissions.PermissionsListener
import com.mapbox.android.core.permissions.PermissionsManager
import com.mapbox.api.directions.v5.models.BannerInstructions
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.api.directions.v5.models.VoiceInstructions
import com.mapbox.geojson.Point
import com.mapbox.maps.*
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.animation.easeTo
import com.mapbox.maps.plugin.animation.getCameraAnimationsPlugin
import com.mapbox.maps.plugin.delegates.listeners.OnMapLoadErrorListener
import com.mapbox.maps.plugin.gestures.GesturesPlugin
import com.mapbox.maps.plugin.gestures.OnMapLongClickListener
import com.mapbox.maps.plugin.gestures.getGesturesPlugin
import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener
import com.mapbox.maps.plugin.locationcomponent.getLocationComponentPlugin
import com.mapbox.navigation.base.TimeFormat
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.internal.extensions.applyDefaultParams
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.trip.model.RouteProgress
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.internal.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.MapMatcherResult
import com.mapbox.navigation.core.trip.session.MapMatcherResultObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.base.model.Expected
import com.mapbox.navigation.ui.maneuver.api.ManeuverCallback
import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi
import com.mapbox.navigation.ui.maneuver.api.StepDistanceRemainingCallback
import com.mapbox.navigation.ui.maneuver.api.UpcomingManeuverListCallback
import com.mapbox.navigation.ui.maneuver.model.Maneuver
import com.mapbox.navigation.ui.maneuver.model.ManeuverError
import com.mapbox.navigation.ui.maneuver.model.StepDistance
import com.mapbox.navigation.ui.maneuver.model.StepDistanceError
import com.mapbox.navigation.ui.maneuver.view.MapboxManeuverView
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSourceOptions
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineOptions
import com.mapbox.navigation.ui.maps.route.line.model.RouteLine
import com.mapbox.navigation.ui.tripprogress.api.MapboxTripProgressApi
import com.mapbox.navigation.ui.tripprogress.view.MapboxTripProgressView
import com.mapbox.navigation.ui.tripprogress.model.DistanceRemainingFormatter
import com.mapbox.navigation.ui.tripprogress.model.EstimatedTimeToArrivalFormatter
import com.mapbox.navigation.ui.tripprogress.model.PercentDistanceTraveledFormatter
import com.mapbox.navigation.ui.tripprogress.model.TimeRemainingFormatter
import com.mapbox.navigation.ui.tripprogress.model.TripProgressUpdateFormatter
import com.mapbox.navigation.ui.utils.internal.ifNonNull
import com.mapbox.navigation.ui.voice.api.MapboxSpeechApi
import com.mapbox.navigation.ui.voice.api.MapboxVoiceInstructionsPlayer
import com.mapbox.navigation.ui.voice.api.SpeechCallback
import com.mapbox.navigation.ui.voice.api.VoiceInstructionsPlayerCallback
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
import com.mapbox.navigation.ui.voice.model.SpeechError
import com.mapbox.navigation.ui.voice.model.SpeechValue
import java.util.Locale
class MainActivity :
AppCompatActivity(),
OnMapLongClickListener {
private lateinit var mapboxMap: MapboxMap
private lateinit var mapboxNavigation: MapboxNavigation
private lateinit var locationComponent: LocationComponentPlugin
private lateinit var navigationCamera: NavigationCamera
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
private lateinit var tripProgressApi: MapboxTripProgressApi
private lateinit var maneuverApi: MapboxManeuverApi
private lateinit var speechAPI: MapboxSpeechApi
private lateinit var permissionsManager: PermissionsManager
private var isNavigating = false
private var routeLineAPI: MapboxRouteLineApi? = null
private var routeLineView: MapboxRouteLineView? = null
private var routeArrowView: MapboxRouteArrowView? = null
private var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer? = null
private val routeArrowAPI: MapboxRouteArrowApi = MapboxRouteArrowApi()
private val navigationLocationProvider = NavigationLocationProvider()
private val pixelDensity = Resources.getSystem().displayMetrics.density
private val overviewEdgeInsets: EdgeInsets by lazy {
EdgeInsets(
40.0 * pixelDensity,
40.0 * pixelDensity,
40.0 * pixelDensity,
40.0 * pixelDensity
)
}
private val landscapeOverviewEdgeInsets: EdgeInsets by lazy {
EdgeInsets(
20.0 * pixelDensity,
mapboxMap.getSize().width.toDouble() / 1.75,
20.0 * pixelDensity,
20.0 * pixelDensity
)
}
private val followingEdgeInsets: EdgeInsets by lazy {
EdgeInsets(
mapboxMap.getSize().height.toDouble() * 2.0 / 3.0,
0.0 * pixelDensity,
0.0 * pixelDensity,
0.0 * pixelDensity
)
}
private val landscapeFollowingEdgeInsets: EdgeInsets by lazy {
EdgeInsets(
mapboxMap.getSize().height.toDouble() * 2.0 / 5.0,
mapboxMap.getSize().width.toDouble() / 2.0,
0.0 * pixelDensity,
40.0 * pixelDensity
)
}
private val currentManeuverCallback = object : ManeuverCallback {
override fun onManeuver(maneuver: Expected<Maneuver, ManeuverError>) {
findViewById<MapboxManeuverView>(R.id.maneuverView)
}
}
private val stepDistanceRemainingCallback = object : StepDistanceRemainingCallback {
override fun onStepDistanceRemaining(
distanceRemaining: Expected<StepDistance, StepDistanceError>
) {
when (distanceRemaining) {
is Expected.Success -> {
findViewById<MapboxManeuverView>(R.id.maneuverView).renderDistanceRemaining(distanceRemaining.value)
}
is Expected.Failure -> {
// Not handled
}
}
}
}
private val upcomingManeuversCallback = object : UpcomingManeuverListCallback {
override fun onUpcomingManeuvers(maneuvers: Expected<List<Maneuver>, ManeuverError>) {
when (maneuvers) {
is Expected.Success -> {
findViewById<MapboxManeuverView>(R.id.maneuverView).renderUpcomingManeuvers(maneuvers.value)
}
is Expected.Failure -> {
// Not handled
}
}
}
}
private val voiceInstructionsPlayerCallback: VoiceInstructionsPlayerCallback =
object : VoiceInstructionsPlayerCallback {
override fun onDone(announcement: SpeechAnnouncement) {
speechAPI.clean(announcement)
}
}
private val speechCallback = object : SpeechCallback {
override fun onSpeech(state: Expected<SpeechValue, SpeechError>) {
when (state) {
is Expected.Success -> {
val currentSpeechValue = state.value
voiceInstructionsPlayer?.play(
currentSpeechValue.announcement,
voiceInstructionsPlayerCallback
)
}
is Expected.Failure -> {
val currentSpeechError = state.error
voiceInstructionsPlayer?.play(
currentSpeechError.fallback,
voiceInstructionsPlayerCallback
)
}
}
}
}
private val mapMatcherResultObserver = object : MapMatcherResultObserver {
override fun onNewMapMatcherResult(mapMatcherResult: MapMatcherResult) {
val transitionOptions: (ValueAnimator.() -> Unit)? = if (mapMatcherResult.isTeleport) {
{
duration = 0
}
} else {
{
duration = 1000
}
}
navigationLocationProvider.changePosition(
mapMatcherResult.enhancedLocation,
mapMatcherResult.keyPoints,
latLngTransitionOptions = transitionOptions,
bearingTransitionOptions = transitionOptions
)
viewportDataSource.onLocationChanged(mapMatcherResult.enhancedLocation)
viewportDataSource.evaluate()
if (mapMatcherResult.isTeleport) {
navigationCamera.resetFrame()
}
}
}
private val routesObserver = object : RoutesObserver {
override fun onRoutesChanged(routes: List<DirectionsRoute>) {
if (routes.isNotEmpty()) {
routeLineAPI?.setRoutes(listOf(RouteLine(routes[0], null)))?.apply {
ifNonNull(routeLineView, mapboxMap.getStyle()) { view, style ->
view.renderRouteDrawData(style, this)
}
}
viewportDataSource.onRouteChanged(routes[0])
if (!isNavigating) {
findViewById<FloatingActionButton>(R.id.start).visibility = VISIBLE
updateCameraToOverview()
}
} else {
viewportDataSource.clearRouteData()
updateCameraToIdle()
clearRouteLine()
}
}
}
private val routeProgressObserver = object : RouteProgressObserver {
override fun onRouteProgressChanged(routeProgress: RouteProgress) {
viewportDataSource.onRouteProgressChanged(routeProgress)
viewportDataSource.evaluate()
routeArrowAPI.updateUpcomingManeuverArrow(routeProgress).apply {
ifNonNull(routeArrowView, mapboxMap.getStyle()) { view, style ->
view.render(style, this)
}
}
findViewById<MapboxTripProgressView>(R.id.tripProgressView).render(tripProgressApi.getTripProgress(routeProgress))
maneuverApi.getUpcomingManeuverList(routeProgress, upcomingManeuversCallback)
ifNonNull(routeProgress.currentLegProgress) { legProgress ->
ifNonNull(legProgress.currentStepProgress) {
maneuverApi.getStepDistanceRemaining(it, stepDistanceRemainingCallback)
}
}
}
}
private val bannerInstructionsObserver = object : BannerInstructionsObserver {
override fun onNewBannerInstructions(bannerInstructions: BannerInstructions) {
if (findViewById<MapboxManeuverView>(R.id.maneuverView).visibility != VISIBLE) {
findViewById<MapboxManeuverView>(R.id.maneuverView).visibility = VISIBLE
}
maneuverApi.getManeuver(bannerInstructions, currentManeuverCallback)
}
}
private val voiceInstructionsObserver = object : VoiceInstructionsObserver {
override fun onNewVoiceInstructions(voiceInstructions: VoiceInstructions) {
speechAPI.generate(
voiceInstructions,
speechCallback
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mapboxMap = findViewById<MapView>(R.id.mapView).getMapboxMap()
checkPermissions {
locationComponent =
findViewById<MapView>(R.id.mapView).getLocationComponentPlugin().apply {
this.locationPuck = LocationPuck2D(
bearingImage = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.mapbox_navigation_puck_icon
)
)
setLocationProvider(navigationLocationProvider)
enabled = true
}
initNavigation()
viewportDataSource = MapboxNavigationViewportDataSource(
MapboxNavigationViewportDataSourceOptions.Builder().build(),
mapboxMap
)
navigationCamera = NavigationCamera(
mapboxMap,
findViewById<MapView>(R.id.mapView).getCameraAnimationsPlugin(),
viewportDataSource
)
init()
tripProgressApi = MapboxTripProgressApi(getTripProgressFormatter())
maneuverApi = MapboxManeuverApi(
MapboxDistanceFormatter(DistanceFormatterOptions.Builder(this).build())
)
speechAPI = MapboxSpeechApi(
this,
getString(R.string.mapbox_access_token),
Locale.US.language
)
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
this,
getString(R.string.mapbox_access_token),
Locale.US.language
)
}
}
private fun checkPermissions(onMapReady: () -> Unit) {
if (PermissionsManager.areLocationPermissionsGranted(this@MainActivity)) {
onMapReady()
} else {
permissionsManager = PermissionsManager(object : PermissionsListener {
override fun onExplanationNeeded(permissionsToExplain: List<String>) {
Toast.makeText(
this@MainActivity, "You need to accept location permissions.",
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionResult(granted: Boolean) {
if (granted) {
onMapReady()
} else {
this@MainActivity.finish()
}
}
})
permissionsManager.requestLocationPermissions(this@MainActivity)
}
}
override fun onStart() {
super.onStart()
findViewById<MapView>(R.id.mapView).onStart()
}
override fun onStop() {
super.onStop()
navigationCamera.resetFrame()
findViewById<MapView>(R.id.mapView).onStop()
}
override fun onDestroy() {
super.onDestroy()
findViewById<MapView>(R.id.mapView).onDestroy()
if (::mapboxNavigation.isInitialized) {
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterMapMatcherResultObserver(mapMatcherResultObserver)
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
mapboxNavigation.unregisterBannerInstructionsObserver(bannerInstructionsObserver)
}
mapboxNavigation.onDestroy()
speechAPI.cancel()
voiceInstructionsPlayer?.shutdown()
}
override fun onLowMemory() {
super.onLowMemory()
findViewById<MapView>(R.id.mapView).onLowMemory()
}
override fun onMapLongClick(point: Point): Boolean {
val currentLocation = navigationLocationProvider.lastLocation
if (currentLocation != null) {
val originPoint = Point.fromLngLat(
currentLocation.longitude,
currentLocation.latitude
)
findRoute(originPoint, point)
}
return false
}
@SuppressLint("MissingPermission")
private fun init() {
initRouteLine()
initStyle()
initViews()
mapboxNavigation.startTripSession()
}
private fun initRouteLine() {
val mapboxRouteLineOptions = MapboxRouteLineOptions.Builder(this)
.withRouteLineBelowLayerId("road-label")
.build()
routeLineAPI = MapboxRouteLineApi(mapboxRouteLineOptions)
routeLineView = MapboxRouteLineView(mapboxRouteLineOptions)
val routeArrowOptions = RouteArrowOptions.Builder(this).build()
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
}
private fun initViews() {
findViewById<FloatingActionButton>(R.id.start).setOnClickListener {
startNavigation()
findViewById<FloatingActionButton>(R.id.start).visibility = GONE
findViewById<CardView>(R.id.tripProgressCard).visibility = VISIBLE
}
findViewById<ImageView>(R.id.stop).setOnClickListener {
stopNavigation()
findViewById<MapboxManeuverView>(R.id.maneuverView).visibility = GONE
findViewById<CardView>(R.id.tripProgressCard).visibility = GONE
}
}
@SuppressLint("MissingPermission")
private fun initNavigation() {
mapboxNavigation = MapboxNavigation(
NavigationOptions.Builder(this)
.accessToken(getString(R.string.mapbox_access_token))
.locationEngine(LocationEngineProvider.getBestLocationEngine(this))
.build()
).apply {
registerLocationObserver(object : LocationObserver {
override fun onRawLocationChanged(rawLocation: Location) {
updateCameraToIdle()
val point = Point.fromLngLat(rawLocation.longitude, rawLocation.latitude)
val cameraOptions = CameraOptions.Builder()
.center(point)
.zoom(13.0)
.pitch(45.0)
.build()
mapboxMap.easeTo(cameraOptions)
navigationLocationProvider.changePosition(rawLocation)
mapboxNavigation.unregisterLocationObserver(this)
}
override fun onEnhancedLocationChanged(
enhancedLocation: Location,
keyPoints: List<Location>
) {
}
})
registerRoutesObserver(routesObserver)
registerRouteProgressObserver(routeProgressObserver)
registerMapMatcherResultObserver(mapMatcherResultObserver)
registerVoiceInstructionsObserver(voiceInstructionsObserver)
registerBannerInstructionsObserver(bannerInstructionsObserver)
}
}
@SuppressLint("MissingPermission")
private fun initStyle() {
mapboxMap.loadStyleUri(
Style.MAPBOX_STREETS,
object : Style.OnStyleLoaded {
override fun onStyleLoaded(style: Style) {
getGesturePlugin().addOnMapLongClickListener(this@MainActivity)
}
},
object : OnMapLoadErrorListener {
override fun onMapLoadError(mapViewLoadError: MapLoadError, msg: String) {
}
}
)
}
private fun findRoute(origin: Point, destination: Point) {
mapboxNavigation.requestRoutes(
RouteOptions.builder()
.applyDefaultParams()
.accessToken(getString(R.string.mapbox_access_token))
.coordinates(listOf(origin, destination))
.alternatives(true)
.build()
)
}
private fun updateCameraToOverview() {
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.overviewPaddingPropertyOverride(landscapeOverviewEdgeInsets)
} else {
viewportDataSource.overviewPaddingPropertyOverride(overviewEdgeInsets)
}
viewportDataSource.evaluate()
navigationCamera.requestNavigationCameraToOverview()
}
private fun updateCameraToFollowing() {
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.followingPaddingPropertyOverride(landscapeFollowingEdgeInsets)
} else {
viewportDataSource.followingPaddingPropertyOverride(followingEdgeInsets)
}
viewportDataSource.evaluate()
navigationCamera.requestNavigationCameraToFollowing()
}
private fun updateCameraToIdle() {
navigationCamera.requestNavigationCameraToIdle()
}
private fun startNavigation() {
isNavigating = true
updateCameraToFollowing()
}
private fun stopNavigation() {
isNavigating = false
updateCameraToIdle()
clearRouteLine()
}
private fun clearRouteLine() {
ifNonNull(routeLineAPI, routeLineView, mapboxMap.getStyle()) { api, view, style ->
view.renderClearRouteLineValue(style, api.clearRouteLine())
}
}
private fun getGesturePlugin(): GesturesPlugin {
return findViewById<MapView>(R.id.mapView).getGesturesPlugin()
}
private fun getTripProgressFormatter(): TripProgressUpdateFormatter {
return TripProgressUpdateFormatter.Builder(this)
.distanceRemainingFormatter(
DistanceRemainingFormatter(
mapboxNavigation.navigationOptions.distanceFormatterOptions
)
)
.timeRemainingFormatter(TimeRemainingFormatter(this))
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
.estimatedTimeToArrivalFormatter(
EstimatedTimeToArrivalFormatter(
this,
TimeFormat.NONE_SPECIFIED
)
).build()
}
}