2012年8月28日 星期二

第一支 Android App 上架,從無到有的全記錄 (4)

一個跟地圖有關的 App, 沒有地圖上的標記也是很奇怪的。所以,接下來我們要在地圖上加兩種標記。

首先我們要加上 Google Map API 提供的 MyLocationOverlay。

在 Main Activity 中宣告一個變數

private MyLocationOverlay m_MyLocationOverlay;

在 onCreate 函式裡,產生這個 Overlay,並且加入到 Map View 中。

m_MyLocationOverlay = new MyLocationOverlay(this, m_MapView);
m_MapView.getOverlays().add(m_MyLocationOverlay);

/*m_MyLocationOverlay.runOnFirstFix(new Runnable() {
    public void run() {
        m_MapView.getController().animateTo(m_MyLocationOverlay.getMyLocation());
    }
});*/

有一段程式碼註解起來了,這段程式碼的用意在,當我們取得 Location 後,把 Map View 上的地圖,搬移到我們所在的位置上。但是因為我們在 GeoUpdateListener 中也有相同的功能,所以,這段程式碼其實可有可無。

接著,在 Main Activity 上,我們覆寫兩個函式。

@Override
protected void onResume() {
    super.onResume();
    m_MyLocationOverlay.enableMyLocation();
    m_MyLocationOverlay.enableCompass();
}

@Override
protected void onPause() {
    super.onPause();
    m_MyLocationOverlay.disableMyLocation();
    m_MyLocationOverlay.disableCompass();
}

現在可以 Build 來測試看看。

device-2012-08-28-112023

如果用模擬器來跑的話,記得從模擬器上送出位置。

接下來,我們要來加上我們自己的標記。

首先,我抓了一個免費的藍色圖釘圖示。pin_blue加到 Android 專案的 resource 裡,名稱叫做 pin_blue。

然後,我們從 Map API 中的 ItemizedOverlay 抽象介面,實做一個叫做 PriLocOverlays 的類別。

public class PriLocOverlays extends ItemizedOverlay<OverlayItem>
{
    private static int MAXNUM = 50;
    private OverlayItem m_Overlays[] = new OverlayItem[MAXNUM];
    private int m_nCount = 0;

    public PriLocOverlays(Drawable defaultMarker) {
        super(boundCenterBottom(defaultMarker));
        // TODO Auto-generated constructor stub
    }

    @Override
    protected OverlayItem createItem(int i) {
        // TODO Auto-generated method stub
        return m_Overlays[i];
    }

    @Override
    public int size() {
        // TODO Auto-generated method stub
        return m_nCount;
    }

    public void addOverlay(OverlayItem overlay)
    {
        if (m_nCount < MAXNUM)
        {
            m_Overlays[m_nCount] = overlay;
            m_nCount++;
            setLastFocusedIndex(-1);
            populate();
        }
    }

}

OverlayItem 的陣列,我們必須要自己管理。我很簡單的只是用個固定陣列來放。然後再加個記數的變數。

createItem 以及 size 兩個函式是我們要實做的,就簡單的傳回陣列裡的物件跟數量就好了。要注意的是,這個 createItem 函式,並不真的需要去產生一個 Item,每次 Map View 需要更新 Overlay 時,都會呼叫這個函式。

在我們自己的 addOverlayItem 內,我們需要呼叫 populate 函式,通知 Map View 來更新這個 Overlay。並且,最好是在呼叫 populate 之前,呼叫 setLastFocusedIndex(-1),清掉之前的 Focus Item,否則咧,會因為找不到之前的 Focus Item 而讓 App Crash 的。( 我實際接到過這個問題的 Bug 回報,還好已經有神人找到問題解答了... )

接著,改 Main Activity,加入 PriLocOverlays 的變數,並且在 onCreate 中建立它。

Drawable drawable = this.getResources().getDrawable(R.drawable.pin_blue);
m_PrivateLocOverlay = new PriLocOverlays(drawable);
m_MapView.getOverlays().add(m_PrivateLocOverlay);

我們還需要有一個把目前位置建立成 OverlayItem 加入到 PrivateLocOverlay 裡的功能。就做在 Option Menu 裡吧...

