修改导航界面

借助 Navigation SDK for Android,您可以确定将哪些内置界面控件和元素显示在地图上,从而修改用户与地图的互动体验。您还可以调整导航界面的视觉外观。如需了解可对导航界面进行哪些修改,请参阅“政策”页面

本文档介绍了如何通过以下两种方式修改地图的界面:

地图界面控件

建议您使用地图界面控件在导航视图上放置自定义界面元素,以确保正确定位。当内置布局发生变化时,Android 版 Navigation SDK 会自动重新定位您的自定义控件。您可以为每个位置同时设置一个自定义控件视图。如果您的设计需要多个界面元素,您可以将它们放入 ViewGroup 中,然后将其传递给 setCustomControl 方法。

setCustomControl 方法提供 CustomControlPosition 枚举中定义的位置:

  • SECONDARY_HEADER(仅在竖屏模式下显示)
  • BOTTOM_START_BELOW
  • BOTTOM_END_BELOW
  • FOOTER
纵向模式的自定义控件位置。
纵向的自定义控件位置
横向模式的自定义控件位置。
横向模式的自定义控件位置

添加自定义控件

  1. 使用自定义界面元素或 ViewGroup 创建 Android View
  2. 膨胀 XML 或实例化自定义视图以获取视图的实例。
  3. NavigationView.setCustomControlSupportNavigationFragment.setCustomControlCustomControlPosition 枚举中所选的自定义控件位置搭配使用。

    以下示例创建了一个 fragment,并在辅助标题位置添加了一个自定义控件。

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

移除自定义控件

如需移除自定义控件,请调用 setCustomControl 方法,并使用 null 视图参数和所选的自定义控件位置。

例如,以下代码段会移除所有自定义次要标题,并返回默认内容:

mNavFragment.setCustomControl(null, CustomControlPosition.SECONDARY_HEADER);

自定义控件位置

辅助标题

纵向模式下的辅助标题自定义控件位置。
纵向模式下的辅助标题自定义控件位置

如需使用此自定义控件位置,请将位置 CustomControlPosition.SECONDARY_HEADER 传递给 setCustomControl

默认情况下,导航模式下的屏幕布局会为位于主标题下方的辅助标题提供位置。此辅助标题会在必要时显示,例如在显示车道指引时。您的应用可以将布局的此次要标题位置用于自定义内容。使用此功能时,您的控件会覆盖所有默认的辅助标题内容。如果您的导航视图具有背景,则该背景会保持原位,并被辅助标题覆盖。当您的应用移除自定义控件时,任何默认辅助标题都可以显示在其位置。

自定义辅助标题位置会将其顶部与主标题的底部对齐。此位置仅在 portrait mode 中受支持。在 landscape mode 中,次标题不可用,并且布局不会更改。

底部开始

竖屏的自定义控件底部起始位置。
纵向模式下自定义控件的底部起始位置
横屏模式的自定义底部开始控件位置。
横向模式下自定义底部起始控件位置

如需使用此自定义控件位置,请将位置 CustomControlPosition.BOTTOM_START_BELOW 传递给 setCustomControl

此自定义控件位置位于地图的底部左上角。在 portrait modelandscape mode 中,它位于预计到达时间卡片和/或自定义页脚上方(如果这两者都不存在,则位于地图底部),并且 Nav SDK 元素(包括重新居中按钮和 Google 徽标)会向上移动,以适应自定义控件视图的高度。此控件位于可见地图边界内,因此向地图底部或起始边缘添加的任何内边距也会更改此控件的位置。

底部

纵向模式的底部自定义控件位置。
纵向模式的底部自定义控件位置
横屏模式的自定义底部控件位置。
横向模式的底部自定义控件位置

如需使用此自定义控件位置,请将位置 CustomControlPosition.BOTTOM_END_BELOW 传递给 setCustomControl

此自定义控件位置位于地图的底部角落。在 portrait mode 中,它位于 ETA 卡片和/或自定义页脚上方(如果这两者都不存在,则位于地图底部),但在 landscape mode 中,它与地图底部对齐。沿着端边(LTR 中的右侧)显示的所有 Nav SDK 元素都会向上移动,以适应自定义控件视图的高度。此控件位于可见地图边界内,因此向地图底部或端边缘添加的内边距也会更改此控件的位置。

纵向模式下的页脚自定义控件位置。
纵向模式下的页脚自定义控件位置
横屏模式下的页脚自定义控件位置。
横向模式下的页脚自定义控件位置

如需使用此自定义控件位置,请将位置 CustomControlPosition.FOOTER 传递给 setCustomControl

此自定义控件位置专为自定义页脚视图而设计。如果 Nav SDK ETA 卡片可见,此控件会位于其上方。如果未指定,则控件会与地图底部对齐。与 BOTTOM_START_BELOWBOTTOM_END_BELOW 自定义控件不同,此控件位于可见地图边界之外,这意味着向地图添加的任何内边距都不会更改此控件的位置。

portrait mode 中,自定义页脚是全宽的。CustomControlPosition.BOTTOM_START_BELOWCustomControlPosition.BOTTOM_END_BELOW 位置的自定义控件,以及重新居中按钮和 Google 徽标等 Nav SDK 界面元素都位于自定义控件页脚上方。箭头的默认位置会考虑自定义页脚高度。

