在地圖上選取 [目前位置] 和 [顯示詳細資訊]

本教學課程說明如何找出 Android 裝置的目前位置,並顯示該位置 (商家或其他搜尋點) 的詳細資訊。按照本教學課程,使用 Google Play 服務 location API 中的 Maps SDK for AndroidPlaces SDK for Android整合式位置預測提供工具來建立 Android 應用程式。

取得程式碼

從 GitHub 複製或下載 Google Maps Android API 第 2 版範例存放區

設定您的開發專案

如要在 Android Studio 中建立教學課程專案,請按照下列步驟操作。

  1. 下載安裝 Android Studio。
  2. Google Play 服務套件加入 Android Studio。
  3. 如果您在閱讀本教學課程時尚未複製或下載 Google Maps Android API 第 2 版範例存放區,請先複製或下載。
  4. 匯入教學課程專案:

    • 在 Android Studio 中,選取 [File] (檔案) > [New] (新增) > [Import Project] (匯入專案)
    • 下載完成後,請前往您儲存 Google Maps Android API 第 2 版範例存放區的位置。
    • 在這個位置找到 CurrentPlaceDetailsOnMap 專案:
      PATH-TO-SAVED-REPO/android-samples/tutorials/CurrentPlaceDetailsOnMap
    • 選取專案目錄,然後按一下 [OK] (確定)。Android Studio 現在會使用 Gradle 建構工具來建立您的專案。

取得 API 金鑰並啟用必要的 API

如想完成本教學課程,您必須取得獲權使用 Maps SDK for Android 與 Places SDK for Android 的 Google API 金鑰。

點選下方按鈕即可取得金鑰並啟用 API。

開始使用

詳情請參閱取得 API 金鑰的完整指南。

在應用程式中加入 API 金鑰

  1. 編輯專案的 gradle.properties 檔案。
  2. 將 API 金鑰貼到 GOOGLE_MAPS_API_KEY 屬性的值中:

    GOOGLE_MAPS_API_KEY=PASTE-YOUR-API-KEY-HERE

    在您建構應用程式時,Gradle 會將 API 金鑰複製到應用程式的 Android 資訊清單中。應用程式的 build.gradle 檔案會包含以下這一行,將資訊清單中的字串 google_maps_key 對應到 Gradle 屬性 GOOGLE_MAPS_API_KEY

        resValue "string", "google_maps_key",
                (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
        

建構並執行應用程式

  1. 將 Android 裝置連接到電腦。按照操作說明為 Android 裝置啟用開發人員選項,並將系統設定為偵測裝置 (您也可以使用 Android Virtual Device (AVD) Manager 來設定虛擬裝置。選擇模擬器時,請務必挑選包含 Google API 的映像檔。詳情請參閱入門指南)。
  2. 在 Android Studio 中,按一下 [Run] (執行) 選單選項 (或播放按鈕圖示), 然後按照系統提示選擇裝置。

Android Studio 會叫用 Gradle 來建構應用程式,然後在裝置或模擬器上執行應用程式。您應該會看到一份地圖,上面有以您目前位置為中心的數個標記,與本頁的圖片類似。

疑難排解:

  • 如果您未看到地圖,請按照上述步驟,檢查您是否已取得 API 金鑰並將其加入應用程式。在 Android Studio 查看 Android Monitor 中的記錄檔,看看是否有關於 API 金鑰的錯誤訊息。
  • 如果地圖只顯示位於雪梨港灣大橋上的一個標記 (應用程式中指定的預設位置),請檢查您是否已授予應用程式位置存取權。應用程式執行時,會依照 Android 權限指南中所述的模式,提示您授予位置存取權。請注意,您也可以直接在裝置上設定權限,方法是選擇 [設定] > [應用程式] > [應用程式名稱] > [權限] > [位置]。如要進一步瞭解如何處理程式碼中的權限,請參閱下方指南,查看如何在應用程式中要求位置存取權
  • 使用 Android Studio 偵錯工具查看記錄檔並為應用程式偵錯。

瞭解程式碼

本教學課程這一段將說明 CurrentPlaceDetailsOnMap 應用程式最重要的部分,協助您瞭解如何建構類似的應用程式。

為 Places API 用戶端執行個體化

下列介面提供了 Places SDK for Android 的主要進入點:

  • GeoDataClient 可存取 Google 的本地位置和商家資訊資料庫。
  • PlaceDetectionClient 可讓您快速存取裝置的目前位置,並可在特定地點回報裝置位置。

LocationServices 介面是 Android 定位服務的主要進入點。

如要使用 API,請在片段或活動的 onCreate() 方法中將 GeoDataClientPlaceDetectionClientFusedLocationProviderClient 執行個體化,如以下程式碼範例所示:

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

        // Construct a GeoDataClient.
        mGeoDataClient = Places.getGeoDataClient(this, null);

        // Construct a PlaceDetectionClient.
        mPlaceDetectionClient = Places.getPlaceDetectionClient(this, null);

        // Construct a FusedLocationProviderClient.
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
    }
    

