Skip to main content

Quick navigation

Contents

The guide takes your through the basic steps needed to create a route and begin navigation.

Before getting started, make sure your project is set up correctly, permissions are granted, and authentication is completed.

Step 1: Set the layout

In this tutorial, we are going to create a dedicated activity for the Navigation UI. As part of this, a NavigationView view will be added to the layout for rendering and visual purposes. We will just name this activity NavigationSampleActivity and the layout file ‘activity_navigation_sample’.

Inside of the layout file, add the following view to the xml.

<com.trimblemaps.navigation.ui.NavigationView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/navigation_view" />

This view is where the navigation UI and components will be attached and rendered.

Step 2: Initialize the NavigationView and some Stops

The NavigationView effectively brings the navigation experience and UI to the screen, attaching to the view we made above. In this example, it’s being set up in the activity’s OnCreate method.

First, add an instance of NavigationView as a class instance variable.

In preparation for the route calculation, create two stops—your origin and destination. (Typically, you would set the user’s current location as the first stop.)

Besides adding the coordinates of the stops manually, you can retrieve the coordinates (latitude and longitude) of a location using geocoding. For more information on how to do this, refer to the Geocoding Example.

Next, it’s time to find the NavigationView from the layout file and do some initialization.

If you use the code above, you will likely see an error with passing in this as the first parameter for navigationView.initialize(). In Step 3 of this tutorial, we will implement the onNavigationReadyCallback for this Activity. For the final step of this section, we will use the Point objects we created before to plan our trip.

At this point, the NavigationView has been set up and you’ve planned your trip. Your code should look something like this:

Next, we will need to implement some methods.

Step 3: Implementing onNavigationReadyCallback and NavigationListener

In the previous section, you will have likely received an error in your code when trying to pass this into navigationView.initialize(). To resolve this error, you’ll need to implement the OnNavigationReadyCallback in your activity. In this section, we also want to implement the NavigationListener for the activity. This will be used later.

Typically an IDE like Android Studio will help you import the methods you need into your Activity to meet the implemented classes requirements. You can also add the below yourself:

This is also a good place to implement the Android activity lifecycle methods to ensure the navigation adheres to Android’s lifecycle. See below:

Step 4: Navigate

With the methods implemented, it’s time to actually start navigation. This can be done from within the onNavigationReady method that was implemented earlier.

The code above creates the options we’ll be passing into the navigation. Most importantly, the trip we created earlier with the provider is retrieved and passed in. We also set the NavigationListener to this, which were the other methods implemented previously. Finally, navigation starts.

Before running the code in Simulator, make sure to setup the theme as below in values/themes.xml.

<resources>
   <style name="Theme.Example" parent="Theme.AppCompat" />
</resources>
Quick Navigation
At this point, we now have an app with embedded navigation taking the user from A to B.

Step 5: Set behavior when navigation is canceled

The user is now navigating to their destination, but there’s one other small piece that can be added—controlling what happens when navigation is canceled.

Should the user press the end navigation button End Navigation, this will trigger the onCancelNavigation() method that was implemented earlier. Here, code can be added to stop navigation and kill the current activity.

Step 6: Activate the trip preview and search screen buttons

Trip preview and location search buttons are displayed by default on the navigation screen. The following code demonstrates how to activate them.

Quick Navigation
Trip preview (1) and location search (2) buttons

Trip preview

/** We can set custom behavior in the onCreate for the navigation screen's trip preview button
 *  to bring the user to the current trip's trip preview.
 *  isNavigating is used to differentiate behavior while on the route and default behavior.
 *  This button can also be hidden using navigationView.hideTripPreviewButton()
 */
navigationView.retrieveTripPreviewBtn().setOnClickListener {
    val intent = Intent(this, TripPreviewActivity::class.java)
    intent.putExtra("isNavigating", true)
    startActivity(intent)
}

// Trip Preview 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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TripPreviewActivity">

    <com.trimblemaps.navigation.ui.components.trippreview.TripPreviewScreen
        android:id="@+id/trip_preview_screen"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:trimblemaps_uiCompass="false"/>

</androidx.constraintlayout.widget.ConstraintLayout>

// Trip Preview

