多应用+插件架构,代码干净,支持一键云编译,码云点赞13K star,4.8-4.12 预售价格198元 广告
目前的行動裝置大部份都有衛星定位的設備,在戶外適當的環境下,可以從衛星接收到精確度很高的位置資訊。在室內或遮閉物比較多的環境,Android系統也可以從網路或電信服務,讀取誤差比較大一些的位置資訊。應用程式可以儲存使用這些位置資訊,例如記錄與儲存目前的位置,在地圖元件中查詢與規劃路徑。 這一章說明最新的Google Services Location API,跟傳統的作法比較,這是一種比較省電與方便的技術,應用程式可以根據自己的需求,讀取需要的位置資訊。目前已經為記事應用程式完成地圖元件,現在為應用程式加入讀取與儲存目前位置資訊的功能,如果記事資料已經儲存位置資訊,可以在地圖檢視與管理: [![AndroidTutorial5_04_03_01](https://box.kancloud.cn/2015-08-17_55d1bb919bf49.png)](https://box.kancloud.cn/2015-08-17_55d1bb919bf49.png) [![AndroidTutorial5_04_03_02](https://box.kancloud.cn/2015-08-17_55d1bb960e162.png)](https://box.kancloud.cn/2015-08-17_55d1bb960e162.png) 開啟還沒有儲存位置資訊的記事資料,可以在地圖檢視與儲存目前的位置: [![AndroidTutorial5_04_03_03](https://box.kancloud.cn/2015-08-17_55d1bb9af21e1.png)](https://box.kancloud.cn/2015-08-17_55d1bb9af21e1.png) [![AndroidTutorial5_04_03_04](https://box.kancloud.cn/2015-08-17_55d1bb9d494ed.png)](https://box.kancloud.cn/2015-08-17_55d1bb9d494ed.png) ## 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)」檔案的內容,必須選擇「Sync Project」執行同步的工作。 6. 開啟「ManifestAndroid.xml」,參考下面的內容,檢查在application標籤下是否已經加入需要的設定: ~~~ <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> ~~~ 7. 同樣在「ManifestAndroid.xml」,參考下面的內容,檢查在manifest標籤下是否已經加入需要的設定: ~~~ <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」,參考下列的程式片段,讓地圖元件類別實作需要的介面: ~~~ public class MapsActivity extends FragmentActivity implements ConnectionCallbacks, 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」,參考下列的程式片段,讓地圖元件類別實作需要的介面: ~~~ public class MapsActivity extends FragmentActivity implements ConnectionCallbacks, 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) { ... // 建立Google API用戶端物件 configGoogleApiClient(); // 建立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(); setUpMapIfNeeded(); // 連線到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 傳送、接收與儲存位置資訊 完成地圖元件基本的工作以後,現在為記事元件加入處理位置資訊的工作。開啟「ItemActivity」,在「clickFunction」方法修改啟動地圖元件的程式碼,傳送記事資料儲存的位置資訊給地圖元件: ~~~ public void clickFunction(View view) { int id = view.getId(); switch (id) { ... case R.id.set_location: // 啟動地圖元件用的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); 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用戶端物件 configGoogleApiClient(); // 建立Location請求物件 configLocationRequest(); // 讀取記事儲存的座標 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); } else { // 連線到Google API用戶端 if (!googleApiClient.isConnected()) { googleApiClient.connect(); } } } ~~~ 地圖元件需要提供使用者選擇標記與訊息框的操作功能,在「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; } }); } ~~~ 調整之前為了測試地圖元件加入的程式碼,包含「setUpMapIfNeeded」與「setUpMap」兩個方法: ~~~ private void setUpMapIfNeeded() { if (mMap == null) { mMap = ((SupportMapFragment) getSupportFragmentManager(). findFragmentById(R.id.map)).getMap(); if (mMap != null) { // 移除地圖設定 //setUpMap(); processController(); } } } // 移除地圖設定方法 private void setUpMap() { // 建立位置的座標物件 LatLng place = new LatLng(25.033408, 121.564099); // 移動地圖 moveMap(place); // 加入地圖標記 addMarker(place, "Hello!", " Google Maps v2!"); } ~~~ 參考下列的程式碼修改「addMarker」方法: ~~~ // 在地圖加入指定位置與標題的標記 private void addMarker(LatLng place, String title, String snippet) { BitmapDescriptor icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_launcher); MarkerOptions markerOptions = new MarkerOptions(); markerOptions.position(place) .title(title) .snippet(snippet) .icon(icon); // 加入並設定記事儲存的位置標記 itemMarker = mMap.addMarker(markerOptions); } ~~~ 完成所有工作了,在實體裝置執行應用程式,測試這一章完成的功能。