在 Main Activity 加入函式。

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.menu_settings)
    {
        OverlayItem overlay = new OverlayItem(m_MyLocationOverlay.getMyLocation(), "Test Loc", "");
        m_PrivateLocOverlay.addOverlay(overlay);
        //requestAddCurrentLocation();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

於是乎,程式跑起來就像這樣。

device-2012-08-28-131318

 

呃...

本來想,就先寫到這樣吧,後來又覺得該寫下這個...

我們在 PriLocOverlays 類別裡,可以覆寫 onTap ( int index ) 這個函式。

當地圖上的標記被點選到的時候,onTap 函式會被呼叫。這時候,我們可以用 index 取出 OverlayItem,進行處理。

@Override
protected boolean onTap(int index)
{
    OverlayItem overlayItem = m_Overlays[index];
    Toast.makeText(m_Activity, overlayItem.getTitle(), Toast.LENGTH_LONG).show();
    // TODO Auto-generated method stub
    return super.onTap(index);
}

我沒多做什麼,就是用 Toast 顯示了一下。

結果就類似這樣。

image

2012年8月20日 星期一

第一支 Android App 上架,從無到有的全記錄 (3)

先再幫自己工商服務一下...

https://play.google.com/store/apps/details?id=com.lancelot.privatelocationdatabase

接著前兩篇的內容。

可以載入地圖之後,下一個要做的事,就是抓到我們的位置,還有,把地圖移到我們所在的位置上。

我們需要的是 LocationManager, LocationListener, 還有 MapController。

LocationManager 的作用是定位我們現在的位置,然後再將定位的結果傳喚給 LocationListener。不過呢,LocationListener 是一個抽象類別,需要我們自己去繼承實做,主要要實做 onLocationChanged 這個函式。

所以,我們先建立一個 GeoUpdateListener 類別。

public class GeoUpdateListener implements LocationListener {

    MapController m_MapController;
    GeoPoint m_geoCurrentLoc;
    MainActivity m_Activity;
   
    public GeoUpdateListener(MainActivity activity, MapController map_controller)
    {
        m_MapController = map_controller;
        m_Activity = activity;
    }
    public void onLocationChanged(Location location) {
        // TODO Auto-generated method stub
        int lat = (int) (location.getLatitude() * 1E6);
        int lng = (int) (location.getLongitude() * 1E6);
        m_geoCurrentLoc = new GeoPoint(lat, lng);
        m_MapController.animateTo(m_geoCurrentLoc);
    }

    public void onProviderDisabled(String provider) {
        // TODO Auto-generated method stub

    }

    public void onProviderEnabled(String provider) {
        // TODO Auto-generated method stub

    }

    public void onStatusChanged(String provider, int status, Bundle extras) {
        // TODO Auto-generated method stub

    }

}

onLocationChanged 的程式碼很簡單,就是把取得來的位置,經緯度轉換成 MapController 能用的整數格式,生成 GeoPoint 物件後,交給 MapController,MapController 就會把地圖移動到我們指定的位置上。

接著改 Main Activity。

加入幾個 Private 變數。

private MapController m_MapController;
private MapView m_MapView;
private LocationManager m_LocationManager;
private GeoUpdateListener m_GeoUpdateListener;

在 Main Activity 的 onCreate函式裡,加入 Location Manager, Geo Update Listener, 取得 Map View, Map Controller。

        m_MapView = (MapView)findViewById(R.id.mapview);
        m_MapView.setBuiltInZoomControls(true);
        //m_MapView.setSatellite(true);
       
        m_MapController = m_MapView.getController();
        m_MapController.setZoom(14); // Zoom 1 is world view
       
        m_GeoUpdateListener = new GeoUpdateListener(this, m_MapController);
       
        m_LocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        m_LocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000,
                0, m_GeoUpdateListener);
        // also need 3G GPS
        m_LocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000,
                0, m_GeoUpdateListener);

那,從 Code看。

Location Manager 要從 System Service 中取得。

位置的提供者,有兩種,從 GPS 來,以及從手機基地台定位而來。我們可以兩種都用。所以在 requestLocationUpdates 函式裡,會有兩種參數 GPS_PROVIDER & NETWORK_PROVIDER。

同時,我們也告訴Location Manager,位置更新的時候,請呼喚 GeoUpdateListener。

程式就改到這邊。

還有一個地方要更動一下。在 AndroidManifest.xml 裡,要加入兩個 Permission。

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

現在我們可以做測試了。

不過這次,我們想用模擬器來玩。

因為,這個 App 的 Build Target 是 Google API,所以,我們必須要建立一個 Google API 的 模擬器。

image

開啟模擬器之後,跑起 App。

image

但是這樣的東西好像沒有定位的功能呢,怎麼辦?

把 Eclipse IDE 上的 DDMS 打開,有個 Emulator Control 的分頁,可以找到 Location Control 的功能。

image

我們試試看,把 Longitude 改成 –122.08, Latitude 改成 37.42,

image

地圖就改到別的地方啦!!

2012年8月16日 星期四

第一支 Android App 上架,從無到有的全記錄 (2)

這樣說吧, App 裡頭要使用 Google Map 的資料,需要 Google 的授權。所以, Google 要求開發者要登記一個 Google Map API 的金鑰。App 還在開發中的時候,先使用 Debug 版的金鑰就可。至於發佈版的金鑰,後面再說吧。

