監聽導覽事件

應用程式可監聽使用者沿著路線導航時發生的事件。

總覽

請使用下列介面監聽導覽事件:

查看程式碼

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

偵測到抵達目的地

此處的「目的地」是指最終目的地或路線點。如要偵測到抵達,請呼叫 Navigator.addArrivalListener(),這會在裝置抵達目的地時註冊回呼。

抵達目的地後,Navigation SDK for Android 會觸發 onArrival() 回呼,並停止即時路線指引。您必須明確呼叫 Navigator.continueToNextDestination() 才能前往下一個路標,並呼叫 Navigator.startGuidance() 才能繼續進行即時路線導航。

呼叫 continueToNextDestination() 時,導航器會捨棄先前目的地的所有資訊。如要分析上一個路線區段的資訊,您必須先從導航器擷取資訊,再呼叫 continueToNextDestination()

為避免記憶體耗損,您必須在不再需要事件監聽器時呼叫 removeArrivalListener(listener)

mNavigator.addArrivalListener(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();
        }
    }
});

接收位置更新資訊

NavigationApi 取得 RoadSnappedLocationProvider,然後呼叫 RoadSnappedLocationProvider.addLocationListener(),在裝置位置或標頭變更時註冊回呼。請注意,這個位置會對齊至道路,因此可能與 Google Play 服務位置 API 中融合位置提供者傳回的位置不同。

Navigation SDK 會盡可能頻繁提供位置更新。當位置更新可用時,Navigation SDK 會觸發 onLocationChanged() 回呼。

路線定位更新與導航無關,即使導航已停止,仍可繼續。如果您讓位置更新功能在背景執行,訂閱位置更新可能會導致電池耗盡、記憶體耗盡,或不小心收集裝置位置資料。當您不再需要監聽器時,請呼叫 RoadSnappedLocationProvider.removeLocationListener

mRoadSnappedLocationProvider =
        NavigationApi.getRoadSnappedLocationProvider(getApplication());
if (mRoadSnappedLocationProvider != null) {
    mRoadSnappedLocationProvider.addLocationListener(
            new RoadSnappedLocationProvider.LocationListener() {
                @Override
                public void onLocationChanged(Location location) {
                    displayMessage("onLocationUpdated: Navigation engine has provided a new"
                                    +   " road-snapped location: "
                                    +   location.toString(),
                            DISPLAY_LOG);
                }
            });
} else {
    displayMessage("ERROR: Failed to get a location provider", DISPLAY_LOG);
}

接收時間和距離更新

呼叫 Navigator.addRemainingTimeOrDistanceChangedListener() 時,如果剩餘時間 (秒) 或距離 (公尺) 變化超過指定的閾值,系統就會註冊回呼。

當時間或距離的變動超過指定值時,Navigation SDK 會觸發 onRemainingTimeOrDistanceChanged() 回呼。

如要查看剩餘時間和距離,請呼叫 Navigator.getTimeAndDistanceList()。請注意,清單中的時間和距離是累加的:顯示的是從目前位置到每個路標的時間和距離,而非從一個路標到另一個路標的時間和距離。TimeAndDistance 物件現在也會傳回 delaySeverity。這個列舉值會是 heavy、meduim、light 或 unknown。這會對應到您在地圖 UI 中看到的預估時間顏色 (嚴重 = 紅色、中等 = 黃色、輕微 = 綠色)。如果您需要建立自己的 ETA 頁尾,這項功能就很實用。

為避免記憶體耗損,您必須在不再需要事件監聽器時呼叫 Navigator.removeRemainingTimeOrDistanceChangedListener(listener)

如果剩餘時間變化超過 60 秒,或剩餘距離變化超過 100 公尺,則下列範例會要求回呼。

mNavigator.addRemainingTimeOrDistanceChangedListener(60, 100,
        new Navigator.RemainingTimeOrDistanceChangedListener() {
            @Override
            public void onRemainingTimeOrDistanceChanged() {
                displayMessage("onRemainingTimeOrDistanceChanged: Time or distance estimate"
                        +   " has changed.",
                        DISPLAY_LOG);
            }
        });

您可以使用 setEtaCardEnabled() 方法,並傳遞值為 TRUE 的參數,透過內建顯示器顯示剩餘時間和距離資訊。如要隱藏時間和距離顯示資訊,請將這個值設為 FALSE

您也可以使用 getTimeAndDistanceList() 方法,公開多個路標的預估到達時間。

接收路線更新

呼叫 Navigator.addRouteChangedListener() 即可在路徑變更時註冊回呼。

路線變更時,Navigation SDK 會觸發 onRouteChanged() 回呼。您可以呼叫 Navigator.getRouteSegmentsNavigator.getCurrentRouteSegment() 來尋找新路線。

為避免記憶體耗損,您必須在不再需要監聽器時呼叫 removeRouteChangedListener(listener)

mNavigator.addRouteChangedListener(new Navigator.RouteChangedListener() {
    @Override
    public void onRouteChanged() {
        displayMessage("onRouteChanged: The driver's route has changed. Current waypoint: "
                +   mNavigator.getCurrentRouteSegment().getDestinationWaypoint().getTitle(),
                DISPLAY_LOG);
    }
});

偵測夜間模式變更

呼叫 NavigationView.addOnNightModeChangedListenerSupportNavigationFragment.addOnNightModeChangedListener,即可在夜間模式變更時註冊回呼。

以下範例說明如何在導覽片段上聆聽夜間模式變更。

mNavFragment.addOnNightModeChangedListener(new NavigationView.OnNightModeChangedListener() {
    @Override
    public void onNightModeChanged(NightModeChangedEvent nightModeChangedEvent) {
        displayMessage("Night mode is active: " + nightModeChangedEvent.inNightMode(),
        DISPLAY_LOG);
    }
});

您也可以透過程式設計設定夜間模式。詳情請參閱「設定夜間模式」。