Android 6 Tutorial 第四堂(3)讀取裝置目前的位置 - Google Services Location by Michael | CodeData
top

Android 6 Tutorial 第四堂(3)讀取裝置目前的位置 - Google Services Location

分享:

專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3)

Android 6 Tutorial 第四堂(2)設計地圖應用程式 – Google Maps Android API v2 << 前情

目前的行動裝置大部份都有衛星定位的設備,在戶外適當的環境下,可以從衛星接收到精確度很高的位置資訊。在室內或遮閉物比較多的環境,Android系統也可以從網路或電信服務,讀取誤差比較大一些的位置資訊。應用程式可以儲存使用這些位置資訊,例如記錄與儲存目前的位置,在地圖元件中查詢與規劃路徑。

這一章說明最新的Google Services Location API,跟傳統的作法比較,這是一種比較省電與方便的技術,應用程式可以根據自己的需求,讀取需要的位置資訊。目前已經為記事應用程式完成地圖元件,現在為應用程式加入讀取與儲存目前位置資訊的功能。開啟還沒有儲存位置資訊的記事資料,選擇位置功能,選擇允許位置資訊授權:


在地圖檢視目前的位置以後,點選目前位置的圖示,在對話框選擇「確定」就可以儲存位置資訊:


開啟已經儲存位置資訊的記事資料,可以在地圖上查詢位置,點選圖示以後再點選說明,可以在對話框選擇清除或記錄新的位置:


14-1 準備工作

依照下列的步驟,執行準備使用Google Services Location API的工作:

  1. 啟動Android Studio並開啟MyAndroidTutorial應用程式。
  2. 選擇Android Studio功能表「Tools -> Android -> SDK Manager」。
  3. 在Android SDK Manager視窗,檢查「Extras -> Google Play services」是否已經安裝。如果還沒有安裝的話,勾選並執行安裝的工作:
  4. 開啟「Gradle Scripts -> build.gradle(Module:app)」,參考下面的內容,檢查是否已經加入需要的設定:

    dependencies {
        ...
        compile 'com.google.android.gms:play-services:7.0.0'
    } 
  5. 如果在上一步驟修改「build.gradle(Module: app)」檔案的內容,必須選擇功能表「Tools -> Android -> Sync Project with Gradle Files」執行同步的工作。

  6. 開啟「ManifestAndroid.xml」,參考下面的內容,檢查在<application>標籤下是否已經加入需要的設定:

    <meta-data android:name="com.google.android.gms.version"
               android:value="@integer/google_play_services_version" />
  7. 同樣在「ManifestAndroid.xml」,參考下面的內容,檢查在<application>標籤下是否已經加入需要的設定:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

開啟「res/values/strings.xml」檔案,加入這一章需要的文字資源:

<string name="title_update_location">記事儲存的位置</string>
<string name="message_update_location">更新或清除儲存的位置資訊?</string>
<string name="update">更新</string>
<string name="clear">清除</string>

<string name="title_current_location">目前位置</string>
<string name="message_current_location">是否儲存目前位置?</string>

<string name="google_play_service_missing">裝置沒有安裝Google Play服務</string>

14-2 使用Google Services Location API

應用程式需要讀取位置資料,使用Google Services提供的Location API,是比較方便的作法。使用在「com.google.android.gms.common.api」套件下的「GoogleApiClient」,可以連線與使用Google Services提供的服務。使用在「com.google.android.gms.location」套件下的API,可以讀取裝置目前的位置資訊。

使用Google Services Location API讀取位置資訊,通常會採用整合在元件的作法,例如記事應用程式的地圖元件,讓它可以連線與讀取位置資訊。開啟「net.macdidi.myandroidtutorial」套件下的「MapsActivity」,加入下列需要的欄位變數:

// Google API用戶端物件
private GoogleApiClient googleApiClient;

// Location請求物件
private LocationRequest locationRequest;

// 記錄目前最新的位置
private Location currentLocation;

// 顯示目前與儲存位置的標記物件
private Marker currentMarker, itemMarker;

14-2-1 使用Google API用戶端

地圖元件需要連線到Google API用戶端,使用位置資訊的服務。開啟「MapsActivity」,參考下列的程式片段,讓地圖元件類別實作需要的介面,分別是在「com.google.android.gms.maps」套件下的ConnectionCallbacks與OnConnectionFailedListener:

