Android + Google Maps 建國科技大學 資管系 饒瑞佶 2017/3 V1
建立新Android專案
選擇Google Maps Activity
專案建立後 檔案google_maps_api.xml內有申請API Key的網址 最後API KEY填入這
申請API Key
建立憑證
完成API KEY建立 專案要用的API Key
確認資訊
將API Key貼入專案內的google_maps_api.xml
AndroidManifest.xml
準備一個座標點 緯度 經度
activity_maps.xml
取得layout上對應的fragment MainActivity.java 取得layout上對應的fragment
onMapReady-當Google Map服務載入後
將地標改成前面取得的座標 改成我們自己的 預設在雪梨
執行如果有錯誤 修改build.gradle(app) 加入:multiDexEnabled = true
執行 確認基本的地圖元件沒問題
放大地圖 mMap.animateCamera(CameraUpdateFactory.zoomTo(16));
執行
加入功能選項
執行 旋轉地圖時出現
改用自訂marker 原來預設的紅氣球
LatLng place = new LatLng(24.066516, 120.549871); //建國科大 BitmapDescriptor icon = BitmapDescriptorFactory.fromResource(R.drawable.school); MarkerOptions markerOptions = new MarkerOptions(); markerOptions.position(place) .title("學校") .snippet("建國科技大學") .icon(icon); mMap.addMarker(markerOptions);
執行
畫線
// 畫線 ArrayList<LatLng> points= new ArrayList<>(); // 所有點集合 PolylineOptions lineOptions = new PolylineOptions(); // 多邊形 double lat = 24.066516; // 建國科大 double lng = 120.549871; LatLng position = new LatLng(lat, lng); points.add(position); // 加入座標點到points物件 double lat1 = 24.050731; // 鹿港龍山寺 double lng1 = 120.435614; LatLng position1 = new LatLng(lat1, lng1); points.add(position1); // 加入座標點到points物件 lineOptions.addAll(points); // 加入所有座標點到多邊形 lineOptions.width(10); lineOptions.color(Color.RED); if(lineOptions != null) { mMap.addPolyline(lineOptions); // 畫出多邊形 } else { Log.d("onPostExecute","draw line error!");
執行
設定地圖點擊事件 setOnMapClickListener
加入點擊後儲存座標點的ArrayList
加入點擊地圖後,在點擊位置標出marker
// 設定點擊事件 mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() { @Override public void onMapClick(LatLng latLng) { if (MarkerPoints.size() > 1) { //超過一個點時,清空地圖版面 MarkerPoints.clear(); mMap.clear(); } // 將點擊位置加入MarkerPoints MarkerPoints.add(latLng); // 加入marker設定 MarkerOptions options = new MarkerOptions(); options.position(latLng); // 設定地點 if (MarkerPoints.size() == 1) { // 起始點顏色 options.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)); } else if (MarkerPoints.size() == 2) { // 後續點的顏色 options.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)); // 將點畫在地圖上 mMap.addMarker(options); });
改進兩點間連線,依照路畫線,而非直線
// 依照實際路畫出路徑圖 if (MarkerPoints.size() >= 2) { // 需要確定有2點 LatLng origin = MarkerPoints.get(0); // 第1個點 LatLng dest = MarkerPoints.get(1); // 第2個點 // 取得 Google Directions API String url = getUrl(origin, dest); // 組合呼叫API需要的URL FetchUrl FetchUrl = new FetchUrl(); // 解析URL回傳結果 FetchUrl.execute(url); // 移動座標視窗 mMap.moveCamera(CameraUpdateFactory.newLatLng(origin)); mMap.animateCamera(CameraUpdateFactory.zoomTo(16)); }
建立Google Directions URL
// 取得 Google Directions API private String getUrl(LatLng origin, LatLng dest) { // 路徑起點 String str_origin = "origin=" + origin.latitude + "," + origin.longitude; // 路徑終點 String str_dest = "destination=" + dest.latitude + "," + dest.longitude; // Sensor enabled String sensor = "sensor=false"; // 建立參數 String parameters = str_origin + "&" + str_dest + "&" + sensor; // 設定輸出格式 String output = "json"; // 建立完整的URL String url = "https://maps.googleapis.com/maps/api/directions/" + output + "?" + parameters; return url; }
解析URL回傳結果 解析JSON並畫出路徑
// 解析URL回傳結果 private class FetchUrl extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... url) { // 接收從Google Directions回傳的資料(JSON Format) String data = ""; try { data = downloadUrl(url[0]); //呼叫downloadUrl取得路徑json資料 } catch (Exception e) { Log.d("Background Task", e.toString()); } return data; protected void onPostExecute(String result) { super.onPostExecute(result); ParserTask parserTask = new ParserTask(); // 解析JSON 資料 parserTask.execute(result);
下載Google Directions回傳json資料
// 下載Google Directions回傳json資料 private String downloadUrl(String strUrl) throws IOException { String data = ""; InputStream iStream = null; HttpURLConnection urlConnection = null; try { URL url = new URL(strUrl); // 建立 http連線 urlConnection = (HttpURLConnection) url.openConnection(); // 連線 url urlConnection.connect(); // 讀取資料 iStream = urlConnection.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(iStream)); StringBuffer sb = new StringBuffer(); String line = ""; while ((line = br.readLine()) != null) { sb.append(line); } data = sb.toString(); br.close(); } catch (Exception e) { Log.d("Exception", e.toString()); } finally { iStream.close(); urlConnection.disconnect(); return data;
解析JSON並畫出路徑 需要DataParser類別 接下頁
接下頁
// Parsing the data in non-ui thread @Override // 解析JSON並畫出路徑 private class ParserTask extends AsyncTask<String, Integer, List<List<HashMap<String, String>>>> { // Parsing the data in non-ui thread @Override protected List<List<HashMap<String, String>>> doInBackground(String... jsonData) { JSONObject jObject; List<List<HashMap<String, String>>> routes = null; try { jObject = new JSONObject(jsonData[0]); DataParser parser = new DataParser(); // DataParser類別 // 開始解析 routes = parser.parse(jObject); } catch (Exception e) { e.printStackTrace(); } return routes; 接下頁
@Override protected void onPostExecute(List<List<HashMap<String, String>>> result) { ArrayList<LatLng> points; PolylineOptions lineOptions = null; // 取出路徑 for (int i = 0; i < result.size(); i++) { points = new ArrayList<>(); lineOptions = new PolylineOptions(); List<HashMap<String, String>> path = result.get(i); // 取出路徑上所有點 for (int j = 0; j < path.size(); j++) { HashMap<String, String> point = path.get(j); double lat = Double.parseDouble(point.get("lat")); double lng = Double.parseDouble(point.get("lng")); LatLng position = new LatLng(lat, lng); points.add(position); } // 將所有點加入lineOptions lineOptions.addAll(points); lineOptions.width(10); lineOptions.color(Color.RED); // 畫出路徑 if(lineOptions != null) { mMap.addPolyline(lineOptions); else { Log.d("onPostExecute","without Polylines drawn");
DataParser類別 public class DataParser { /** Receives a JSONObject and returns a list of lists containing latitude and longitude */ public List<List<HashMap<String,String>>> parse(JSONObject jObject){ List<List<HashMap<String, String>>> routes = new ArrayList<>() ; JSONArray jRoutes; JSONArray jLegs; JSONArray jSteps; try { jRoutes = jObject.getJSONArray("routes"); /** Traversing all routes */ for(int i=0;i<jRoutes.length();i++){ jLegs = ( (JSONObject)jRoutes.get(i)).getJSONArray("legs"); List path = new ArrayList<>(); 接下頁
接下頁 /** Traversing all legs */ for(int j=0;j<jLegs.length();j++){ jSteps = ( (JSONObject)jLegs.get(j)).getJSONArray("steps"); /** Traversing all steps */ for(int k=0;k<jSteps.length();k++){ String polyline = ""; polyline = (String)((JSONObject)((JSONObject)jSteps.get(k)).get("polyline")).get("points"); List<LatLng> list = decodePoly(polyline); /** Traversing all points */ for(int l=0;l<list.size();l++){ HashMap<String, String> hm = new HashMap<>(); hm.put("lat", Double.toString((list.get(l)).latitude) ); hm.put("lng", Double.toString((list.get(l)).longitude) ); path.add(hm); } routes.add(path); } catch (JSONException e) { e.printStackTrace(); }catch (Exception e){ return routes; 接下頁
接下頁 private List<LatLng> decodePoly(String encoded) { List<LatLng> poly = new ArrayList<>(); int index = 0, len = encoded.length(); int lat = 0, lng = 0; while (index < len) { int b, shift = 0, result = 0; do { b = encoded.charAt(index++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20); int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lat += dlat; shift = 0; result = 0; int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lng += dlng; LatLng p = new LatLng((((double) lat / 1E5)), (((double) lng / 1E5))); poly.add(p); } return poly; 接下頁
畫面加入其他物件
加入TextView
<LinearLayout xmlns:android="http://schemas. android xmlns:map="http://schemas.android.com/apk/res-auto" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" map:mapType="normal"> <TextView android:text="Google Maps 測試" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/textView" /> <fragment android:id="@+id/map" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_weight="1"/> </LinearLayout>
整合位置服務 Google Play Services Location API GPS或WiFi
準備工作 開啟Android SDK Manager,檢查「Extras -> Google Play services」 是否已經安裝。如果還沒有安裝的話,勾選並執行安裝的工作
修改或確認build.gradle(Module:app) 是否有:compile 'com.google.android.gms:play-services:x.x.x‘ 版本會依據寫程式的時間點而不同
修改ManifestAndroid.xml 加入: <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> 前面的Google Maps服務
修改ManifestAndroid.xml 加入: <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Android取得位置資料 使用Google Services下的Location API 先使用「com.google.android.gms.common.api」套件下的 「GoogleApiClient」,連線與使用Google Services服務 再使用「com.google.android.gms.location」套件下的 API,取得裝置目前的位置資訊
複製前面Google Maps使用的Activity 複製前面Google Maps使用的MainActivity.java,成為新的 GPSMainActivity.java,讓兩者不會互相干擾 GPSMainActivity.java中只要保留地圖與marker部分程式,原來畫 線與路徑規劃(Directions API)部分可以刪除 此處最重要的是取得目前地點座標(GPS或WiFi網路),將其以 marker方式顯示到Google Maps上
GPSMainActivity.java中只要保留地圖與marker部分程式 取消 後面會移到onResume 取消 只保留mMap
GPSMainActivity.java加入需要的物件宣告 // Location API // Google API用戶端物件 private GoogleApiClient googleApiClient; // Location請求物件 private LocationRequest locationRequest; // 取得裝置目前最新的位置 private Location currentLocation; // 目前位置對應的marker private Marker currentMarker; 前面的Google Maps服務
GPSMainActivity.java加入需要繼承 GoogleApiClient.ConnectionCallbacks與GoogleApiClient.OnConnectionFailedListener 需要加入對應的事件與方法
對應繼承需要加入的3個事件與方法
GPSMainActivity.java加入接收位置更新資訊的繼承 LocationListener 對應繼承需要加入的事件與方法
先使用「com. google. android. gms. common 先使用「com.google.android.gms.common.api」套件下的「GoogleApiClient」,連線與使用Google Services服務 建立Google API client物件,並在onCreate中呼叫
建立Google API client物件 // 建立Google API client物件 private synchronized void configGoogleApiClient() { googleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); } 該物件會執行ConnectionCallbacks與OnConnectionFailedListener兩個繼承對應的3個方法
再使用「com.google.android.gms.location」套件下的API,取得裝置目前的位置資訊 建立LocationRequest物件與呼叫
建立LocationRequest物件 // 建立Location request物件 private void configLocationRequest() { locationRequest = new LocationRequest(); // 設定讀取位置資訊更新的間隔時間為一秒(1000ms) locationRequest.setInterval(1000); // 設定從API讀取位置資訊的間隔時間為一秒(1000ms) locationRequest.setFastestInterval(1000); // 設定優先讀取高精確度的位置資訊(GPS) locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); }
修改「onConnected」、「onConnectionFailed」與「onLocationChanged」方法 在onConnected(也就是已經連線到Google Services後)加入啟動位 置更新服務,會對應到onLocationChanged事件 這裡需要搭配Android 6.0之後的Permission政策做修正
onConnected // 已經連線到Google Services @Override public void onConnected(@Nullable Bundle bundle) { // 啟動位置更新服務,位置資訊更新的時候,應用程式會自動呼叫LocationListener.onLocationChanged if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions // here to request the missing permissions, and then overriding // public void onRequestPermissionsResult(int requestCode, String[] permissions, // int[] grantResults) // to handle the case where the user grants the permission. See the documentation // for ActivityCompat#requestPermissions for more details. return; } LocationServices.FusedLocationApi.requestLocationUpdates( googleApiClient, locationRequest, GPSMapsActivity.this);
加入Permission詢問視窗 Android 6.0之後的Permission政策,雖然在Manifest.xml中有加入Permissions,但仍還是要求執行時需要出現詢問視窗,再次跟使用者確認!
int MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION; // 詢問視窗 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); // 接收permission詢問視窗回傳值 @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { Log.i("GPS", "requestCode=" + requestCode); switch (requestCode) { case 0: { // location if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! Do the } else { // permission denied, boo! Disable the } return;
onConnectionFailed方法
onConnectionFailed方法 // Google Services連線失敗,ConnectionResult參數是連線失敗的資訊 @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { int errorCode = connectionResult.getErrorCode(); // 裝置沒有安裝Google Play服務 if (errorCode == ConnectionResult.SERVICE_MISSING) { Toast.makeText(this, "裝置沒有安裝Google Play服務",Toast.LENGTH_LONG).show(); }
onLocationChanged方法
onLocationChanged方法 // 位置改變實會觸發,Location參數是目前的位置 @Override public void onLocationChanged(Location location) { currentLocation = location; Log.i("GPS", "onLocationChanged"); // 取得目前位置 LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); // 設定目前位置的marker if (currentMarker == null) { currentMarker = mMap.addMarker(new MarkerOptions().position(latLng)); } else { currentMarker.setPosition(latLng); } // 移動地圖到目前的位置 mMap.moveCamera(CameraUpdateFactory.newLatLng(latLng)); //放大地圖16倍 mMap.animateCamera(CameraUpdateFactory.zoomTo(16));
加入onResume、onPause與onStop @Override protected void onResume() { super.onResume(); if (mMap == null) { //取得地圖 ((SupportMapFragment) getSupportFragmentManager(). findFragmentById(R.id.map)).getMapAsync(this); } // 連線到Google API用戶端 if (!googleApiClient.isConnected()) { Log.i("GPS", "onResume"); googleApiClient.connect();
@Override protected void onPause() { super.onPause(); // 取消位置請求服務 if (googleApiClient.isConnected()) { LocationServices.FusedLocationApi.removeLocationUpdates( googleApiClient, this); } protected void onStop() { super.onStop(); // 移除Google API用戶端連線 googleApiClient.disconnect();
執行 onCreate onResume onConnected onLocationChanged onResume:取得地圖與GoogleApiClient onConnected:請求連線與使用Google Services服務 onLocationChanged:取得最新的位置
執行