2015年4月24日

使用 Google Maps Adnroid API v2 完整客製化地圖(一)


由於網路與手機的快速成長,LBS 功能已經漸漸滲入我們的生活中,外出導航、查看門市據點、分享自己的位置資訊等應用越來越多,客製化的要求也越來越高,下面就來解說一下如何完整的客製化 google map
Google Maps Android v1 API 已在 2012/12/3 停用,取代之的就是 Google Maps Adnroid API v2 使用 fragment 或 mapview 來顯示 google map。其實還有 Google Maps JavaScript API v3 做法是在 android webview 中嵌入 html 檔案來顯示 google map,google map 的相關設定當然就是使用 JavaScript 啦!
因為 Google Maps JavaScript API v3 還不是很穩定,所以這裡還是使用舊版的 Google Maps Adnroid API v2 來實現客製化 google map

硬體需求及限制

  • android 2.3.3 ( 含 ) 以上系統
  • Android Devices 必須有安裝 Google Service Framework ( 安裝 Google Play Store 時會自動安裝)
  • 如果你的 Android Device 目前 run 的版本低於 4.0.4 的話,你就需要申請一個 Google Account

註冊流程

(1) 建立一個 Google Account ( 如果已經有的可以跳過 )
(2) 建立一個 project ( 要使用已經存在的 project 也可以 )

(3) 切到 APIs 頁籤,將 Google Maps Android API v2 與 Google Places API 服務打開



(4) 建立 android api key
※ 注意!keystore 有分 debug / release,如果你只是用 IDE 在手機直接執行,請使用 debug;打包上架才換到 release。如果你上架跟 IDE debug 使用同一個 keystore 就沒有這個問題
※ 補充:如何查看 IDE debug keystore ( 以 eclipse 為例 ),Window → Prefences → 左邊展開 Android → Build 如下圖





(6) 建立 server key

設置限定可使用此 api key 的 ip address,不限制則不需輸入
※ 注意,請先記住 server api key,之後搜尋 google 地標需要使用他

程式解說

  1. 打開 Android SDK 下載 Google Play Services
  2. 將下面檔案複製到 workspace 中,然後 import 至 eclipse,並設定為 library
    
    C:\android-sdk目錄\extras\google\googleplayservices\libproject\google-play-services_lib
  3. 新建專案並引入上述的 library
  4. 在 Manifest.xml 中加入以下權限
    
    <!-- 存取 internet -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- 存取網路狀態 -->
    <uses-permission android:name="android.permission.ACCESSNETWORKSTATE" />
    <!-- 存取 SD卡檔案(google map需要) -->
    <uses-permission android:name="android.permission.WRITEEXTERNALSTORAGE" />
    <!-- google map -->
    <uses-permission android:name="tw.com.maxkit.simple.android.testgooglemap.permission.MAPSRECEIVE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READGSERVICES" />
    <uses-permission android:name="android.permission.ACCESSCOARSELOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINELOCATION" />
    <uses-permission android:name="android.permission.GETACCOUNTS" />
  5. 在 Manifest.xml 中加入以下資訊
    ※ 注意! " com.google.android.maps.v2.APIKEY " 的 value 請換成剛剛申請的 API key 不然會看不到地圖!
    
    <meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/googleplayservicesversion" />
    <meta-data
    android:name="com.google.android.maps.v2.APIKEY"
    android:value="AIzaSyBTfr91s8THATE03xYbWAzWwgUyQezfrzg" />  //剛剛申請的 API key
  6. 在畫面的 xml 中加入一個 SupportMapFragment 用來顯示地圖
    
    <fragment
    android:id="@+id/frglbsmap"
    android:layoutwidth="fillparent"
    android:layoutheight="fillparent"
    android:layoutbelow="@+id/btnlbs_click"
    class="com.google.android.gms.maps.SupportMapFragment" />
  7. 在 activity 中加入以下程式碼,地圖就出來了
    
    // 初始化
    private void initServerData() {
    //檢測 device 是否有安裝 google play services,且  google play services 版本是否符合需求
    int status = GooglePlayServicesUtil
      .isGooglePlayServicesAvailable(getBaseContext());
    if (status != ConnectionResult.SUCCESS) {
     int requestCode = 10;  //google map 最低 google play services 需求為 api 10
     Dialog dialog = GooglePlayServicesUtil.getErrorDialog(status, this,
       requestCode);
     dialog.show();
    } else {
     mGoogleMap = frglbsmap.getMap();
     mGoogleMap.setMyLocationEnabled(true);  //顯示自己的位置
     LocationManager locationManager = (LocationManager) getSystemService(LOCATIONSERVICE);
     if (locationManager.isProviderEnabled(LocationManager.GPSPROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORKPROVIDER)) {
      //如果GPS或網路定位開啟,更新位置
      Criteria criteria = new Criteria();
      String provider = locationManager.getBestProvider(criteria, true);  //取得定位裝置 ()
      Location location = locationManager.getLastKnownLocation(provider);
      if (location != null) {
       onLocationChanged(location);
      }
      locationManager.requestLocationUpdates(provider, 50000, 0, this);
     } else {
      Toast.makeText(MainActivity.this, "請打開定位功能", Toast.LENGTHLONG).show();
     }
    }
    }
    
  8. 在 activity 中 implements LocationListener,並加入以下程式碼,地圖就會自動更新畫面了
    
    @Override
    public void onLocationChanged(Location location) {
    mLatitude = location.getLatitude();
    mLongitude = location.getLongitude();
    LatLng latLng = new LatLng(mLatitude, mLongitude);
    mGoogleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng));
    mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(15));
    // 在map上加上圖示(黃色小人)
    if (imageOverlay != null) {
     imageOverlay.remove();
    }
    addMyLocationIcon(latLng);  //在 map上增加自己的位置
    }
    @Override
    public void onProviderDisabled(String provider) {
    }
    @Override
    public void onProviderEnabled(String provider) {
    }
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