import android.content.Intent
import android.location.Location
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.trimblemaps.api.directions.v1.models.DirectionsResponse
import com.trimblemaps.api.directions.v1.models.RouteOptions
import com.trimblemaps.api.geocoding.v1.models.TrimbleMapsLocation
import com.trimblemaps.api.poi.v1.models.PoisResponse
import com.trimblemaps.geojson.Point
import com.trimblemaps.mapsdk.geometry.LatLng
import com.trimblemaps.mapsdk.plugins.route.RouteRequestCallback
import com.trimblemaps.navigation.base.internal.extensions.LocaleEx.getUnitTypeForLocale
import com.trimblemaps.navigation.base.internal.extensions.inferDeviceLocale
import com.trimblemaps.navigation.core.TrimbleMapsTrip
import com.trimblemaps.navigation.core.TrimbleMapsTripProvider
import com.trimblemaps.navigation.core.trip.model.TripStop
import com.trimblemaps.navigation.core.trip.model.toTripStop
import com.trimblemaps.navigation.plugins.poisonroute.ui.PoisOnRouteCallback
import com.trimblemaps.navigation.ui.components.trippreview.*

// Databinding from the XML
import com.your_app_name.databinding.ActivityTripPreviewBinding

class TripPreviewActivity : AppCompatActivity() {

    private lateinit var tripPreview: TripPreviewScreen
    private var isNavigating = false

    companion object {
        var directionsResponse: DirectionsResponse? = null
    }

    private lateinit var binding: ActivityTripPreviewBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityTripPreviewBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        isNavigating = intent.getBooleanExtra("isNavigating", false)
        initViews()
        tripPreview.onCreate(savedInstanceState)
        tripPreview.setPoisOnRouteCallback(object : PoisOnRouteCallback {
            override fun onResponse(response: PoisResponse?) {
            }

            override fun onFailure(throwable: Throwable?) {
            }
        })


        val locations = intent.getSerializableExtra("locations") as? ArrayList<TrimbleMapsLocation>
        if(locations != null && !TrimbleMapsTripProvider.isActive()) {
            TrimbleMapsTripProvider.create(). apply {
                locations.forEach { stop -> addStop(stop.toTripStop())}
                options.apply {
                    language = inferDeviceLocale().toString()
                    voiceUnits = inferDeviceLocale().getUnitTypeForLocale()
                    alternatives = false
                }
            }
        }
        if (tripPreview.trip == null)
            tripPreview.trip = TrimbleMapsTripProvider.retrieve()

    }

    public override fun onResume() {
        super.onResume()
        tripPreview.onResume()
    }

    public override fun onPause() {
        super.onPause()
        tripPreview.onPause()
    }

    override fun onStart() {
        super.onStart()
        tripPreview.onStart()
    }

    override fun onStop() {
        super.onStop()
        tripPreview.onStop()
    }

    override fun onLowMemory() {
        super.onLowMemory()
        tripPreview.onLowMemory()
    }

    override fun onDestroy() {
        super.onDestroy()
        // tripPreview may not be initialized if the Activity was restarted but the OS process had been
        //  killed/restarted and we redirected to LaunchActivity
        if (::tripPreview.isInitialized) {
            tripPreview.onDestroy()
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
    }

    private fun initViews() {
        tripPreview = findViewById(R.id.trip_preview_screen)
        tripPreview.routeRequestCallback = routeRequestCallback
        tripPreview.setCallbackListeners(directionsCallback)
        tripPreview.startButtonCallback = startCallback
        tripPreview.stopListChangedCallback = stopsListChangedCallback
        tripPreview.backButtonCallback = tripPreviewCallback
    }

    private val routeRequestCallback = object : RouteRequestCallback {
        override fun onRouteReady(route: DirectionsResponse?) {
        }

        override fun onRequestFailure(throwable: Throwable, routeOptions: RouteOptions) {
        }
    }

    /**
     * User changed the stops list
     */
    private val stopsListChangedCallback = object : OnStopsListChangedCallback {

        override fun addStop(stop: TripStop) {
        }

        override fun deleteStop(index: Int) {
        }

        override fun moveStop(from: Int, to: Int) {
        }

        override fun insertStop(stop: TripStop, atIndex: Int) {
        }
    }

    private val directionsCallback = object: OnTripPreviewSummarySheetDirectionsButtonCallback {
        override fun onTripPreviewSummarySheetDirectionsButtonPressed() {}
    }

    private val startCallback = object: TripPreviewScreen.StartButtonListener {

        override fun onStartButtonPressed(trip: TrimbleMapsTrip?, isChanged: Boolean) {
            if (isChanged && trip != null)
                TrimbleMapsTripProvider.retrieve().updateTrip(trip)

            if (!isNavigating) {
                val intent = Intent(this@TripPreviewActivity, NavigationActivity::class.java)
                // Can't send this in an Intent as it is too large, just put it somewhere
                //  where NavigationActivity can get it
                directionsResponse = tripPreview.directions
                startActivity(intent)
            }
            finish()
        }
    }

    private val tripPreviewCallback = object: OnExitTripPreviewCallback {
        override fun onExit(trip: TrimbleMapsTrip?, isChanged: Boolean) {
            if (isChanged && trip != null)
                TrimbleMapsTripProvider.retrieve().updateTrip(trip)

            // If we aren't navigating, we need to return to the search screen
            if (!isNavigating) {
                val intent = Intent(this@TripPreviewActivity, SearchActivity::class.java)
                startActivity(intent)
            }
            finish()
        }
    }
}

