2015年4月27日

Unicode Surrogate Pairs

特殊的中文字

這個字是個特別的中文字,它沒辦法在一般最通用的 UTF-8 網頁中被看見,也沒辦法儲存到一個定義為 UTF-8 encoding 的 MySQL Database 裡面。這個字在網頁上看不見,所以這篇文章的這個字都是用圖片貼上去的

講到 Unicode 的問題,又需要去查看一串字元編碼歷史,淺釋 Unicode 這篇文章把字元編碼的來龍去脈講得很清楚了,就不在這邊贅述。

如果要完全解決文字編碼的問題,其實就是使用 UTF-32 或是 UTF-16 就好了,但是現在討論一個重點,為什麼 UTF-8 在網頁世界中,會是最流行的編碼方式,原因就是大部分的作業系統與系統程式都是英美語系的工程師做的,為了跟 ASCII code 相容,如果也能夠使用一樣的程式就處理掉多國語言編碼,這樣就天下太平了,程式不需要改太多。

像我們這樣撰寫網頁程式的工程師,其實早已經習慣撰寫網頁,就使用 UTF-8 編碼,一般也不會想到會遇到超過 UTF-8 規範定義的字元,如果使用者硬是在網頁上輸入了 這個字,對於 Java 來說,他是可以接受的,因為 Java 語言內部的編碼格式是 UTF-16,但對於資料庫來說,當我們要把資料放進 table 時,得到的卻是一個 SQLException。

SQL Error: 1366, SQLState: 22001

使用 UTF-8,調整 DB 設定也沒用

遇到 SQLExcpetion 的問題,很直覺會先想到是不是資料庫的編碼寫錯了,還是 MySQL J-Connector 出了什麼問題,連線時忘了設定 encoding。

但因為根本的原因在於 Java 可以容許使用 UTF-16 的字,但是 MySQL Database 以及 Browser 的網頁都是使用 UTF-8,所以不管我們再怎麼改設定,或是想方法,其實都是徒勞無功的。

Surrogate Pair

再看一下這一段 java 程式,雖然鍵入了四個中文字的字串,但實際上,卻因為第三個字有著不同的特性,最後輸出時,就把第三個字排除掉了。

Java 的 Character.isHighSurrogate 與 Character.isLowSurrogate 可以用來判斷是不是符合了上面描述的規則的特殊字。

        String text = "測試字";

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            System.out.println("ch="+ch+":"+String.format("%02X", (int)ch)+"\n");
            if (!Character.isHighSurrogate(ch) && !Character.isLowSurrogate(ch)) {
                sb.append(ch);
            }
        }

        System.out.println("sb=" + sb.toString());

執行後輸出結果為

D:\temp>java Test4
ch=測:6E2C

ch=試:8A66

ch=?:D85D

ch=?:DE57

ch=字:5B57

sb=測試字

除了 這個字之外,其他的字都是 2 bytes,Java 內部的 UTF-16 會使用一個單位(2 byte)儲存 UCS-2 字碼,超過這個範圍的字碼則會拆解成「代理對(surrogate pair)」,用4 bytes儲存。

解決方案

  1. 忽略這個問題,畢竟遇到這些特殊字的機會並不多
  2. 儲存到 DB 前,使用上面的 isHighSurrogate 與 isLowSurrogate,將不合法的字元排除掉。
  3. 將 DB 與 網頁輸出都改為 UTF-16,但是 IE 沒有支援 UTF-16,只能用在 Chrome,不建議這樣做,因為 UTF-8 才是網頁的主流。

Reference

UTF-8 wiki

How to handle SQL state [HY000]; error code [1366]; Incorrect string value?

What is a surrogate pair in Java?

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 來顯示 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

