2011-04-27

在 Android 裡使用 Google Maps

最簡單的方法是用 WebView 連到 Google Maps,但卻是最糟糕的方式,因為有 MapView 可以用。

在 Android 裡使用 MapView,與一般的 Android 應用程式有些不一樣的地方。

Google Maps Library

首先就是 Android 平台沒有內建 Google Maps Library,所以有兩個地方要注意。

第一,在建立 Android 專案時,Build Target 得用 Google APIs,而非以前用的 Android 2.2。


第二,必須在 AndroidManifest.xml 裡宣告使用 Google Maps Library,否則會出現 ClassNotFoundException。
<application android:icon="@drawable/icon" android:label="@string/app_name">
   <activity ...>...</activity>
   <uses-library android:name="com.google.android.maps"/>
</application>
這樣就可以在 XML 裡使用 MapView 了。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <com.google.android.maps.MapView
        android:id="@+id/gmap"
        android:apiKey="..."
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true"/>
</LinearLayout>
因為 MapView 不是 Android 內建的 class,所以在 XML 設定裡得用全名,就是 com.google.android.maps.MapView,為什麼不直接內建 MapView 到 Android 呢?簡單說就是,Android 是 Open source,而 Google Maps 是 Google 的(Android 的 Google 專案:Google API),親兄弟還是要明算帳。

再來就是要用 MapActivity 取代原本的 Activity,否則會出現「MapViews can only be created inside instances of MapActivity.」的 exception,MapActivity 處理了連網取資料、資料暫存、activity life cycle 等等重要的動作。
public class Gmap extends MapActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }
}
完成後,在 Emulator 上執行,出現「No compatible targets were found. Do you wish to add a new Android Virtual Device?」的訊息,原因是 MapView 用的 target 是 Google APIs,而非原來的 Android 2.2,馬上來建一個 AVD。


重新啟動,emulator ok了,但是只看到一張 Google 方格紙,沒看到地圖,在 LogCat 裡看到錯誤訊息「Couldn't get connection factory client」,原來是忘了在 MapView 裡加上 Google Maps API Key。

取得 Android emulator 專用的 Google Maps API Key

取得 Android 使用的 Google Maps API Key 方法跟網頁用的 API Key 的方法不太一樣,網頁用的只要提供網址就可以,Android 用的得提供「Certificate's MD5 fingerprint」,先不管這是什麼,重點是要怎麼取得?請參考 Getting the MD5 Fingerprint of the SDK Debug Certificate,執行以下的命令:
keytool -list -alias androiddebugkey -keystore <User Home>\.android\debug.keystore -storepass android -keypass android
要將 <User Home> 替換掉,這樣就可以取得「認證指紋 (MD5)」,然後到 Sign Up for the Android Maps API,輸入認證指紋就可以取得 API Key,最後將 API Key 填入 MapView 裡的 android:apiKey 就可以了往下一步前進了。

Android 應用程式的權限管理

重新啟動 emulator,還是只看到一張 Google 方格紙,原來是沒有開放 Android 應用程式的網路存取權限,開啟 AndroidManifest.xml 加入以下三個設定。
<manifest ...>
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
  <uses-permission android:name="android.permission.INTERNET"/>
  <application ...</application>
</manifest>
因為 MapActivity 會將檔案 cache 起來,所以需要開放檔案存取權限,就是前兩個設定,最後一個是開放網路存取權限。

地圖終於出現了!


我在哪裡?

Google API 裡有關 Google Maps 的主要 class 有三:
  • MapView 提供所有跟地圖設定相關的 API 可以使用,如是否顯示放大縮小工具、切換為衛星地圖等等。
  • MapController 控制地圖的縮放與移動。 
  • MyLocationOverlay 則是結合 GPS 提供目前所在的相關資訊。
在地圖上標記我的位置,並且隨手機移動而移動地圖中心點。
    public class Gmap extends MapActivity implements OnClickListener {
    
        private MapView mapView;
        private MapController mapCtrl;
        private MyLocationOverlay myLoc;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            this.mapView = (MapView) this.findViewById(R.id.gmap);
            this.mapCtrl = mapView.getController();
            // 顯示縮放工具
            this.mapView.setBuiltInZoomControls(true);
            // 設定 Button 功能
            ((Button) this.findViewById(R.id.streetViewBtn)).setOnClickListener(this);
            ((Button) this.findViewById(R.id.satelliteViewBtn)).setOnClickListener(this);
            // 初始化 MyLocationOverlay
            this.myLoc = new MyLocationOverlay(this, this.mapView);
            // 註冊每當位置改變時執行的 runnable
            myLoc.runOnFirstFix(new Runnable() {
    
                @Override
                public void run() {
                    // 移動地圖中心點到目前的位置
                    mapCtrl.animateTo(myLoc.getMyLocation());
                }
            });
            // 將目前位置標記
            this.mapView.getOverlays().add(myLoc);
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            // Android 應用程式背景執行時,關閉位置偵測
            this.myLoc.disableMyLocation();
            this.myLoc.disableCompass();
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            // Android 應用程式前景執行時,啟動位置偵測
            this.myLoc.enableMyLocation();
            this.myLoc.enableCompass();
        }
    
        @Override
        public void onClick(View v) {
            // 使用主 activity 實做 OnClickListener 以改進效能
            switch (v.getId()) {
            case R.id.streetViewBtn:
                this.mapView.setSatellite(false);
                break;
            case R.id.satelliteViewBtn:
                this.mapView.setSatellite(true);
                break;
            }
        }
    
        @Override
        protected boolean isRouteDisplayed() {
            return false;
        }
    }
    XML 設定檔,利用 layout_weight 將 Button 放在最下方,剩餘空間全做地圖顯示用。
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <com.google.android.maps.MapView
            android:id="@+id/gmap"
            android:apiKey="0A...A"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="1"
            android:clickable="true"/>
        <LinearLayout 
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0">
            <Button
                android:id="@+id/streetViewBtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="街道"/>
            <Button
                android:id="@+id/satelliteViewBtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="衛星"/>
        </LinearLayout>
    </LinearLayout>

    事實上,在 emulator 上執行上面的程式是不會有作用的,因為 emulator 沒有 GPS 可以用。

    貼心的 Google 在 Eclipse 提供了一個 Emulator Control View,透過這個 View,就可以任意輸入手機座標來驅動上面的範例。

    5 則留言:

    1. 請問我在開啟管理權限的地方會出現錯誤
      是為什麼呢??

      回覆刪除
    2. 請問我在開啟管理權限那裡會出現錯誤
      是為什麼呢?

      回覆刪除
    3. 請問是在開啟 AndroidManifest.xml 時出現錯誤的嗎?是檔案開不起來嗎?如果是這樣,請參考 http://cw1057.blogspot.com/2011/03/orgeclipsecoreruntimecoreexception.html 這篇文章。

      如果不是這個問題,或者你可以提供詳細一點的資訊,如錯誤訊息。

      回覆刪除
    4. 謝謝你!這篇文章還蠻清楚的^^

      回覆刪除
    5. Android 應用程式的權限管理
      執行後 地圖沒有出現??
      是為什麼??

      回覆刪除