// Extensions to other classes
fun Location.toPoint(): Point = Point.fromLngLat(this.longitude, this.latitude)
fun TripStop.toPoint(): Point = Point.fromLngLat(this.trimbleMapsLocation.coords().lon().toDouble(), this.trimbleMapsLocation.coords().lat().toDouble())

fun LatLng.toPoint(): Point = Point.fromLngLat(this.longitude, this.latitude)
/** We can set custom behavior in the onCreate for the navigation screen's search button
 *  to bring the user to the search screen.
 *  This allows the user to search for POIs along the route
 *  isNavigating is used to differentiate behavior while on the route and default behavior.
 *  This button can also be hidden using navigationView.hideSearchButton()
 */
navigationView.retrieveSearchBtn().setOnClickListener {
    val intent = Intent(this, PoiActivity::class.java)
    intent.putExtra("isNavigating", true)
    startActivity(intent)
}

// PoiActivity 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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SearchActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/searchFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

// PoiActivity
import android.annotation.SuppressLint
import android.content.Intent
import android.location.Location
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import com.microsoft.appcenter.crashes.Crashes
import com.trimblemaps.api.geocoding.v1.models.TrimbleMapsLocation
import com.trimblemaps.mapsdk.camera.CameraUpdateFactory
import com.trimblemaps.mapsdk.location.modes.CameraMode
import com.trimblemaps.mapsdk.location.modes.RenderMode
import com.trimblemaps.mapsdk.maps.Style
import com.trimblemaps.mapsdk.plugins.places.autocomplete.model.PlaceOptions
import com.trimblemaps.mapsdk.plugins.places.autocomplete.ui.PlaceSelectionListener
import com.trimblemaps.navigation.plugins.poisonroute.model.PoisOnRouteOptions
import com.trimblemaps.navigation.plugins.poisonroute.ui.PoisOnRouteFragment
import com.trimblemaps.mapsdk.style.layers.PropertyFactory.*

import com.trimblemaps.android.core.permissions.PermissionsManager
import com.trimblemaps.api.poi.v1.models.PoisResponse
import com.trimblemaps.mapsdk.location.LocationComponentActivationOptions
import com.trimblemaps.mapsdk.plugins.places.searchflow.ui.SearchFlowFragment
import com.trimblemaps.navigation.core.TrimbleMapsNavigation
import com.trimblemaps.navigation.core.TrimbleMapsNavigationProvider
import com.trimblemaps.navigation.core.TrimbleMapsTrip
import com.trimblemaps.navigation.core.TrimbleMapsTripProvider
import com.trimblemaps.navigation.core.trip.model.TripStop
import com.trimblemaps.navigation.core.trip.model.toTripStop
import com.trimblemaps.navigation.plugins.poisonroute.ui.PoisOnRouteCallback
import java.security.AccessController.getContext

// Databinding from the XML
import com.your_app_name.databinding.ActivitySearchBinding


class PoiActivity : AppCompatActivity() {