landscape mode 中,自定义页脚占一半宽度,并与起始边(LTR 中的左侧)对齐,就像 Nav SDK ETA 卡片一样。CustomControlPosition.BOTTOM_START_BELOW 位置的自定义控件和 Nav SDK 界面元素(例如重新居中按钮和 Google 徽标)位于自定义控件页脚上方。CustomControlPosition.BOTTOM_END_BELOW 位置的自定义控件和底部(对于 LTR,为右侧)沿线的任何 Nav SDK 界面元素都与地图底部保持对齐。当存在自定义页脚时,箭头的默认位置不会发生变化,因为页脚不会延伸到地图的末端。

CustomControlPosition.BOTTOM_START_BELOWCustomControlPosition.BOTTOM_END_BELOW 位置的自定义控件以及重新居中按钮和 Google 徽标等 Nav SDK 界面元素位于自定义控件页脚上方。

地图界面配件

Navigation SDK for Android 提供在导航期间显示的界面配件,这些配件与 Android 版 Google 地图应用中显示的配件类似。您可以按照本部分所述调整这些控件的可见性或视觉外观。您在此处所做的更改会在下一次导航会话中反映出来。

如需了解可对导航界面进行哪些修改,请参阅“政策”页面

查看代码

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);
  }
}

修改导航标题

使用 SupportNavigationFragment.setStylingOptions()NavigationView.setStylingOptions() 可更改导航标题的主题以及标题下方显示的下一个转弯指示器的主题(如果有)。

您可以设置以下属性:

属性类型属性
背景颜色
  • 主要日间模式 - 导航标题的白天颜色
  • 辅助日间模式 - 下一个转弯指示器的白天颜色
  • 主要夜间模式 - 导航标题的夜间颜色
  • 辅助夜间模式 - 下一个转弯指示器的夜间颜色
用于显示说明的文本元素
  • 文本颜色
  • 字体
  • 第一行的文字大小
  • 第二行文字的大小
后续步骤的文本元素
  • 字体
  • 距离值的文本颜色
  • 距离值的文本大小
  • 距离单位的文本颜色
  • 距离单位的文本大小
车道图标
  • 大型机动图标的颜色
  • 小型车辆操控图标的颜色
车道导航
  • 建议车道的颜色

以下示例展示了如何设置样式选项:

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));

关闭路况图层

使用 GoogleMap.setTrafficEnabled() 可在地图上启用或停用交通图层。此设置会影响整个地图上显示的交通密度指示。不过,这不会影响导航仪绘制的路线上的交通信息。

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));

启用红绿灯和停止标志

您可以启用在导航期间在地图中显示红绿灯和停止标志的功能,以便为路线和行程操作提供更多背景信息。

默认情况下,Navigation SDK 中会停用交通信号灯和停止标志。如需启用此功能,请为每个地图项单独调用 DisplayOptions

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

添加自定义标记

Navigation SDK for Android 现在使用 Google 地图 API 来创建标记。如需了解详情,请参阅 Google 地图 API 文档

浮动文本

您可以在应用中的任何位置添加浮动文本,前提是该文本不会遮盖 Google 提供方说明。Navigation SDK 不支持将文本锚定到地图上的经纬度或标签。如需了解详情,请参阅信息窗口

显示限速

您可以通过编程方式显示或隐藏限速图标。使用 NavigationView.setSpeedLimitIconEnabled()SupportNavigationFragment.setSpeedLimitIconEnabled() 显示或隐藏限速图标。启用后,限速图标会在导航期间显示在底部角落。该图标会显示车辆行驶道路的限速。该图标仅在有可靠限速数据的位置显示。

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

显示重新定位按钮时,速度限制图标会暂时隐藏。

设置夜间模式

您可以通过编程方式控制夜间模式的行为。使用 NavigationView.setForceNightMode()SupportNavigationFragment.setForceNightMode() 开启或关闭夜间模式,或让 Navigation SDK for Android 进行控制。

  • AUTO 让 Navigation SDK 根据设备位置和当地时间确定适当的模式。
  • FORCE_NIGHT 会强制开启夜间模式。
  • FORCE_DAY 会强制开启日间模式。

以下示例展示了如何在导航 fragment 中强制开启夜间模式:

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

显示路线列表

首先,创建视图并将其添加到层次结构中。

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));
}

请务必将生命周期事件转发到 DirectionsListView,就像在 NavigationView 中一样。例如:

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

隐藏备选路线

如果界面因信息过多而显得杂乱无序,您可以通过显示的备选路线数量少于默认值(2 条)或根本不显示备选路线来减少杂乱。您可以在提取路线之前配置此选项,方法是使用以下枚举值之一调用 RoutingOptions.alternateRoutesStrategy() 方法:

枚举值说明
AlternateRoutesStrategy.SHOW_ALL 默认。最多显示两个备选路线。
AlternateRoutesStrategy.SHOW_ONE 显示一条备选路线(如果有)。
AlternateRoutesStrategy.SHOW_NONE 隐藏备选路线。

以下代码示例演示了如何完全隐藏备选路线。

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

行程进度条

导航栏中添加了行程进度条。

行程进度条是一种垂直条,会在导航开始时显示在地图的右后角。启用后,它会显示整个行程的概览,以及用户的目的地和当前位置。

这样,用户无需放大,即可快速预测即将发生的任何问题(例如交通问题)。然后,他们可以根据需要重新规划行程。如果用户重新规划行程,进度条会重置,就像从该点开始新的行程一样。

行程进度条会显示以下状态指示器:

  • 路线已行驶时间 - 行程已行驶部分。

  • 当前位置 - 用户在行程中的当前位置。

  • 交通状况 - 即将到来的交通状况。

  • 终点 - 行程的最终目的地。

通过对 NavigationViewSupportNavigationFragment 调用 setTripProgressBarEnabled() 方法来启用行程进度条。例如:

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