程式解說

  1. 修改自己位置的 mark
    
    /**
     * 在 map上增加自己的位置
     * @param latLng
     */
    private void addMyLocationIcon(LatLng latLng) {
    GroundOverlayOptions newarkMap = new GroundOverlayOptions().image(
      BitmapDescriptorFactory.fromResource(R.drawable.man1))
      .position(latLng, 94, 200);
    imageOverlay = mGoogleMap.addGroundOverlay(newarkMap);
    }
    

  2. 使用 MarkerOptions 在 map 上顯示自訂地標,可設定地標圖示、名稱等資訊
    
    /**
     * 在map上顯示自訂地標
     * 
     * @param locationInfoList
     */
    public void parseLocationAndShowMap(List locationInfoList) {
    mGoogleMap.clear();
    mapInfoAdapter.setKeyword(false);
    // 標注據點位置 for (LocationInfo locationInfo : locationInfoList) { MarkerOptions markerOptions = new MarkerOptions(); double lat = Double.parseDouble(locationInfo.getLat()); double lng = Double.parseDouble(locationInfo.getLng()); LatLng latLng = new LatLng(lat, lng); markerOptions.position(latLng); String name = locationInfo.getName(); int icon = R.drawable.location; switch (locationInfo.getAtype()) { case 1: // 公司 icon = R.drawable.maxkit; break; case 2: // 客戶 icon = R.drawable.custom; break; case 3: // 自行車道 icon = R.drawable.bicycle; break; default: icon = R.drawable.location; break; } markerOptions.title(name); // map上icon點擊後顯示資料 markerOptions.icon(BitmapDescriptorFactory.fromResource(icon)); // 設定map上顯示的圖示 //建立地標 MarkerHelper markerHelper = new MarkerHelper(locationInfo); String snippet = GsonUtil.gson.toJson(markerHelper); markerOptions.snippet(snippet); mGoogleMap.addMarker(markerOptions); } LatLng latLng = new LatLng(mLatitude, mLongitude); addMyLocationIcon(latLng); }

  3. 繼承 InfoWindowAdapter 類別,並在 getInfoContents 中設定 mark 點擊後顯示畫面
    
    @Override
    public View getInfoContents(Marker marker) {
    // 連結xml中實體物件
    LayoutInflater inflater = (LayoutInflater) context
      .getSystemService(Context.LAYOUTINFLATERSERVICE);
    View infoWindow = inflater.inflate(R.layout.mapinfosbl, null);
    TextView tvtitle = (TextView)infoWindow.findViewById(R.id.tvmapInfotitle);
    TextView tvaddr = (TextView)infoWindow.findViewById(R.id.tvmapInfo_addr);
    TextView tvtel = (TextView)infoWindow.findViewById(R.id.tvmapInfotel);
    ImageView imgvpic = (ImageView)infoWindow.findViewById(R.id.imgvmapInfopic);
    MarkerHelper markerHelper = GsonUtil.gson.fromJson(marker.getSnippet(), MarkerHelper.class);
    //設定title tv_title.setText(marker.getTitle());
    //設定地址 String addr = markerHelper.getAddr(); if(TextUtils.isEmpty(addr)){ tvaddr.setText(""); }else{ tvaddr.setText(addr); }
    //設定電話 String tel = markerHelper.getTel(); if(TextUtils.isEmpty(tel)){ tvtel.setText(""); }else{ tvtel.setText(tel); }
    if(!this.isKeyword){ //關鍵字搜尋不用顯示圖片 imgvpic.setVisibility(View.VISIBLE); Integer path = markerHelper.getPic(); if(path!=null){ imgvpic.setImageResource(path); }else{ Log.i(ConfigUtil.TAG, "no image"); } }else{ imgv_pic.setVisibility(View.GONE); }
    return infoWindow; } @Override public View getInfoWindow(Marker arg0) { return null; }

  4. 設定 map 上 mark,點擊後顯示的畫面
    
    //地圖上 marker 點擊後彈出的畫面
    mapInfoAdapter = new MapInfoAdapter(MainActivity.this, false);
    mGoogleMap.setInfoWindowAdapter(mapInfoAdapter);
    

  5. 設定 InfoWindow 點擊後功能
    
    // map標識
    mGoogleMap.setOnInfoWindowClickListener(new OnInfoWindowClickListener() {
    @Override public void onInfoWindowClick(Marker arg0) { try { MarkerHelper markerHelper = GsonUtil.gson.fromJson( arg0.getSnippet(), MarkerHelper.class); Log.i(ConfigUtil.TAG, "phone:" + markerHelper.getTel()); // 撥打電話 Intent intent = new Intent(Intent.ACTION_CALL, Uri .parse("tel:" + markerHelper.getTel())); startActivity(intent); } catch (Exception e) { Log.e(ConfigUtil.TAG, "Exception:" + e); } } });

  6. 搜尋地標,可分為使用 type, keyword, radar 三種方式 ( 詳細內容可參考 Google Places API Web Service )
    (1) type:
    送出 https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=座標&radius=搜尋範圍&types=種類&sensor=true&key=server api key
    範例:
    
    //呼叫API
    https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=24.1674116,120.6679557&radius=5000&types=bank&sensor=true&key=AIzaSyC9nSWENfNXaPrQ9pMFtvxL5NSwbpMiEE
    
    //回傳 { "htmlattributions" : [], "nextpagetoken" : "CoQC8gAAAB6cvLyq0yHNFy6xoBRrpTVsUCLD2kVsvcSIrXiRRfrnvE9jFUzdq-sRLbKq4Q5bHCrcN1SwayQcDsvsADKuHbx5ucnVZa0l85iAUm9kzGtEiMK1NcJWiHxNe0REVWYwDAz0kXUSlu3rKfR26VNoBORPXxRcagaRfAKHZHqKTplmCOw0gZOR70drr6eZXFGhymSaZ5jSlaoQL9-bel4hfqZOaLekWYpjr9gSI7X-Op66wWpeFKYeVPleA6heArUDBsHt8js4ykSAPRk4I22ikcUonw98BxbZmgAjt6hQd8zuhs5v9QI4046TBpZ0oHNjGzTznWs9vB91BK1hTEAQSEGS91M0wqOcKJHBQBSHhMx8aFLzuXUD3ha6M1ob3VM4-uxeXps8N",
       "results" : [
      {
         "geometry" : {
            "location" : {
               "lat" : 24.161911,
               "lng" : 120.651707
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/genericbusiness-71.png",
         "id" : "48b7e2d33ccc3897eed4b98fa0ce3ad0654633c2",
         "name" : "星展銀行",
         "openinghours" : {
            "opennow" : false,
            "weekdaytext" : []
         },
         "placeid" : "ChIJex4xo49aTQRuHMhLV-kDc",
         "reference" : "CmRfAAAA0uofNZht6utbASwNmEpMxu39xdoEd9eU65BTDVCoNvbBBsPbYfyZYddBB3cB8PmTGKbJNRXXDu1AdTQorDhjpDKKbJdNHCGlqjx5LKd3RtLTM7ERbuhi-mvCIZTIHrIEhCtERO5biWmTqLdpx-047gDGhT-5qgakVk9fx9sjlRZUfOKpYCZIg",
         "scope" : "GOOGLE",
         "types" : [ "bank", "finance", "establishment" ],
         "vicinity" : "西屯區台中港路二段60之8號"
      },
      {
         "geometry" : {
            "location" : {
               "lat" : 24.147987,
               "lng" : 120.669588
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/genericbusiness-71.png",
         "id" : "e9adb41f5c16918b9ea7c9e198b5058b5484ff6f",
         "name" : "星展銀行",
         "openinghours" : {
            "opennow" : false,
            "weekdaytext" : []
         },
         "placeid" : "ChIJv2OHQ9aTQRknnvtT9f1us",
         "reference" : "CmRgAAAAt01TuRucNuYv88xttZlznmXKEHfFrdlanQZwkPrjcrVivtumLny9flPaZNy4YF366QsafHJWppMwUTx8bIMgvNrBOpbTTnmeOjLA8oKi4bFuhKq4xHXvYHHufTzjEEhCdP4J1C2m4S4aSfdqY9gbPGhQWkuEgceFvbqhBLyLM5qKBB2uoWw",
         "scope" : "GOOGLE",
         "types" : [ "bank", "finance", "establishment" ],
         "vicinity" : "西區民權路219號"
      },
      ...
      ...
      {
         "geometry" : {
            "location" : {
               "lat" : 24.139748,
               "lng" : 120.677998
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/genericbusiness-71.png",
         "id" : "ae07416012e09331be83ffbab77779628e0fecc8",
         "name" : "花旗(台灣)商業銀行ATM",
         "openinghours" : {
            "opennow" : false,
            "weekdaytext" : []
         },
         "placeid" : "ChIJq39fRI9aTQR1M2sdNU3nM",
         "reference" : "CnRwAAAA7GlK9vwBW-myUnXTnB2Tv-V5XJVHJQx91CE95VPiS5OJ5ayyCjD4g1x3yiLJcW-1v1bbOaABMXEJHycbXXSKoKzfIjoLlwXIl7l-aEg8-zjOlwjBliaCHsbImHFcJkcunjDhiwjiKhYS3qAiGYhIQDXjcoIVkaEaTsAwmVTRWdBoURb7EUTY-oV9ZCqc3iOvmcwSH9jU",
         "scope" : "GOOGLE",
         "types" : [ "atm", "bank", "finance", "establishment" ],
         "vicinity" : "中區民權路102號"
      }
    ],
    "status" : "OK"
    }
    

    (2) keyword:
    送出 https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=座標&radius=搜尋範圍&keyword=關鍵字&sensor=true&key=server api key
    範例:
    //呼叫 https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=24.1674116,120.6679557&radius=5000&keyword=自行車道&sensor=true&key=AIzaSyC9nSWENfNXaPrQ9pMFtvxL5NSwbpMiEE
    
    //回傳 { "htmlattributions" : [], "results" : [ { "geometry" : { "location" : { "lat" : 24.136734, "lng" : 120.697396 } }, "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/genericbusiness-71.png", "id" : "74d4245ed61187f907175744fe5dc6f7c8a4830c", "name" : "東光園道自行車道", "photos" : [ { "height" : 460, "htmlattributions" : [ "\u003ca href=\"https://plus.google.com/102113594474011668675\"\u003e林老頭\u003c/a\u003e" ], "photoreference" : "CuQB1wAAAGcJIdgm3ornEgoy9apsL7UjHTcOEYYIqn1cK55YGn4fajRwP4kKBr25PWm5e9RCKx9HEv0kg4gpTljSJAREsTIh9lOMuOjBxI6aQBP6vmUvaAVUXqfXrHHXoVG2BypVOqAeOYzg3sH1cWa3TM7HrbOUvwJ7VsnGSqMSgiIyrwzUYu-nTdUrGKGPMEeSC5i2eKs9PZhdaovZwogMrlr25yfEUH2C15jXkFtCikTHH7W-U6JYOicwAsHIqNPHqlpw-JuuOReGzxP1dt8rd-frUXqhFgYQR1X7k1KeiMYcEhCWlvdamFzPz0eQxPFkZcDwGhQ20ycIsDLytFbMsGshU4N-SCvNPw", "width" : 816 } ], "placeid" : "ChIJgfDC9Dg9aTQRyJmwVbF12E", "rating" : 3.8, "reference" : "CnRrAAAA8rDOWN4IVcqYfzLmpU4lKFFLTCXo339Cm6rC1wg7kAgrS8SpDdeZudwNqkBRzgGs-NqbNm5SZKMnfjzn49ZPYlaUw53Kq-T3WVs466PTKJ51Ia1O91iDrJq5SF1lIvAmvCwqBIb3BIC5UFVdBhIQE4XQTI07iOK2lD13zm1ZphoUFT7FDj4Cgk29BaxOgYULua5wgyk",
         "scope" : "GOOGLE",
         "types" : [ "establishment" ],
         "vicinity" : "東區東光園路446之1號"
      },
      {
         "geometry" : {
            "location" : {
               "lat" : 24.171131,
               "lng" : 120.704385
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/genericbusiness-71.png",
         "id" : "e4351c405528bfc1085af05445aaa25cedb428ed",
         "name" : "旱溪「親水式」自行車道",
         "placeid" : "ChIJJxfG4AXaTQRq2DOJE0wrDQ",
         "reference" : "CoQBdAAAAHiN0alkLB-3xj8Haqef71asoPLBDS9w-klUH1t1bQceMEl44sYtDG3iETmj97DBRyllHrgLBUYy2Pd66ds30kGs751xNA1FCOQvV6SqAV2tiwoAZ1R5XP1YkfnajraCGD3gXXmXLuQgx26mPvXwYyPwHyIQkFNJRbajsGub0EhDJHlsfFvE81ShEVDuqgzUFGhSy14J6FaRXHZn0rX1ttQFkffkmbg",
         "scope" : "GOOGLE",
         "types" : [ "establishment" ],
         "vicinity" : "台中市北屯區旱溪西路三段"
      },
      ...
      ...
      {
         "geometry" : {
            "location" : {
               "lat" : 24.157597,
               "lng" : 120.65739
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/placeapi/icons/restaurant-71.png",
         "id" : "fd211ebe704db2dc4889e293768a2c98d461d784",
         "name" : "燒烤王",
         "photos" : [
            {
               "height" : 1224,
               "htmlattributions" : [
                  "\u003ca href=\"https://plus.google.com/117051252423976960156\"\u003e葵小\u003c/a\u003e"
               ],
               "photoreference" : "CnRoAAAAak-SYbk7I2xCIeNUONEVbjK4DpcxD5Qd4b45skXbLARZ2DrY1Gq2vQDOf7Ygc1Zn2k2ejpUvbc7xrfqBHdwg8tSoK2aPIhj3pRPCipbKPPm9DirAveduwC5fJm0xbgihevYSSCaHaXvsOzVn5ThIQ46Hl6qTeGvOgI8wkRvGN-hoUkuVqKONKwuid4QCeup5pqxgGEk",
               "width" : 1632
            }
         ],
         "placeid" : "ChIJfYieKps9aTQR7X7CLRNl940",
         "rating" : 3.9,
         "reference" : "CmRdAAAAj2ixVlTCAPfNZlwSfsfIS1Sh1n-5Pj6nyCGYqe5ruArp9lYPS-neb0UOnMeG0WBlKIHdDNW84eUht3KXIKyliRuqRWgcrfstRghXwNT-sqg5QSug3mMyqAnEcUxhEhA0T0qNLxjHmiyc1BgwbUchGhQDJw8fYPLzbjcpHex2ESjQu-V4A",
         "scope" : "GOOGLE",
         "types" : [ "restaurant", "food", "establishment" ],
         "vicinity" : "西屯區精誠路8號"
      }
    ],
    "status" : "OK"
    }
    

執行結果截圖




Refecnes

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

使用 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 ) 換掉