    private var currentLocation: Location? = null
    private val placeOptions = PlaceOptions.builder()
        .viewMode(PlaceOptions.MODE_FLOATING)
        .historyCount(0) // Don't show recents
        .hideMenuIcon(true)
        .includeTrimblePlaceIds(true)
        .useCustomPlaces(true)

    lateinit var fragment: PoisOnRouteFragment
    lateinit var trip: TrimbleMapsTrip
    lateinit var trimbleMapsNavigation: TrimbleMapsNavigation

    private lateinit var binding: ActivitySearchBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivitySearchBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        trip = TrimbleMapsTripProvider.retrieve()
        trimbleMapsNavigation = TrimbleMapsNavigationProvider.retrieve()

        if (savedInstanceState == null) {
            initSearch()
        } else {
            fragment = supportFragmentManager.findFragmentByTag(PoisOnRouteFragment.TAG) as PoisOnRouteFragment
            fragment.setOnPlaceSelectedListener(placeSelectionListener)
            fragment.setOnStyleLoadedListener(onStyleLoadedCallback)
        }
    }

    @SuppressLint("MissingPermission")
    private val onStyleLoadedCallback = Style.OnStyleLoaded { style ->
        fragment.map?.apply {
            moveCamera(CameraUpdateFactory.zoomTo(14.0))
        }
        if (PermissionsManager.isForegroundLocationPermissionsGranted(this)) {
            fragment.map?.locationComponent?.apply {
                // Activate with options
                activateLocationComponent(
                    LocationComponentActivationOptions.builder(this@PoiActivity, style).build()
                )

                // Enable to make component visible
                isLocationComponentEnabled = true
                cameraMode = CameraMode.TRACKING_GPS_NORTH
                renderMode = RenderMode.COMPASS
            }
        }
    }

    private fun initSearch() {
        currentLocation = trip.lastRawLocation
        currentLocation?.let {
            placeOptions.center(it.toPoint())
        }
        val options: PoisOnRouteOptions = PoisOnRouteOptions.builder()
            .viewMode(PoisOnRouteOptions.MODE_FULLSCREEN)
            .placeOptions(placeOptions.build())
            .routeOptions(trip.remainingTripRouteOptions())
            .tripId(trimbleMapsNavigation.getRouteUUID())
            .geometryIndex(trimbleMapsNavigation.getRouteProgress()?.currentLegProgress?.geometryIndex)
            .legIndex(trimbleMapsNavigation.getRouteProgress()?.currentLegProgress?.legIndex)
            .maxDistanceAhead(trimbleMapsNavigation.getRouteProgress()?.currentLegProgress?.distanceRemaining?.toDouble())
            .directions(trimbleMapsNavigation.getRoutes())
            .build()

        fragment = PoisOnRouteFragment.newInstance(options)
        fragment.setOnPlaceSelectedListener(placeSelectionListener)
        fragment.setOnStyleLoadedListener(onStyleLoadedCallback)
        fragment.setPoisOnRouteCallback(object : PoisOnRouteCallback {
            override fun onResponse(response: PoisResponse?) {
            }

            override fun onFailure(throwable: Throwable?) {
            }
        })

        supportFragmentManager.commit {
            setReorderingAllowed(true)
            add(binding.searchFragment.id, fragment, PoisOnRouteFragment.TAG)
            setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
        }
    }

    private val placeSelectionListener = object : PlaceSelectionListener {
        override fun onPlaceSelected(feature: TrimbleMapsLocation) {
            updateRouteWithStop(feature)
            supportFragmentManager.beginTransaction().remove(fragment).commit()
        }

        override fun onCancel() {
            supportFragmentManager.beginTransaction().remove(fragment).commit()
            finish()
        }
    }

    private fun updateRouteWithStop(location: TrimbleMapsLocation) {
        // Insert this stop into the trip
        // First, find the next open (not completed) stop
        val nextOpenStop = trip.nextOpenStop
        // Figure out the stop index for that stop
        val stopIndex = trip.stops.indexOf(nextOpenStop)
        if (stopIndex <= 0 ) {
            trip.addStop(location.toTripStop())
        } else {
            trip.insertStop(location.toTripStop(), stopIndex)
        }
        finish()
    }
}

Full code for all steps

Last updated April 8, 2025.
Contents