要求位置存取權

您的應用程式必須要求位置存取權,才能判斷裝置的位置,並讓使用者能輕觸地圖上的 [我的位置] 按鈕。

本教學課程會提供可讓您要求精確位置存取權的相關程式碼。 詳情請參閱 Android 權限指南。

  1. 將權限新增為 Android 資訊清單中 <manifest> 元素的子項:

        <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="com.example.currentplacedetailsonmap">
            <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        </manifest>
        
  2. 在應用程式中要求執行階段權限,讓使用者有機會允許或拒絕位置存取權。下列程式碼會檢查使用者是否已授予精確位置存取權;如果未授予,程式碼就會要求權限:

        private void getLocationPermission() {
            /*
             * 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);
            }
        }
        
  3. 覆寫 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 cancelled, the result arrays are empty.
                    if (grantResults.length > 0
                            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        mLocationPermissionGranted = true;
                    }
                }
            }
            updateLocationUI();
        }
        

    本教學課程的後續章節會說明 updateLocationUI() 方法。

新增地圖

使用 Maps SDK for Android 顯示地圖。

  1. 在活動的版面配置檔案 activity_maps.xml 中加入 <fragment> 元素。這個元素會將 SupportMapFragment 定義為地圖的容器並提供 GoogleMap 物件的存取權。本教學課程使用 Android 支援資料庫版本的地圖片段,確保與舊版 Android 架構能回溯相容。

        <fragment xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/map"
            android:name="com.google.android.gms.maps.SupportMapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.example.currentplacedetailsonmap.MapsActivityCurrentPlace" />
    
        
  2. 在活動的 onCreate() 方法中,將版面配置檔案設為內容檢視畫面:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_maps);
        }
        
  3. 導入 OnMapReadyCallback 介面並覆寫 onMapReady() 方法,以便在 GoogleMap 物件可用時設定地圖:

        public void onMapReady(GoogleMap map) {
            mMap = map;
    
            // Do other setup activities here too, as described elsewhere in this tutorial.
    
            // Turn on the My Location layer and the related control on the map.
            updateLocationUI();
    
            // Get the current location of the device and set the position of the map.
            getDeviceLocation();
        }
        
  4. 在活動的 onCreate() 方法中,呼叫 FragmentManager.findFragmentById() 以取得地圖片段的處理常式,然後使用 getMapAsync() 註冊地圖回呼:

        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
        
  5. 撰寫 updateLocationUI() 方法來設定地圖上的位置控制項。如果使用者已授予位置存取權,請在地圖上啟用「我的位置」圖層和相關控制項,否則請停用圖層和控制項,並將目前位置設為空值:

        private void updateLocationUI() {
            if (mMap == null) {
                return;
            }
            try {
                if (mLocationPermissionGranted) {
                    mMap.setMyLocationEnabled(true);
                    mMap.getUiSettings().setMyLocationButtonEnabled(true);
                } else {
                    mMap.setMyLocationEnabled(false);
                    mMap.getUiSettings().setMyLocationButtonEnabled(false);
                    mLastKnownLocation = null;
                    getLocationPermission();
                }
            } catch (SecurityException e)  {
                Log.e("Exception: %s", e.getMessage());
            }
        }
        

取得 Android 裝置的位置並設定地圖位置

使用整合式位置預測提供工具,找出裝置的最後已知位置,然後使用該位置來定位地圖。本教學課程會提供您需要的程式碼。如要進一步瞭解如何取得裝置的位置資訊,請參閱 Google Play 服務 location API 中的整合式位置預測提供工具指南。

    private void getDeviceLocation() {
        /*
         * Get the best and most recent location of the device, which may be null in rare
         * cases when a location is not available.
         */
        try {
            if (mLocationPermissionGranted) {
                Task locationResult = mFusedLocationProviderClient.getLastLocation();
                locationResult.addOnCompleteListener(this, new OnCompleteListener() {
                    @Override
                    public void onComplete(@NonNull Task task) {
                        if (task.isSuccessful()) {
                            // Set the map's camera position to the current location of the device.
                            mLastKnownLocation = task.getResult();
                            mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
                                    new LatLng(mLastKnownLocation.getLatitude(),
                                            mLastKnownLocation.getLongitude()), DEFAULT_ZOOM));
                        } else {
                            Log.d(TAG, "Current location is null. Using defaults.");
                            Log.e(TAG, "Exception: %s", task.getException());
                            mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(mDefaultLocation, DEFAULT_ZOOM));
                            mMap.getUiSettings().setMyLocationButtonEnabled(false);
                        }
                    }
                });
            }
        } catch(SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }
    

