Modify the navigation UI

Using the Navigation SDK for Android, you can modify the user experience with your map by determining which of the built-in UI controls and elements appear on the map. You can also adjust the visual appearance of the navigation UI. Refer to the Policies page for guidelines on acceptable modifications to the navigation UI.

This document describes how to modify your map's user interface in two ways:

Map UI controls

Map UI controls are the recommended way to place custom UI elements on the navigation view to ensure proper positioning. When the built-in layout changes, Navigation SDK for Android automatically repositions your custom controls. You may set one custom control view at a time for each position. If your design requires multiple UI elements, you can place them in a ViewGroup and pass it to the setCustomControl method.

The setCustomControl method provides positions as defined in the CustomControlPosition enum:

  • SECONDARY_HEADER (appears in portrait mode only)
  • BOTTOM_START_BELOW
  • BOTTOM_END_BELOW
  • FOOTER
Custom control positions for portrait orientation.
Custom control positions for portrait orientation
Custom control positions for landscape orientation.
Custom control positions for landscape orientation

Add a custom control

  1. Create an Android View with the custom UI element or ViewGroup.
  2. Inflate the XML or instantiate the custom view to get an instance of the view.
  3. Use NavigationView.setCustomControl or SupportNavigationFragment.setCustomControl with the chosen custom control position from the CustomControlPosition enum.

    The example below creates a fragment and adds a custom control in the secondary header position.

     mNavFragment.setCustomControl(getLayoutInflater().
       inflate(R.layout.your_custom_control, null),
       CustomControlPosition.SECONDARY_HEADER);
     ```
    

Remove a custom control

To remove a custom control, call the setCustomControl method with a null view parameter and the chosen custom control position.

For example, the following snippet removes the any custom secondary header and returns to the default content:

mNavFragment.setCustomControl(null, CustomControlPosition.SECONDARY_HEADER);

Custom control positions

Secondary header

Secondary header custom control position for portrait orientation.
Secondary header custom control position for portrait orientation

To use this custom control position, pass the position CustomControlPosition.SECONDARY_HEADER to setCustomControl.

By default, screen layouts in navigation mode provide a position for a secondary header located beneath the primary header. This secondary header appears when necessary, such as with lane guidance. Your app can use this secondary header position of the layout for custom content. When you use this feature, your control covers any default secondary header content. If your navigation view has a background, that background remains in place, covered by the secondary header. When your app removes the custom control, any default secondary header can appear in its place.

The custom secondary header position aligns its top edge with the bottom edge of the primary header. This position is only supported in portrait mode. In landscape mode, the secondary header is unavailable, and the layout does not change.

Bottom start

Bottom start custom control position for portrait orientation.
Bottom start custom control position for portrait orientation
Bottom start custom control position for landscape orientation.
Bottom start custom control position for landscape orientation

To use this custom control position, pass the position CustomControlPosition.BOTTOM_START_BELOW to setCustomControl.

This custom control position sits in the bottom start corner of the map. In both portrait mode and landscape mode, it sits above the ETA card and/or custom footer (or along the bottom of the map if neither are present), and Nav SDK elements including the re-center button and Google logo move up to account for the height of the custom control view. This control is positioned inside the visible map bounds, so any padding added to the bottom or start edges of the map will also change the position of this control.

Bottom end

Bottom end custom control position for portrait orientation.
Bottom end custom control position for portrait orientation
Bottom end custom control position for landscape orientation.
Bottom end custom control position for landscape orientation

To use this custom control position, pass the position CustomControlPosition.BOTTOM_END_BELOW to setCustomControl.

This custom control position sits in the bottom end corner of the map. In portrait mode, it sits above the ETA card and/or custom footer (or along the bottom of the map if neither are present), but in landscape mode it is aligned with the bottom of the map. Any Nav SDK elements visible along the end side (right side in LTR) move up to account for the height of the custom control view. This control is positioned inside the visible map bounds, so any padding added to the bottom or end edges of the map will also change the position of this control.

Footer custom control position for portrait orientation.
Footer custom control position for portrait orientation
Footer custom control position for landscape orientation.
Footer custom control position for landscape orientation

To use this custom control position, pass the position CustomControlPosition.FOOTER to setCustomControl.

This custom control position is designed for a custom footer view. If the Nav SDK ETA card is visible, this control sits above it. If not, the control is aligned with the bottom of the map. Unlike the BOTTOM_START_BELOW and BOTTOM_END_BELOW custom controls, this control is positioned outside the visible map bounds, which means that any padding added to the map won't change the position of this control.

In portrait mode, the custom footer is full width. Custom controls in both CustomControlPosition.BOTTOM_START_BELOW and CustomControlPosition.BOTTOM_END_BELOW positions, as well as Nav SDK UI elements like the re-center button and the Google logo, are positioned above the custom control footer. The default position of the chevron takes the custom footer height into account.

In landscape mode, the custom footer is half width and aligned to the start side (left side in LTR), just like the Nav SDK ETA card. The custom controls in the CustomControlPosition.BOTTOM_START_BELOW position and Nav SDK UI elements like the re-center button and the Google logo are positioned above the custom control footer. The custom controls in CustomControlPosition.BOTTOM_END_BELOW position and any Nav SDK UI elements along the end side (right side in LTR) stay aligned with the bottom of the map. The default position of the chevron does not change when a custom footer is present since the footer does not extend to the end side of the map.

Custom controls in CustomControlPosition.BOTTOM_START_BELOW and CustomControlPosition.BOTTOM_END_BELOW positions, as well as Nav SDK UI elements like the re-center button and the Google logo are positioned above the custom control footer.

Map UI accessories

The Navigation SDK for Android provides UI accessories that appear during navigation similar to those found in the Google Maps for Android application. You can adjust the visibility or visual appearance of these controls as described in this section. Changes you make here reflect during the next navigation session.

Refer to the Policies page for guidelines on acceptable modifications to the navigation UI.

View the code

package com.example.navsdkcustomization;

import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.CameraPerspective;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.libraries.navigation.ListenableResultFuture;
import com.google.android.libraries.navigation.NavigationApi;
import com.google.android.libraries.navigation.Navigator;
import com.google.android.libraries.navigation.SimulationOptions;
import com.google.android.libraries.navigation.StylingOptions;
import com.google.android.libraries.navigation.SupportNavigationFragment;
import com.google.android.libraries.navigation.Waypoint;

/** An activity that displays a map and a customized navigation UI. */
public class NavigationActivityCustomization extends AppCompatActivity {

  private static final String TAG = NavigationActivityCustomization.class.getSimpleName();
  private Navigator mNavigator;
  private SupportNavigationFragment mNavFragment;
  private GoogleMap mMap;
  // Define the Sydney Opera House by specifying its place ID.
  private static final String SYDNEY_OPERA_HOUSE = "ChIJ3S-JXmauEmsRUcIaWtf4MzE";
  // Set fields for requesting location permission.
  private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;
  private boolean mLocationPermissionGranted;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Initialize the Navigation SDK.
    initializeNavigationSdk();
  }

  /**
   * Starts the Navigation SDK and sets the camera to follow the device's location. Calls the
   * navigateToPlace() method when the navigator is ready.
   */
  private void initializeNavigationSdk() {
    /*
     * Request location permission, so that we can get the location of the
     * device. The result of the permission request is handled by a callback,
     * onRequestPermissionsResult.
     */
    if (ContextCompat.checkSelfPermission(
            this.getApplicationContext(), android.Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED) {
      mLocationPermissionGranted = true;
    } else {
      ActivityCompat.requestPermissions(
          this,
          new String[] {android.Manifest.permission.ACCESS_FINE_LOCATION},
          PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
    }

    if (!mLocationPermissionGranted) {
      displayMessage(
          "Error loading Navigation SDK: " + "The user has not granted location permission.");
      return;
    }

    // Get a navigator.
    NavigationApi.getNavigator(
        this,
        new NavigationApi.NavigatorListener() {
          /** Sets up the navigation UI when the navigator is ready for use. */
          @Override
          public void onNavigatorReady(Navigator navigator) {
            displayMessage("Navigator ready.");
            mNavigator = navigator;
            mNavFragment =
                (SupportNavigationFragment)
                    getSupportFragmentManager().findFragmentById(R.id.navigation_fragment);

            // Get the map.
            mNavFragment.getMapAsync(
                new OnMapReadyCallback() {
                  @Override
                  public void onMapReady(GoogleMap map) {
                    mMap = map;
                    // Navigate to a place, specified by Place ID.
                    navigateToPlace(SYDNEY_OPERA_HOUSE);
                  }
                });
          }

          /**
           * Handles errors from the Navigation SDK.
           *
           * @param errorCode The error code returned by the navigator.
           */
          @Override
          public void onError(@NavigationApi.ErrorCode int errorCode) {
            switch (errorCode) {
              case NavigationApi.ErrorCode.NOT_AUTHORIZED:
                displayMessage(
                    "Error loading Navigation SDK: Your API key is "
                        + "invalid or not authorized to use the Navigation SDK.");
                break;
              case NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED:
                displayMessage(
                    "Error loading Navigation SDK: User did not accept "
                        + "the Navigation Terms of Use.");
                break;
              case NavigationApi.ErrorCode.NETWORK_ERROR:
                displayMessage("Error loading Navigation SDK: Network error.");
                break;
              case NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING:
                displayMessage(
                    "Error loading Navigation SDK: Location permission " + "is missing.");
                break;
              default:
                displayMessage("Error loading Navigation SDK: " + errorCode);
            }
          }
        });
  }

  /** Customizes the navigation UI and the map. */
  private void customizeNavigationUI() {
    // Set custom colors for the navigator.
    mNavFragment.setStylingOptions(
        new StylingOptions()
            .primaryDayModeThemeColor(0xff1A237E)
            .secondaryDayModeThemeColor(0xff3F51B5)
            .primaryNightModeThemeColor(0xff212121)
            .secondaryNightModeThemeColor(0xff424242)
            .headerLargeManeuverIconColor(0xffffff00)
            .headerSmallManeuverIconColor(0xffffa500)
            .headerNextStepTypefacePath("/system/fonts/NotoSerif-BoldItalic.ttf")
            .headerNextStepTextColor(0xff00ff00)
            .headerNextStepTextSize(20f)
            .headerDistanceTypefacePath("/system/fonts/NotoSerif-Italic.ttf")
            .headerDistanceValueTextColor(0xff00ff00)
            .headerDistanceUnitsTextColor(0xff0000ff)
            .headerDistanceValueTextSize(20f)
            .headerDistanceUnitsTextSize(18f)
            .headerInstructionsTypefacePath("/system/fonts/NotoSerif-BoldItalic.ttf")
            .headerInstructionsTextColor(0xffffff00)
            .headerInstructionsFirstRowTextSize(24f)
            .headerInstructionsSecondRowTextSize(20f)
            .headerGuidanceRecommendedLaneColor(0xffffa500));

    mMap.setTrafficEnabled(false);

    // Place a marker at the final destination.
    if (mNavigator.getCurrentRouteSegment() != null) {
      LatLng destinationLatLng = mNavigator.getCurrentRouteSegment().getDestinationLatLng();

      Bitmap destinationMarkerIcon =
          BitmapFactory.decodeResource(getResources(), R.drawable.ic_person_pin_48dp);

      mMap.addMarker(
          new MarkerOptions()
              .position(destinationLatLng)
              .icon(BitmapDescriptorFactory.fromBitmap(destinationMarkerIcon))
              .title("Destination marker"));

      // Listen for a tap on the marker.
      mMap.setOnMarkerClickListener(
          new GoogleMap.OnMarkerClickListener() {
            @Override
            public boolean onMarkerClick(Marker marker) {
              displayMessage(
                  "Marker tapped: "
                      + marker.getTitle()
                      + ", at location "
                      + marker.getPosition().latitude
                      + ", "
                      + marker.getPosition().longitude);

              // The event has been handled.
              return true;
            }
          });
    }

    // Set the camera to follow the device location with 'TILTED' driving view.
    mMap.followMyLocation(CameraPerspective.TILTED);
  }

  /**
   * Requests directions from the user's current location to a specific place (provided by the
   * Google Places API).
   */
  private void navigateToPlace(String placeId) {
    Waypoint destination;
    try {
      destination =
          Waypoint.builder().setPlaceIdString(placeId).build();
    } catch (Waypoint.UnsupportedPlaceIdException e) {
      displayMessage("Error starting navigation: Place ID is not supported.");
      return;
    }

    // Create a future to await the result of the asynchronous navigator task.
    ListenableResultFuture<Navigator.RouteStatus> pendingRoute =
        mNavigator.setDestination(destination);

    // Define the action to perform when the SDK has determined the route.
    pendingRoute.setOnResultListener(
        new ListenableResultFuture.OnResultListener<Navigator.RouteStatus>() {
          @Override
          public void onResult(Navigator.RouteStatus code) {
            switch (code) {
              case OK:
                // Hide the toolbar to maximize the navigation UI.
                if (getActionBar() != null) {
                  getActionBar().hide();
                }

                // Customize the navigation UI.
                customizeNavigationUI();

                // Enable voice audio guidance (through the device speaker).
                mNavigator.setAudioGuidance(Navigator.AudioGuidance.VOICE_ALERTS_AND_GUIDANCE);

                // Simulate vehicle progress along the route for demo/debug builds.
                if (BuildConfig.DEBUG) {
                  mNavigator
                      .getSimulator()
                      .simulateLocationsAlongExistingRoute(
                          new SimulationOptions().speedMultiplier(5));
                }

                // Start turn-by-turn guidance along the current route.
                mNavigator.startGuidance();
                break;
              // Handle error conditions returned by the navigator.
              case NO_ROUTE_FOUND:
                displayMessage("Error starting navigation: No route found.");
                break;
              case NETWORK_ERROR:
                displayMessage("Error starting navigation: Network error.");
                break;
              case ROUTE_CANCELED:
                displayMessage("Error starting navigation: Route canceled.");
                break;
              default:
                displayMessage("Error starting navigation: " + String.valueOf(code));
            }
          }
        });
  }

  /** Handles the result of the request for location permissions. */
  @Override
  public void onRequestPermissionsResult(
      int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    mLocationPermissionGranted = false;
    switch (requestCode) {
      case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION:
        {
          // If request is canceled, the result arrays are empty.
          if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
          }
        }
    }
  }

  /**
   * Shows a message on screen and in the log. Used when something goes wrong.
   *
   * @param errorMessage The message to display.
   */
  private void displayMessage(String errorMessage) {
    Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
    Log.d(TAG, errorMessage);
  }
}

Modify the navigation header

Use SupportNavigationFragment.setStylingOptions() or NavigationView.setStylingOptions() to change the theme of the navigation header and the next-turn indicator that appears below the header when available.

You can set the following attributes:

Attribute TypeAttributes
Background color
  • Primary day mode - the daytime color of the navigation header
  • Secondary day mode - the daytime color of the next-turn indicator
  • Primary night mode - the nighttime color of the navigation header
  • Secondary night mode - the nighttime color of the next-turn indicator
Text elements for instructions
  • Text color
  • Font
  • Text size of the first row
  • Text size of the second row
Text elements for next steps
  • Font
  • Text color of the distance value
  • Text size of the distance value
  • Text color of the distance units
  • Text size of the distance units
Maneuver icons
  • Color of the large maneuver icon
  • Color of the small maneuver icon
Lane guidance
  • Color of the recommended lane or lanes

The following example shows how to set styling options:

private SupportNavigationFragment mNavFragment;
mNavFragment = (SupportNavigationFragment) getFragmentManager()
  .findFragmentById(R.id.navigation_fragment);

// Set the styling options on the fragment.
mNavFragment.setStylingOptions(new StylingOptions()
  .primaryDayModeThemeColor(0xff1A237E)
  .secondaryDayModeThemeColor(0xff3F51B5)
  .primaryNightModeThemeColor(0xff212121)
  .secondaryNightModeThemeColor(0xff424242)
  .headerLargeManeuverIconColor(0xffffff00)
  .headerSmallManeuverIconColor(0xffffa500)
  .headerNextStepTypefacePath("/system/fonts/NotoSerif-BoldItalic.ttf")
  .headerNextStepTextColor(0xff00ff00)
  .headerNextStepTextSize(20f)
  .headerDistanceTypefacePath("/system/fonts/NotoSerif-Italic.ttf")
  .headerDistanceValueTextColor(0xff00ff00)
  .headerDistanceUnitsTextColor(0xff0000ff)
  .headerDistanceValueTextSize(20f)
  .headerDistanceUnitsTextSize(18f)
  .headerInstructionsTypefacePath("/system/fonts/NotoSerif-BoldItalic.ttf")
  .headerInstructionsTextColor(0xffffff00)
  .headerInstructionsFirstRowTextSize(24f)
  .headerInstructionsSecondRowTextSize(20f)
  .headerGuidanceRecommendedLaneColor(0xffffa500));

Turn off the traffic layer

Use GoogleMap.setTrafficEnabled() to enable or disable the traffic layer on the map. This setting affects the indications of traffic density shown on the map as a whole. However, it does not affect the traffic indications on the route plotted by the navigator.

private GoogleMap mMap;
// Get the map, and when the async call returns, setTrafficEnabled
// (callback will be on the UI thread)
mMap = mNavFragment.getMapAsync(navMap -> navMap.setTrafficEnabled(false));

Enable traffic lights and stop signs

You can enable the display of traffic lights and stop signs in the map during active navigation, which provides additional context for routes and trip maneuvers.

By default, traffic lights and stop signs are disabled in the Navigation SDK. To enable this feature, call DisplayOptions for each feature independently.

DisplayOptions displayOptions =
  new DisplayOptions().showTrafficLights(true).showStopSigns(true);

Add custom markers

Navigation SDK for Android now uses Google Maps APIs for markers. Go to the Maps API documentation for more information.

Floating text

You can add floating text anywhere in your app, provided it does not cover the Google attribution. The Navigation SDK doesn't support anchoring the text to a latitude/longitude on the map, or to a label. Go to Info windows for more information.

Display the speed limit

You can programmatically show or hide the speed limit icon. Use NavigationView.setSpeedLimitIconEnabled() or SupportNavigationFragment.setSpeedLimitIconEnabled() to display or hide the speed limit icon. When enabled, the speed limit icon displays in a bottom corner during guidance. The icon displays the speed limit of the road that the vehicle is traveling on. The icon only appears in locations where reliable speed limit data is available.

 // Display the Speed Limit icon
 mNavFragment.setSpeedLimitIconEnabled(true);

The speed limit icon is temporarily hidden when the recenter button is displayed.

Set night mode

You can programmatically control the behavior of night mode. Use NavigationView.setForceNightMode() or SupportNavigationFragment.setForceNightMode() to turn night mode on or off, or let the Navigation SDK for Android control it.

  • AUTO Lets the Navigation SDK determine the appropriate mode according to the device location and local time.
  • FORCE_NIGHT forces night mode on.
  • FORCE_DAY forces day mode on.

The following example shows forcing night mode to turn on within a navigation fragment:

// Force night mode on.
mNavFragment.setForceNightMode(FORCE_NIGHT);

Display directions list

First, create the view and add it to your hierarchy.

void setupDirectionsListView() {
  // Create the view.
  DirectionsListView directionsListView = new DirectionsListView(getApplicationContext());
  // Add the view to your view hierarchy.
  ViewGroup group = findViewById(R.id.directions_view);
  group.addView(directionsListView);

  // Add a button to your layout to close the directions list view.
  ImageButton button = findViewById(R.id.close_directions_button); // this button is part of the container we hide in the next line.
  button.setOnClickListener(
      v -> findViewById(R.id.directions_view_container).setVisibility(View.GONE));
}

Be sure to forward life cycle events to the DirectionsListView just like they are with NavigationView. For example:

protected void onResume() {
  super.onResume();
  directionsListView.onResume();
}

Hiding alternate routes

When the user interface becomes cluttered with too much information, you can reduce clutter by displaying fewer alternate routes than the default (two), or by displaying no alternate routes at all. You can configure this option before you fetch the routes by calling the RoutingOptions.alternateRoutesStrategy() method with one of the following enumeration values:

Enumeration ValueDescription
AlternateRoutesStrategy.SHOW_ALL Default. Displays up to two alternate routes.
AlternateRoutesStrategy.SHOW_ONE Displays one alternate route (if one is available).
AlternateRoutesStrategy.SHOW_NONE Hides alternate routes.

The following code example demonstrates how to hide alternate routes altogether.

RoutingOptions routingOptions = new RoutingOptions();
routingOptions.alternateRoutesStrategy(AlternateRoutesStrategy.SHOW_NONE);
navigator.setDestinations(destinations, routingOptions, displayOptions);

Trip progress bar

The trip progress bar added to navigation.

The trip progress bar is a vertical bar that appears on the trailing right edge of the map when navigation starts. When enabled, it displays an overview for an entire trip, along with the user's destination and current position.

The provides users the ability to quickly anticipate any upcoming issues, such as traffic, without needing to zoom in. They can then reroute the trip if necessary. If the user reroutes the trip, the progress bar resets as if a new trip has started from that point.

The trip progress bar displays the following status indicators:

  • Route elapsed—the elapsed portion of the trip.

  • Current position—the user's current location in the trip.

  • Traffic status—the status of upcoming traffic.

  • Final destination—the final trip destination.

Enable the trip progress bar by calling the setTripProgressBarEnabled() method on NavigationView or SupportNavigationFragment. For example:

// Enable the trip progress bar.
mNavFragment.setTripProgressBarEnabled(true);