public class MapsActivity extends FragmentActivity
        implements OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener {
    ...

在元件加入介面需要實作的方法,後續會在方法中加入需要執行的工作:

// ConnectionCallbacks
@Override
public void onConnected(Bundle bundle) {
    // 已經連線到Google Services
}

// ConnectionCallbacks
@Override
public void onConnectionSuspended(int i) {
    // Google Services連線中斷
    // int參數是連線中斷的代號
}

// OnConnectionFailedListener
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
    // Google Services連線失敗
    // ConnectionResult參數是連線失敗的資訊
}

14-2-2 接收位置更新資訊

使用者需要為記事資料儲存位置的時候,需要在地圖顯示目前的位置讓使用者檢視與儲存,所以為地圖元件加入接收位置更新資訊的功能。開啟「MapsActivity」,參考下列的程式片段,讓地圖元件類別實作需要的介面com.google.android.gms.location.LocationListener:

public class MapsActivity extends FragmentActivity
        implements OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener,
        LocationListener {
    ...

在元件加入介面需要實作的方法,後續會在方法中加入需要執行的工作:

// LocationListener
@Override
public void onLocationChanged(Location location) {
    // 位置改變
    // Location參數是目前的位置
}

14-3 Google API用戶端連線與接收位置更新資訊

需要使用Google Services Location服務,需要建立好需要的Google API用戶端物件,在「MapsActivity」加入下列建立Google API用戶端物件的方法:

// 建立Google API用戶端物件
private synchronized void configGoogleApiClient() {
    googleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .addApi(LocationServices.API)
            .build();
}

在「MapsActivity」的「onCreate」方法加入呼叫上列方法的敘述:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 建立Google API用戶端物件
    configGoogleApiClient();
}

應用程式啟動以後,就會建立好需要的Google API用戶端物件。在後續執行連線與運作的時候,應用程式會執行ConnectionCallbacks與OnConnectionFailedListener介面對應的方法。

應用程式需要接收最新的位置資訊,需要依照應用程式的需求,建立與啟動LocationRequest服務。在「MapsActivity」加入下列建立LocationRequest物件的方法:

// 建立Location請求物件
private void configLocationRequest() {
    locationRequest = new LocationRequest();
    // 設定讀取位置資訊的間隔時間為一秒(1000ms)
    locationRequest.setInterval(1000);
    // 設定讀取位置資訊最快的間隔時間為一秒(1000ms)
    locationRequest.setFastestInterval(1000);
    // 設定優先讀取高精確度的位置資訊(GPS)
    locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

在「MapsActivity」的「onCreate」方法加入呼叫上列方法的敘述:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // 建立Location請求物件
    configLocationRequest();        
}

在「MapsActivity」的「onConnected」、「onConnectionFailed」與「onLocationChanged」方法,分別加入啟動位置更新服務與錯誤處理的敘述:

// ConnectionCallbacks
@Override
public void onConnected(Bundle bundle) {
    // 已經連線到Google Services
    // 啟動位置更新服務
    // 位置資訊更新的時候,應用程式會自動呼叫LocationListener.onLocationChanged
    LocationServices.FusedLocationApi.requestLocationUpdates(
            googleApiClient, locationRequest, MapsActivity.this);
}

// OnConnectionFailedListener
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
    // Google Services連線失敗
    // ConnectionResult參數是連線失敗的資訊
    int errorCode = connectionResult.getErrorCode();

    // 裝置沒有安裝Google Play服務
    if (errorCode == ConnectionResult.SERVICE_MISSING) {
        Toast.makeText(this, R.string.google_play_service_missing,
                Toast.LENGTH_LONG).show();
    }
}

// LocationListener
@Override
public void onLocationChanged(Location location) {
    // 位置改變
    // Location參數是目前的位置
    currentLocation = location;
    LatLng latLng = new LatLng(
            location.getLatitude(), location.getLongitude());

    // 設定目前位置的標記
    if (currentMarker == null) {
        currentMarker = mMap.addMarker(new MarkerOptions().position(latLng));
    }
    else {
        currentMarker.setPosition(latLng);
    }

    // 移動地圖到目前的位置
    moveMap(latLng);
}

Google API用戶端連線與接收位置更新資訊,是很耗用資源與電力的服務,所以會在元件的生命週期方法執行控制的工作。參考下列的程式片段,修改「MapsActivity」的「onResume」方法,還有加入「onPause」與「onStop」兩個方法:

@Override
protected void onResume() {
    super.onResume();

    // 連線到Google API用戶端
    if (!googleApiClient.isConnected() && currentMarker != null) {
        googleApiClient.connect();
    }
}

@Override
protected void onPause() {
    super.onPause();

    // 移除位置請求服務
    if (googleApiClient.isConnected()) {
        LocationServices.FusedLocationApi.removeLocationUpdates(
                googleApiClient, this);
    }
}

@Override
protected void onStop() {
    super.onStop();

    // 移除Google API用戶端連線
    if (googleApiClient.isConnected()) {
        googleApiClient.disconnect();
    }
}

14-4 定位設備授全與位置資訊管理

為了處理Android 6的授權架構,開啟「ItemActivity」,加入下列的欄位變數與方法宣告:

// 定位設備授權請求代碼
private static final int REQUEST_FINE_LOCATION_PERMISSION = 102;

// 啟動地圖與定位元件
private void processLocation() {
    // 啟動地圖元件用的Intent物件
    Intent intentMap = new Intent(this, MapsActivity.class);

    // 設定儲存的座標
    intentMap.putExtra("lat", item.getLatitude());
    intentMap.putExtra("lng", item.getLongitude());
    intentMap.putExtra("title", item.getTitle());
    intentMap.putExtra("datetime", item.getLocaleDatetime());

    // 啟動地圖元件
    startActivityForResult(intentMap, START_LOCATION);
}    

同樣在「ItemActivity」類別,加入請求定位設備授權的方法:

// 讀取與處理定位設備授權請求
private void requestLocationPermission() {
    // 如果裝置版本是6.0(包含)以上
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // 取得授權狀態,參數是請求授權的名稱
        int hasPermission = checkSelfPermission(
                Manifest.permission.ACCESS_FINE_LOCATION);

        // 如果未授權
        if (hasPermission != PackageManager.PERMISSION_GRANTED) {
            // 請求授權
            //     第一個參數是請求授權的名稱
            //     第二個參數是請求代碼
            requestPermissions(
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    REQUEST_FINE_LOCATION_PERMISSION);
        }
        else {
            // 啟動地圖與定位元件
            processLocation();
        }
    }
}

同樣在「ItemActivity」類別,找到「onRequestPermissionsResult」方法,參考下列的說明加入需要的程式碼:

@Override
public void onRequestPermissionsResult(int requestCode,
                                       String[] permissions,
                                       int[] grantResults) {
    if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
        ...
    }
    else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
        ...
    }
    // 如果是定位設備授權請求
    else if (requestCode == REQUEST_FINE_LOCATION_PERMISSION) {
        // 如果在授權請求選擇「允許」
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 啟動地圖與定位元件
            processLocation();
        }
        // 如果在授權請求選擇「拒絕」
        else {
            // 顯示沒有授權的訊息
            Toast.makeText(this, R.string.write_external_storage_denied,
                    Toast.LENGTH_SHORT).show();
        }
    }
    else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

同樣在「ItemActivity」類別,找到「clickFunction」方法,參考下列的說明修改原來的程式碼:

public void clickFunction(View view) {
    int id = view.getId();

    switch (id) {
        ...
        case R.id.set_location:
            // 讀取與處理定位設備授權請求
            requestLocationPermission();
            break;
        ...
    }

}

同樣在「ItemActivity」,在「onActivityResult」方法加入接收位置資訊的程式碼:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == Activity.RESULT_OK) {
        switch (requestCode) {
            ...
            case START_LOCATION:
                // 讀取與設定座標
                double lat = data.getDoubleExtra("lat", 0.0);
                double lng = data.getDoubleExtra("lng", 0.0);
                item.setLatitude(lat);
                item.setLongitude(lng);
                break;
            ...
        }
    }
}

完成上面的工作以後,使用者在已經儲存位置資訊的記事資料開啟地圖元件,就會在地圖畫面上顯示儲存的位置。使用者在地圖選擇儲存位置後,也可以儲存在記事資料庫中。

14-5 地圖元件的操作功能

最後的工作是在地圖元件提供使用者操作的功能,包含檢視與儲存目前的位置,還有更新或清除記事資料已經儲存的位置。開啟「MapsActivity」,在「onCreate」方法加入需要的程式碼:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // 連線到Google API用戶端
    if (!googleApiClient.isConnected()) {
        googleApiClient.connect();
    }
}

同樣在「MapsActivity」類別,找到「onMapReady」方法,參考下列的說明修改原來的程式碼:

@Override
public void onMapReady(GoogleMap googleMap) {
    mMap = googleMap;

    // 讀取記事儲存的座標
    Intent intent = getIntent();
    double lat = intent.getDoubleExtra("lat", 0.0);
    double lng = intent.getDoubleExtra("lng", 0.0);

    // 如果記事已經儲存座標
    if (lat != 0.0 && lng != 0.0) {
        // 建立座標物件
        LatLng itemPlace = new LatLng(lat, lng);
        // 加入地圖標記
        addMarker(itemPlace, intent.getStringExtra("title"),
                intent.getStringExtra("datetime"));
        // 移動地圖
        moveMap(itemPlace);
    }

    processController();
}

地圖元件需要提供使用者選擇標記與訊息框的操作功能,在「MapsActivity」加入下列的方法:

private void processController() {
    // 對話框按鈕事件
    final DialogInterface.OnClickListener listener =
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    switch (which) {
                        // 更新位置資訊
                        case DialogInterface.BUTTON_POSITIVE:
                            // 連線到Google API用戶端
                            if (!googleApiClient.isConnected()) {
                                googleApiClient.connect();
                            }
                            break;
                        // 清除位置資訊
                        case DialogInterface.BUTTON_NEUTRAL:
                            Intent result = new Intent();
                            result.putExtra("lat", 0);
                            result.putExtra("lng", 0);
                            setResult(Activity.RESULT_OK, result);
                            finish();
                            break;
                        // 取消
                        case DialogInterface.BUTTON_NEGATIVE:
                            break;
                    }
                }
            };

    // 標記訊息框點擊事件
    mMap.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
        @Override
        public void onInfoWindowClick(Marker marker) {
            // 如果是記事儲存的標記
            if (marker.equals(itemMarker)) {
                AlertDialog.Builder ab = new AlertDialog.Builder(MapsActivity.this);

                ab.setTitle(R.string.title_update_location)
                        .setMessage(R.string.message_update_location)
                        .setCancelable(true);

                ab.setPositiveButton(R.string.update, listener);
                ab.setNeutralButton(R.string.clear, listener);
                ab.setNegativeButton(android.R.string.cancel, listener);

                ab.show();
            }
        }
    });

    // 標記點擊事件
    mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
        @Override
        public boolean onMarkerClick(Marker marker) {
            // 如果是目前位置標記
            if (marker.equals(currentMarker)) {
                AlertDialog.Builder ab = new AlertDialog.Builder(MapsActivity.this);

                ab.setTitle(R.string.title_current_location)
                        .setMessage(R.string.message_current_location)
                        .setCancelable(true);

                ab.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent result = new Intent();
                        result.putExtra("lat", currentLocation.getLatitude());
                        result.putExtra("lng", currentLocation.getLongitude());
                        setResult(Activity.RESULT_OK, result);
                        finish();
                    }
                });
                ab.setNegativeButton(android.R.string.cancel, null);

                ab.show();

                return true;
            }

            return false;
        }
    });
}

同樣在「MapsActivity」類別,參考下列的程式碼修改「addMarker」方法:

// 在地圖加入指定位置與標題的標記
private void addMarker(LatLng place, String title, String context) {
    BitmapDescriptor icon =
            BitmapDescriptorFactory.fromResource(R.mipmap.ic_launcher);

    MarkerOptions markerOptions = new MarkerOptions();
    markerOptions.position(place)
            .title(title)
            .snippet(context)
            .icon(icon);

    // 加入並設定記事儲存的位置標記
    itemMarker = mMap.addMarker(markerOptions);
}

完成所有工作了,在實體裝置執行應用程式,測試這一章完成的功能。

課程相關的檔案都可以GitHub瀏覽與下載。

http://github.com/macdidi5/Android-6-Tutorial

後續 >> Android 6 Tutorial 第五堂(1)廣播接收元件 – BroadcastReceiver 與 AlarmManager

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

nu92909801/23

你好,我是Android Studio的初學者,我想實做出這個單元的GPS定位,不過我想詢問「MyAndroidTutorial應用程式」是什麼?要在哪裡開?然後我到Android SDK Manager視窗後找不到「Extras 」...

尾玉11/25

請問
// 移動地圖到目前的位置
moveMap(latLng);
好像沒有moveMap這個函式要import甚麼

尾玉11/25

ItemActivity 是甚麼@@ 如何創建

尾玉11/25

可以看全部import 哪些嗎

熱門論壇文章

熱門技術文章