取得目前地點

使用 Places SDK for Android 取得裝置目前位置的可能地點清單。在這種情況下,「地點」是指商家或其他搜尋點。

使用者按一下 [取得地點] 按鈕後,本教學課程就會顯示目前地點。系統會提供可能的地點清單,讓使用者從中選擇,並在地圖上為該地點新增位置標記。本教學課程會提供您使用 Places SDK for Android 時需要的程式碼。詳情請參閱取得目前地點指南。

  1. 為選項選單建立版面配置檔案 (current_place_menu.xml),並覆寫 onCreateOptionsMenu() 方法來設定選項選單。請參閱隨附的程式碼範例應用程式。
  2. 使用者按一下 [取得地點] 選項時,會覆寫 onOptionsItemSelected() 方法以取得目前地點:
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            if (item.getItemId() == R.id.option_get_place) {
                showCurrentPlace();
            }
            return true;
        }
        
  3. 建立 showCurrentPlace() 方法,取得裝置目前位置的可能地點清單:

        private void showCurrentPlace() {
            if (mMap == null) {
                return;
            }
    
            if (mLocationPermissionGranted) {
                // Use fields to define the data types to return.
                List<Place.Field> placeFields = Arrays.asList(Place.Field.NAME, Place.Field.ADDRESS,
                        Place.Field.LAT_LNG);
    
                // Use the builder to create a FindCurrentPlaceRequest.
                FindCurrentPlaceRequest request =
                        FindCurrentPlaceRequest.newInstance(placeFields);
    
                // Get the likely places - that is, the businesses and other points of interest that
                // are the best match for the device's current location.
                @SuppressWarnings("MissingPermission") final
                Task<FindCurrentPlaceResponse> placeResult =
                        mPlacesClient.findCurrentPlace(request);
                placeResult.addOnCompleteListener (new OnCompleteListener<FindCurrentPlaceResponse>() {
                    @Override
                    public void onComplete(@NonNull Task<FindCurrentPlaceResponse> task) {
                        if (task.isSuccessful() && task.getResult() != null) {
                            FindCurrentPlaceResponse likelyPlaces = task.getResult();
    
                            // Set the count, handling cases where less than 5 entries are returned.
                            int count;
                            if (likelyPlaces.getPlaceLikelihoods().size() < M_MAX_ENTRIES) {
                                count = likelyPlaces.getPlaceLikelihoods().size();
                            } else {
                                count = M_MAX_ENTRIES;
                            }
    
                            int i = 0;
                            mLikelyPlaceNames = new String[count];
                            mLikelyPlaceAddresses = new String[count];
                            mLikelyPlaceAttributions = new List[count];
                            mLikelyPlaceLatLngs = new LatLng[count];
    
                            for (PlaceLikelihood placeLikelihood : likelyPlaces.getPlaceLikelihoods()) {
                                // Build a list of likely places to show the user.
                                mLikelyPlaceNames[i] = placeLikelihood.getPlace().getName();
                                mLikelyPlaceAddresses[i] = placeLikelihood.getPlace().getAddress();
                                mLikelyPlaceAttributions[i] = placeLikelihood.getPlace()
                                        .getAttributions();
                                mLikelyPlaceLatLngs[i] = placeLikelihood.getPlace().getLatLng();
    
                                i++;
                                if (i > (count - 1)) {
                                    break;
                                }
                            }
    
                            // Show a dialog offering the user the list of likely places, and add a
                            // marker at the selected place.
                            MapsActivityCurrentPlace.this.openPlacesDialog();
                        }
                        else {
                            Log.e(TAG, "Exception: %s", task.getException());
                        }
                    }
                });
            } else {
                // The user has not granted permission.
                Log.i(TAG, "The user did not grant location permission.");
    
                // Add a default marker, because the user hasn't selected a place.
                mMap.addMarker(new MarkerOptions()
                        .title(getString(R.string.default_info_title))
                        .position(mDefaultLocation)
                        .snippet(getString(R.string.default_info_snippet)));
    
                // Prompt the user for permission.
                getLocationPermission();
            }
        }
        
  4. 建立 openPlacesDialog() 方法以顯示表單,讓使用者從可能的地點清單中選取地點。在地圖上為已選取的地點新增標記。標記內容包含地點的名稱和地址,以及 API 提供的任何屬性:

        private void openPlacesDialog() {
            // Ask the user to choose the place where they are now.
            DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // The "which" argument contains the position of the selected item.
                    LatLng markerLatLng = mLikelyPlaceLatLngs[which];
                    String markerSnippet = mLikelyPlaceAddresses[which];
                    if (mLikelyPlaceAttributions[which] != null) {
                        markerSnippet = markerSnippet + "\n" + mLikelyPlaceAttributions[which];
                    }
    
                    // Add a marker for the selected place, with an info window
                    // showing information about that place.
                    mMap.addMarker(new MarkerOptions()
                            .title(mLikelyPlaceNames[which])
                            .position(markerLatLng)
                            .snippet(markerSnippet));
    
                    // Position the map's camera at the location of the marker.
                    mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(markerLatLng,
                            DEFAULT_ZOOM));
                }
            };
    
            // Display the dialog.
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setTitle(R.string.pick_place)
                    .setItems(mLikelyPlaceNames, listener)
                    .show();
        }
        
  5. 為資訊視窗內容建立自訂版面配置,這樣就能在資訊視窗中顯示多行內容。首先,請新增 XML 版面配置檔案 custom_info_contents.xml,其中包含資訊視窗標題的文字檢視區塊,以及其他程式碼片段的文字檢視區塊 (即資訊視窗的文字內容):

        <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layoutDirection="locale"
            android:orientation="vertical">
            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:textColor="#ff000000"
                android:textStyle="bold" />
    
            <TextView
                android:id="@+id/snippet"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#ff7f7f7f" />
        </LinearLayout>
    
        
  6. 導入 InfoWindowAdapter 介面以加載版面配置,並載入資訊視窗內容:

        @Override
        public void onMapReady(GoogleMap map) {
            // Do other setup activities here too, as described elsewhere in this tutorial.
            mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
    
            @Override
            // Return null here, so that getInfoContents() is called next.
            public View getInfoWindow(Marker arg0) {
                return null;
            }
    
            @Override
            public View getInfoContents(Marker marker) {
                // Inflate the layouts for the info window, title and snippet.
                View infoWindow = getLayoutInflater().inflate(R.layout.custom_info_contents, null);
    
                TextView title = ((TextView) infoWindow.findViewById(R.id.title));
                title.setText(marker.getTitle());
    
                TextView snippet = ((TextView) infoWindow.findViewById(R.id.snippet));
                snippet.setText(marker.getSnippet());
    
                return infoWindow;
              }
            });
        }
        

