按照本指南中的说明,使用 Android 版 Navigation SDK 在您的应用中绘制到多个目的地(也称为航点)的路线。
概览
- 将 Navigation SDK 集成到您的应用中,如设置项目中所述。
- 向您的应用添加
SupportNavigationFragment
或NavigationView
。此界面元素会向您的 activity 添加交互式地图和精细导航界面。 - 使用
NavigationApi
类初始化 SDK。 定义
Navigator
以控制精细导航:- 使用
setDestinations()
添加目的地。 - 使用
startGuidance()
开始导航。 - 使用
getSimulator()
模拟车辆沿路线的进度,以便测试、调试和演示应用。
- 使用
构建并运行您的应用。
查看代码
显示/隐藏导航 activity 的 Java 代码。
package com.example.navsdkmultidestination; import android.content.pm.PackageManager; import android.location.Location; 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.CameraPerspective; import com.google.android.libraries.navigation.ArrivalEvent; 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.RoadSnappedLocationProvider; import com.google.android.libraries.navigation.SimulationOptions; import com.google.android.libraries.navigation.SupportNavigationFragment; import com.google.android.libraries.navigation.TimeAndDistance; import com.google.android.libraries.navigation.Waypoint; import java.util.ArrayList; import java.util.List; /** * An activity that displays a map and a navigation UI, guiding the user from their current location * to multiple destinations, also known as waypoints. */ public class NavigationActivityMultiDestination extends AppCompatActivity { private static final String TAG = NavigationActivityMultiDestination.class.getSimpleName(); private static final String DISPLAY_BOTH = "both"; private static final String DISPLAY_TOAST = "toast"; private static final String DISPLAY_LOG = "log"; private Navigator mNavigator; private RoadSnappedLocationProvider mRoadSnappedLocationProvider; private SupportNavigationFragment mNavFragment; private final List<Waypoint> mWaypoints = new ArrayList<>(); private Navigator.ArrivalListener mArrivalListener; private Navigator.RouteChangedListener mRouteChangedListener; private Navigator.RemainingTimeOrDistanceChangedListener mRemainingTimeOrDistanceChangedListener; private RoadSnappedLocationProvider.LocationListener mLocationListener; private Bundle mSavedInstanceState; private static final String KEY_JOURNEY_IN_PROGRESS = "journey_in_progress"; private boolean mJourneyInProgress = false; // Set fields for requesting location permission. private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1; private boolean mLocationPermissionGranted; /** * Sets up the navigator when the activity is created. * * @param savedInstanceState The activity state bundle. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Save the navigator state, used to determine whether a journey is in progress. mSavedInstanceState = savedInstanceState; if (mSavedInstanceState != null && mSavedInstanceState.containsKey(KEY_JOURNEY_IN_PROGRESS)) { mJourneyInProgress = (mSavedInstanceState.getInt(KEY_JOURNEY_IN_PROGRESS) != 0); } setContentView(R.layout.activity_main); // Initialize the Navigation SDK. initializeNavigationSdk(); } /** Releases navigation listeners when the activity is destroyed. */ @Override protected void onDestroy() { super.onDestroy(); if ((mJourneyInProgress) && (this.isFinishing())) { mNavigator.removeArrivalListener(mArrivalListener); mNavigator.removeRouteChangedListener(mRouteChangedListener); mNavigator.removeRemainingTimeOrDistanceChangedListener( mRemainingTimeOrDistanceChangedListener); if (mRoadSnappedLocationProvider != null) { mRoadSnappedLocationProvider.removeLocationListener(mLocationListener); } displayMessage("OnDestroy: Released navigation listeners.", DISPLAY_LOG); } } /** Saves the state of the app when the activity is paused. */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mJourneyInProgress) { outState.putInt(KEY_JOURNEY_IN_PROGRESS, 1); } else { outState.putInt(KEY_JOURNEY_IN_PROGRESS, 0); } } /** * Starts the Navigation SDK and sets the camera to follow the device's location. Calls the * navigateToPlaces() 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.", DISPLAY_BOTH); 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.", DISPLAY_BOTH); mNavigator = navigator; mNavFragment = (SupportNavigationFragment) getSupportFragmentManager().findFragmentById(R.id.navigation_fragment); // Set the camera to follow the device location with 'TILTED' driving view. mNavFragment.getMapAsync( googleMap -> googleMap.followMyLocation(CameraPerspective.TILTED)); // Navigate to the specified places. navigateToPlaces(); } /** * 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.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED: displayMessage( "Error loading Navigation SDK: User did not accept " + "the Navigation Terms of Use.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.NETWORK_ERROR: displayMessage("Error loading Navigation SDK: Network error.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING: displayMessage( "Error loading Navigation SDK: Location permission " + "is missing.", DISPLAY_BOTH); break; default: displayMessage("Error loading Navigation SDK: " + errorCode, DISPLAY_BOTH); } } }); } /** Requests directions from the user's current location to a list of waypoints. */ private void navigateToPlaces() { // Set up a waypoint for each place that we want to go to. createWaypoint("ChIJq6qq6jauEmsRJAf7FjrKnXI", "Sydney Star"); createWaypoint("ChIJ3S-JXmauEmsRUcIaWtf4MzE", "Sydney Opera House"); createWaypoint("ChIJLwgLFGmuEmsRzpDhHQuyyoU", "Sydney Conservatorium of Music"); // If this journey is already in progress, no need to restart navigation. // This can happen when the user rotates the device, or sends the app to the background. if (mSavedInstanceState != null && mSavedInstanceState.containsKey(KEY_JOURNEY_IN_PROGRESS) && mSavedInstanceState.getInt(KEY_JOURNEY_IN_PROGRESS) == 1) { return; } // Create a future to await the result of the asynchronous navigator task. ListenableResultFuture<Navigator.RouteStatus> pendingRoute = mNavigator.setDestinations(mWaypoints); // 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: mJourneyInProgress = true; // Hide the toolbar to maximize the navigation UI. if (getActionBar() != null) { getActionBar().hide(); } // Register some listeners for navigation events. registerNavigationListeners(); // Display the time and distance to each waypoint. displayTimesAndDistances(); // 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.", DISPLAY_BOTH); break; case NETWORK_ERROR: displayMessage("Error starting navigation: Network error.", DISPLAY_BOTH); break; case ROUTE_CANCELED: displayMessage("Error starting navigation: Route canceled.", DISPLAY_BOTH); break; default: displayMessage("Error starting navigation: " + String.valueOf(code), DISPLAY_BOTH); } } }); } /** * Creates a waypoint from a given place ID and title. * * @param placeId The ID of the place to be converted to a waypoint. * @param title A descriptive title for the waypoint. */ private void createWaypoint(String placeId, String title) { try { mWaypoints.add(Waypoint.builder().setPlaceIdString(placeId).setTitle(title).build()); } catch (Waypoint.UnsupportedPlaceIdException e) { displayMessage( "Error starting navigation: Place ID is not supported: " + placeId, DISPLAY_BOTH); } } /** Displays the calculated travel time and distance to each waypoint. */ private void displayTimesAndDistances() { List<TimeAndDistance> timesAndDistances = mNavigator.getTimeAndDistanceList(); int leg = 1; String message = "You're on your way!"; for (TimeAndDistance timeAndDistance : timesAndDistances) { message = message + "\nRoute leg: " + leg++ + ": Travel time (seconds): " + timeAndDistance.getSeconds() + ". Distance (meters): " + timeAndDistance.getMeters(); } displayMessage(message, DISPLAY_BOTH); } /** * Registers some event listeners to show a message and take other necessary steps when specific * navigation events occur. */ private void registerNavigationListeners() { mArrivalListener = new Navigator.ArrivalListener() { @Override public void onArrival(ArrivalEvent arrivalEvent) { displayMessage( "onArrival: You've arrived at a waypoint: " + mNavigator.getCurrentRouteSegment().getDestinationWaypoint().getTitle(), DISPLAY_BOTH); // Start turn-by-turn guidance for the next leg of the route. if (arrivalEvent.isFinalDestination()) { displayMessage("onArrival: You've arrived at the final destination.", DISPLAY_BOTH); } else { mNavigator.continueToNextDestination(); mNavigator.startGuidance(); } } }; // Listens for arrival at a waypoint. mNavigator.addArrivalListener(mArrivalListener); mRouteChangedListener = new Navigator.RouteChangedListener() { @Override public void onRouteChanged() { displayMessage( "onRouteChanged: The driver's route has changed. Current waypoint: " + mNavigator.getCurrentRouteSegment().getDestinationWaypoint().getTitle(), DISPLAY_LOG); } }; // Listens for changes in the route. mNavigator.addRouteChangedListener(mRouteChangedListener); // Listens for road-snapped location updates. mRoadSnappedLocationProvider = NavigationApi.getRoadSnappedLocationProvider(getApplication()); mLocationListener = new RoadSnappedLocationProvider.LocationListener() { @Override public void onLocationChanged(Location location) { displayMessage( "onLocationUpdated: Navigation engine has provided a new" + " road-snapped location: " + location.toString(), DISPLAY_LOG); } @Override public void onRawLocationUpdate(Location location) { displayMessage( "onLocationUpdated: Navigation engine has provided a new" + " raw location: " + location.toString(), DISPLAY_LOG); } }; if (mRoadSnappedLocationProvider != null) { mRoadSnappedLocationProvider.addLocationListener(mLocationListener); } else { displayMessage("ERROR: Failed to get a location provider", DISPLAY_LOG); } mRemainingTimeOrDistanceChangedListener = new Navigator.RemainingTimeOrDistanceChangedListener() { @Override public void onRemainingTimeOrDistanceChanged() { displayMessage( "onRemainingTimeOrDistanceChanged: Time or distance estimate" + " has changed.", DISPLAY_LOG); } }; // Listens for changes in time or distance. mNavigator.addRemainingTimeOrDistanceChangedListener( 60, 100, mRemainingTimeOrDistanceChangedListener); } /** 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, String displayMedium) { if (displayMedium.equals(DISPLAY_BOTH) || displayMedium.equals(DISPLAY_TOAST)) { Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); } if (displayMedium.equals(DISPLAY_BOTH) || displayMedium.equals(DISPLAY_LOG)) { Log.d(TAG, errorMessage); } } }
添加导航 fragment
SupportNavigationFragment
是用于显示导航视觉输出的界面组件,包括交互式地图和精细导航路线。您可以在 XML 布局文件中声明 fragment,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.google.android.libraries.navigation.SupportNavigationFragment"
android:id="@+id/navigation_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
或者,您也可以使用 FragmentActivity.getSupportFragmentManager()
以编程方式构建 fragment,如 Android 文档中所述。
作为 fragment 的替代方案,界面组件还可作为 NavigationView
使用。在大多数情况下,我们建议使用 SupportNavigationFragment
(即 NavigationView
的封装容器),而不是直接与 NavigationView
交互。如需了解详情,请参阅导航地图互动最佳实践
。
请求位置权限
您的应用必须请求位置信息权限,才能确定设备的位置。
本教程提供了请求精确位置信息权限所需的代码。 如需了解详情,请参阅 Android 权限指南。
在 Android 清单中将权限添加为
<manifest>
元素的子元素:<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.navsdkmultidestination"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> </manifest>
在应用中请求运行时权限,让用户有机会允许或拒绝位置信息权限。以下代码会检查用户是否已授予精确位置信息权限。如果未授予,它将请求此权限:
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.", DISPLAY_BOTH); return; }
替换
onRequestPermissionsResult()
回调以处理权限请求的结果:@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; } } } }
初始化 Navigation SDK 并配置行程
NavigationApi
类提供初始化逻辑,用于授权您的应用使用 Google 导航。Navigator
类可用于控制导航历程的配置以及启动/停止导航历程。
创建一个辅助方法,用于在屏幕上和日志中显示消息。
private void displayMessage(String errorMessage, String displayMedium) { if (displayMedium.equals(DISPLAY_BOTH) || displayMedium.equals(DISPLAY_TOAST)) { Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); } if (displayMedium.equals(DISPLAY_BOTH) || displayMedium.equals(DISPLAY_LOG)) { Log.d(TAG, errorMessage); } }
初始化 Navigation SDK 并替换
onNavigatorReady()
回调,以便在导航器准备就绪时启动导航: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.", DISPLAY_BOTH); mNavigator = navigator; mNavFragment = (SupportNavigationFragment) getFragmentManager() .findFragmentById(R.id.navigation_fragment); // Set the camera to follow the device location with 'TILTED' driving view. mNavFragment.getCamera().followMyLocation(Camera.Perspective.TILTED); // Navigate to the specified places. navigateToPlaces(); } /** * 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.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED: displayMessage("Error loading Navigation SDK: User did not accept " + "the Navigation Terms of Use.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.NETWORK_ERROR: displayMessage("Error loading Navigation SDK: Network error.", DISPLAY_BOTH); break; case NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING: displayMessage("Error loading Navigation SDK: Location permission " + "is missing.", DISPLAY_BOTH); break; default: displayMessage("Error loading Navigation SDK: " + errorCode, DISPLAY_BOTH); } } });
添加了一种方法,用于根据给定地点 ID 和标题创建
Waypoint
对象。private void createWaypoint(String placeId, String title) { try { mWaypoints.add( Waypoint.builder() .setPlaceIdString(placeId) .setTitle(title) .build() ); } catch (Waypoint.UnsupportedPlaceIdException e) { displayMessage("Error starting navigation: Place ID is not supported: " + placeId, DISPLAY_BOTH); } }
添加一个方法,用于显示计算的行程时间和到每个航点的距离。
private void displayTimesAndDistances() { List<TimeAndDistance> timesAndDistances = mNavigator.getTimeAndDistanceList(); int leg = 1; String message = "You're on your way!"; for (TimeAndDistance timeAndDistance : timesAndDistances) { message = message + "\nRoute leg: " + leg++ + ": Travel time (seconds): " + timeAndDistance.getSeconds() + ". Distance (meters): " + timeAndDistance.getMeters(); } displayMessage(message, DISPLAY_BOTH); }
设置此行程的所有航点。(请注意,如果您使用导航器无法绘制路线的地点 ID,可能会收到错误消息。本教程中的示例应用使用澳大利亚的航点的地点 ID。请参阅下文中有关获取不同的地点 ID 的说明。)计算路线后,
SupportNavigationFragment
会在地图上显示一条多段线来表示路线,并在每个航点处显示一个标记。private void navigateToPlaces() { // Set up a waypoint for each place that we want to go to. createWaypoint("ChIJq6qq6jauEmsRJAf7FjrKnXI", "Sydney Star"); createWaypoint("ChIJ3S-JXmauEmsRUcIaWtf4MzE", "Sydney Opera House"); createWaypoint("ChIJLwgLFGmuEmsRzpDhHQuyyoU", "Sydney Conservatorium of Music"); // If this journey is already in progress, no need to restart navigation. // This can happen when the user rotates the device, or sends the app to the background. if (mSavedInstanceState != null && mSavedInstanceState.containsKey(KEY_JOURNEY_IN_PROGRESS) && mSavedInstanceState.getInt(KEY_JOURNEY_IN_PROGRESS) == 1) { return; } // Create a future to await the result of the asynchronous navigator task. ListenableResultFuture<Navigator.RouteStatus> pendingRoute = mNavigator.setDestinations(mWaypoints); // 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: mJourneyInProgress = true; // Hide the toolbar to maximize the navigation UI. if (getActionBar() != null) { getActionBar().hide(); } // Register some listeners for navigation events. registerNavigationListeners(); // Display the time and distance to each waypoint. displayTimesAndDistances(); // 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.", DISPLAY_BOTH); break; case NETWORK_ERROR: displayMessage("Error starting navigation: Network error.", DISPLAY_BOTH); break; case ROUTE_CANCELED: displayMessage("Error starting navigation: Route canceled.", DISPLAY_BOTH); break; default: displayMessage("Error starting navigation: " + String.valueOf(code), DISPLAY_BOTH); } } }); }
构建并运行应用
- 将 Android 设备连接到您的计算机。按照说明在您的 Android 设备上启用开发者选项,并配置您的系统,使之检测该设备。您也可以使用 Android 虚拟设备 (AVD) 管理器来配置虚拟设备。选择模拟器时,请务必选择包含 Google API 的映像。)
- 在 Android Studio 中,点击 Run 菜单选项(或 Play 按钮图标)。按提示选择设备。
有关如何提升用户体验的提示
- 用户必须先接受《Google 导航服务条款》,然后才能使用导航功能。您只需接受一次即可。默认情况下,SDK 会在首次调用导航器时提示用户接受。如果您愿意,可以在应用用户体验流程的早期(例如注册或登录期间)使用
showTermsAndConditionsDialog()
触发导航服务条款对话框。 - 如果您使用地点 ID 初始化航点(而不是经纬度目的地),导航质量和预计到达时间 (ETA) 准确性会显著提高。
此示例会根据特定地点 ID 派生航点。获取地点 ID 的其他方法包括:
- 使用地点 ID 查找工具获取特定地理位置的地点 ID。
- 使用 Geocoding API 查找给定地址的地点 ID。如果您有完整且明确的航点地址,Geocoding API 会正常运行。请参阅地理编码最佳实践指南。
- 使用 Places API 文本搜索查找给定地址的地点 ID。如果您的航点地址不完整或不明确,Places API 会非常有用。请参阅地理编码最佳实践指南。