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
MainActivity.java 取得 layout 上對應的 fragment
onmapready- 當 Google Map 服務載入後 取得 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; @Override 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 類別 接下頁
接下頁
// 解析 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; do { b = encoded.charat(index++) - 63; result = (b & 0x1f) << shift; shift += 5; while (b >= 0x20); 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.com/apk/res/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_width="match_parent" android:layout_height="wrap_content" 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.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); @Override protected void onstop() { super.onstop(); // 移除 Google API 用戶端連線 if (googleapiclient.isconnected()) { googleapiclient.disconnect();
執行 oncreate onresume onconnected onlocationchanged onresume: 取得地圖與 GoogleApiClient onconnected: 請求連線與使用 Google Services 服務 onlocationchanged: 取得最新的位置
執行