儲存地圖的狀態

儲存地圖的攝影機位置和裝置位置。當使用者旋轉 Android 裝置或變更設定時,Android 架構會銷毀並重建地圖活動。為了確保流暢的使用者體驗,建議您儲存相關的應用程式狀態,並在需要時復原。

本教學課程會提供儲存地圖狀態時需要用到的所有程式碼。詳情請參閱savedInstanceState軟體包指南。

  1. 在地圖活動中設定存放活動狀態的鍵/值:

        private static final String KEY_CAMERA_POSITION = "camera_position";
        private static final String KEY_LOCATION = "location";
        
  2. 執行 onSaveInstanceState() 回呼以在活動暫停時儲存狀態:

        @Override
        protected void onSaveInstanceState(Bundle outState) {
            if (mMap != null) {
                outState.putParcelable(KEY_CAMERA_POSITION, mMap.getCameraPosition());
                outState.putParcelable(KEY_LOCATION, mLastKnownLocation);
                super.onSaveInstanceState(outState);
            }
        }
        
  3. 在活動的 onCreate() 方法中,擷取裝置位置及地圖攝影機位置 (如果先前已儲存過):

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (savedInstanceState != null) {
                mCurrentLocation = savedInstanceState.getParcelable(KEY_LOCATION);
                mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
            }
            ...
        }