取得金鑰前,先要簽署 Maps API。

Sign Up for the Android Maps API - Android Maps API - Google Code

image

打勾,填入 MD5 Fingerprint,然後按下 "Generate API Key"。

MD5 Fingerprint 哪裡來??

Android SDK 附有一個名稱為 "debug.keystore" 的檔案。

  • Windows Vista / Windows 7: C:\Users\<user>\.android\debug.keystore
  • Windows XP: C:\Documents and Settings\<user>\.android\debug.keystore

在這個檔案的目錄內,建立一個 bat 檔。內容寫一行

<jdk path>\bin\keytool -list -v -alias androiddebugkey -keystore debug.keystore -storepass android -keypass android >> debug_map_key.txt

<jdk path> 是 Java SDK 的安裝目錄。

執行 bat 檔以後,會將輸出內容寫到 debug_map_key.txt 檔案裡。檔案裡就可以看到這麼一段 :

別名名稱: androiddebugkey
建立日期: 2012/1/18
項目類型: PrivateKeyEntry
憑證鏈長度: 1
憑證 [1]:
擁有者: CN=Android Debug, O=Android, C=US
發出者: CN=Android Debug, O=Android, C=US
序號: 4f162768
有效期自: Wed Jan 18 09:59:04 CST 2012 到: Fri Jan 10 09:59:04 CST 2042
憑證指紋:
     MD5:  1F:04:CE:54:D2:57:0D:7C:11:1F:5D:A1:B2:D6:E8:93
     SHA1: 4E:D1:A5:3E:68:F2:05:C8:09:9C:29:9C:A9:38:29:4C:3C:3B:A8:1A
     SHA256: C9:DA:67:77:3F:4B:94:F5:AE:47:7F:FF:A6:14:9C:B5:96:10:00:7E:99:C8:45:40:A3:64:9F:83:BA:12:56:D1
     簽章演算法名稱: SHA1withRSA
     版本: 3

那一行 MD5 就是我們要的。

複製貼上到網頁上,按下 "Generate API Key"。

image

到此,我們就取得金鑰了。

回到 Eclipse IDE 的程式碼。

在 main activity 的 layout xml 檔案裡,把 android:apiKey 那一段,"your key" 改為我們取得的金鑰。

    <com.google.android.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:apiKey="0vn4fYcAmXsjC902Q8puaWZLabVkY0t5jqjRzSw"
    android:clickable="true" />

再來,因為我們需要透過 Internet 下載地圖資料,所以,要在 AndroidMenifest.xml 裡加入 Internet 的 Permission。

<uses-permission android:name="android.permission.INTERNET"/>

現在 Build 執行看看吧...

2012-08-16_10-25-01

看到地圖囉!!!

可是看不到街道、看不到自己的位置...

那是後續的工作...

2012年8月15日 星期三

第一支 Android App 上架,從無到有的全記錄 (1)

screen_1screen_2

這支 App 用到三個程式模組,Google 地圖、資料庫、還有廣告。

App 所要達到的功能,很簡單 :

當出遊或是閒晃時,走到某個值得記錄的地點,可以將它標記起來。未來再度行經附近的時候,地圖上就會標示出來。

花了一陣子的業餘時間,前幾天丟到架上了。( 歡迎下載 )

https://play.google.com/store/apps/details?id=com.lancelot.privatelocationdatabase

好,先從 Google Map 開始。

要用 Google Map 功能,先需要 Google Map 的 API。

Android 雖然跟 Google 關係匪淺,但是, Android SDK 本身是不含有 Google Map 相關功能的,所以,必須要透過 Android SDK Manager 工具,下載 Google APIs。這一大包裡頭,就有 Google Map 相關的類別。

image

接著,可以從 Eclipse IDE 建立 Android Application 專案。專案的 Build Target 要改用 Google APIs。

image

然後應該就可以生出個預設的 Hello World 程式了。

接著先在 AndroidManifest.xml 檔案中,加入 Use Library 到 Application裡。我們要使用的是 com.google.android.maps 這個 package。

<uses-library android:name="com.google.android.maps"/>

image

然後在主畫面的 layout xml 檔案裡,加入 Map View 元件。

    <com.google.android.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:apiKey="your_key"
    android:clickable="true" />

最後,把主要 Activity 改為從 MapActivity 繼承。

import com.google.android.maps.MapActivity;

import android.os.Bundle;
import android.view.Menu;

public class MainActivity extends MapActivity {

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

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}

@Override
protected boolean isRouteDisplayed() {
// TODO Auto-generated method stub
return false;
}


}

好了,趕快 Build 一下,看一下成果。

成果是,只看到方格線,還有一個 Google 的浮水印。其他什麼都沒有。

2012-08-15_19-07-01

因為我們還有一個很重要的 Map API Key 還沒取得。