錯誤訊息

有寫過 android 的應該都知道,如果出現錯誤訊息,通常我們會先看關鍵字 " Caused by ",他可以幫你快速的找到問題點,下面就來看看幾個錯誤訊息。
  1. No resource found that matches the given name...
    請更新 google play service lib (你引用的 lib )
    
    [2015-04-22 11:09:49 - TestGoogleMap] D:\workspace\generalclient\TestGoogleMap\AndroidManifest.xml:36: error: Error: No resource found that matches the given name (at 'value' with value '@integer/googleplayservicesversion').
  2. Error inflating class fragment
    如果你確定你的 fragment 是沒有問題的,請在仔細往下看看是否有其他錯誤訊息。就下面的例子來看,他其實還有一個錯誤 " The Maps API requires the additional following permissions to be set in the AndroidManifest.xml " 也就是權限的部分,加上權限後請 clean 再重新執行看看
    
    04-22 18:13:55.992: E/AndroidRuntime(616): FATAL EXCEPTION: main
    04-22 18:13:55.992: E/AndroidRuntime(616): java.lang.RuntimeException: Unable to start activity ComponentInfo{tw.com.maxkit.simple.android.testgooglemap/tw.com.maxkit.simple.android.testgooglemap.activity.MainActivity}: android.view.InflateException: Binary XML file line #14: Error inflating class fragment
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread.access$600(ActivityThread.java:130)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.os.Handler.dispatchMessage(Handler.java:99)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.os.Looper.loop(Looper.java:137)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread.main(ActivityThread.java:4745)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at java.lang.reflect.Method.invokeNative(Native Method)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at java.lang.reflect.Method.invoke(Method.java:511)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at dalvik.system.NativeStart.main(Native Method)
    04-22 18:13:55.992: E/AndroidRuntime(616): Caused by: android.view.InflateException: Binary XML file line #14: Error inflating class fragment
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:704)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.rInflate(LayoutInflater.java:746)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.inflate(LayoutInflater.java:489)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.android.internal.policy.impl.PhoneWindow.setContentView(PhoneWindow.java:256)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.Activity.setContentView(Activity.java:1867)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at tw.com.maxkit.simple.android.testgooglemap.activity.MainActivity.onCreate(MainActivity.java:54)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.Activity.performCreate(Activity.java:5008)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
    04-22 18:13:55.992: E/AndroidRuntime(616):  ... 11 more
    04-22 18:13:55.992: E/AndroidRuntime(616): Caused by: java.lang.SecurityException: The Maps API requires the additional following permissions to be set in the AndroidManifest.xml to ensure a correct behavior:
    04-22 18:13:55.992: E/AndroidRuntime(616): <uses-permission android:name="android.permission.WRITEEXTERNALSTORAGE"/>
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.maps.api.android.lib6.c.ck.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.maps.api.android.lib6.c.i.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.maps.api.android.lib6.c.el.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.maps.api.android.lib6.c.ab.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.maps.api.android.lib6.c.aa.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.maps.internal.x.onTransact(SourceFile:107)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.os.Binder.transact(Binder.java:326)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.maps.internal.IMapFragmentDelegate$a$a.onCreateView(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.maps.SupportMapFragment$a.onCreateView(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.dynamic.a$4.b(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.dynamic.a.a(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.dynamic.a.onCreateView(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at com.google.android.gms.maps.SupportMapFragment.onCreateView(Unknown Source)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.Fragment.performCreateView(Fragment.java:1786)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:920)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1104)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.FragmentManagerImpl.addFragment(FragmentManager.java:1206)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.FragmentManagerImpl.onCreateView(FragmentManager.java:2159)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.support.v4.app.FragmentActivity.onCreateView(FragmentActivity.java:297)
    04-22 18:13:55.992: E/AndroidRuntime(616):  at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:676)
    04-22 18:13:55.992: E/AndroidRuntime(616):  ... 21 more
  3. provider==null
    這其實可以看成是一個 NullPointException,就像字面上說的一樣,provider 是 null 但是你沒檢查卻直接拿來使用了,結果當然是閃退!那為什麼 provider 是 null 呢?因為你 device 的 location 沒開... 基本上這個問題只能增加提示阻擋,因為你沒辦法幫使用者開啟定位服務,這是不被允許的!
    
    04-22 18:27:07.476: E/AndroidRuntime(2053): FATAL EXCEPTION: main
    04-22 18:27:07.476: E/AndroidRuntime(2053): java.lang.RuntimeException: Unable to start activity ComponentInfo{tw.com.maxkit.simple.android.testgooglemap/tw.com.maxkit.simple.android.testgooglemap.activity.MainActivity}: java.lang.IllegalArgumentException: provider==null
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread.access$600(ActivityThread.java:130)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.os.Handler.dispatchMessage(Handler.java:99)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.os.Looper.loop(Looper.java:137)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread.main(ActivityThread.java:4745)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at java.lang.reflect.Method.invokeNative(Native Method)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at java.lang.reflect.Method.invoke(Method.java:511)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at dalvik.system.NativeStart.main(Native Method)
    04-22 18:27:07.476: E/AndroidRuntime(2053): Caused by: java.lang.IllegalArgumentException: provider==null
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.location.LocationManager.getLastKnownLocation(LocationManager.java:1159)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at tw.com.maxkit.simple.android.testgooglemap.activity.MainActivity.initServerData(MainActivity.java:86)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at tw.com.maxkit.simple.android.testgooglemap.activity.MainActivity.onCreate(MainActivity.java:60)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.Activity.performCreate(Activity.java:5008)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
    04-22 18:27:07.476: E/AndroidRuntime(2053):  ... 11 more
  4. Failed to load map. Error contacting Google servers. This is probably an authentication issue...
    
    04-23 09:21:01.765: E/Google Maps Android API(3209): Failed to load map. Error contacting Google servers. This is probably an authentication issue (but could be due to network errors).
  5. This IP, site or mobile application is not authorized to use this API key
    請確認你的 API server key 是否正確,key 是否已經啟用 ( ConfigUtil 中的 APIKEYGOOGLEMAP )
    { "errormessage" : "This IP, site or mobile application is not authorized to use this API key.",   "htmlattributions" : [],   "results" : [],   "status" : "REQUESTDENIED"}
  6. This API project is not authorized to use this API. Please ensure that this API is activated in the APIs Console
    請確認你是否已啟用 Google Places API
    
    {   "error_message" : "This API project is not authorized to use this API. Please ensure that this API is activated in the APIs Console: Learn more: https://code.google.com/apis/console",   "htmlattributions" : [],   "results" : [],   "status" : "REQUESTDENIED"}
  7. Could not find class 'android.app.AppOpsManager'
    這邊暫不處理,AppOpsManager 是 API 19 ( android 4.4.2 才出來的 class ),目前沒有使用到,這是系統自動拋出的訊息,不影響 map 使用
    
    04-23 09:21:02.140: E/dalvikvm(3209): Could not find class 'android.app.AppOpsManager', referenced from method com.google.android.gms.common.hg.a

客製化的部分請參考 使用 Google Maps Adnroid API v2 完整客製化地圖(二)

Refecnes

範例下載
※ 執行前請先將 Manifest.xml 中的 com.google.android.maps.v2.APIKEY ( android api key ) 與 ConfigUtil 中的 APIKEYGOOGLEMAP ( server api key ) 換掉