Android2.1でGoogle Map API、メニュー画面、GPSでの位置の計測を使う方法が理解できたので作った。
GPSとって5秒おきにアップデートし、Google Mapに自分の位置を表示する。
移動に合わせて地図の中心点を動かし、移動のログを地図上に赤線で表示する。
GPS使いまくったら、電車で藤沢と日吉の間を往復する間使い続けただけでHTC Desireの電池が40%ぐらい減った。5秒間隔は狭すぎるか。
バイナリはAndroid2.1以上向け。右クリックで保存し.apkにリネームすればインストールできる
昨日テストした。日吉から藤沢方面に戻るところ。
緯度と経度のListをメモリ上に保存してあるので、線を引いて示す事ができる。
メモリ上に保存してあるだけなのでアプリを終了すると消える。でもAndroidアプリはそもそも終了しないので電源を切るまでは残る。そのうちSDカードに保存するようにしよう
 
Menuボタンを押すと
private static class MenuId{
    private static final int START_GPS = 1;
    private static final int LAST_LOCATION = 2;
    private static final int SET_ZOOM = 3;
    private static final int SATELLITE_TOGGLE = 4;
    private static final int LOG_TOGGLE = 5;
}
ボタンを追加して
@Override
public boolean onCreateOptionsMenu(Menu menu) {
      boolean supRetVal = super.onCreateOptionsMenu(menu);
      menu.add(0, MenuId.START_GPS, 0, "Start GPS");
      menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
      return supRetVal;
}
アイテムが選択された時のイベント内でどのボタンが押されたかを判定する。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case MenuId.START_GPS:
        // 何か処理
            break;
        case MenuId.LAST_LOCATION:
        // 何か処理
            break;
    }
}
イベント内でitem.setTitle()を使えばボタンの文字列を変更できる。
■Google Mapを表示する
mapの表示の前に準備がいる。
  - Eclipseで[Window]→[Android SDK and AVD Manager]→updateでGoogle APIをインストールする
- projectのPropertiesからbuild targetを変更。Android 2.1からGoogle APIのAndroid 2.1にする
- 最初に作ったActivityをcom.google.maps.MapActivityの継承に変更
- Google Maps API Keyを取得
Google Maps API Keyの取得に必要なMD5 fingerprintは、Macなら
keytool -list -keystore ~/.android/debug.keystore
して適当なパスワードを入れたら生成された。~/.androidディレクトリごと生成された。
AndroidManifest.xmlにandroid.permission.INTERNETとcom.google.android.mapsを追加する
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="org.shokai.gpstracker"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".GpsTracker"
                  android:label="@string/app_name"
                  android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
		<uses-library android:name="com.google.android.maps" />
    </application>
	<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    
</manifest>
res/layout/main.xmlにAPI key取得時にでてきたサンプルコードを貼り付けると、MapViewが表示される。
<com.google.android.maps.MapView
	android:id="@+id/mapview"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:enabled="true"
	android:clickable="true"
	android:apiKey="your-api-key"
	/>
clickableやenabledをtrueにした。地図を指でつまんで動かせるようになる。
main.xmlを書き換えたら、R.javaにいつのまにかMapViewのidが入っていた。
import com.google.android.maps.*;
MapView map = (MapView)findViewById(R.id.mapview);
という風にcontrollerから扱える。
これでとりあえず地図は表示できる。
■地図を操作する
onLocationChangedでLocationオブジェクトが取れるので、そこから緯度経度が取れる。
MapViewからMapControllerを取得して操作する。
double lat = location.getLatitude(); // 緯度
double lon = location.getLongitude(); // 軽度
MapController mc = map.getController();
GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
mc.setCenter(p);
mc.setZoom(18);
■Google Map上にOverlayを表示する
自分の位置を表示するだけなら、
地図/位置情報/GPSを使うAndroidアプリを作るには (1/3) – @ITのようにMyLocationOverlayを使うといい。
ただこの通りやるとGPSの取得間隔や精度を詳細に指定できるrequestLocationUpdateの機能とぶつかるのでそのままは使わず、onLocationChangedの中で位置を指定するようにした。
とりあえずMyLoactionOverlayを作ってmapに配置しておくが、表示はしない
MapView map = (MapView)findViewById(R.id.mapview);
MyLocationOverlay myOverlay = new MyLocationOverlay(getApplicationContext(), map);
myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
map.getOverlays().add(myOverlay);
自分アイコンを地図に表示
public void onLocationChanged(Location location) {
    myOverlay.getMyLocation();
}
ログの線の描画は、Overlayを継承したLogOverlayクラスを自分で作り、draw関数の中を実装してやると作れる。drawはどうやらMapViewが描画しなおす毎に実行されるっぽい。MapView.invalidate()を呼ぶとすぐ再描画できる。
Google MapにDrawableを配置する – Android Wiki*に東京と大阪の間を緯度経度で指定して線で結ぶ例があったので参考にした。
LogOverlay.java
package org.shokai.gpstracker;
import java.util.*;
import android.graphics.*;
import android.graphics.Paint.*;
import com.google.android.maps.*;
public class LogOverlay extends Overlay {
	private Paint linePaint;
	private List<GeoPoint> points;
	
	public LogOverlay() {
		this.points = new ArrayList<GeoPoint>();
		this.linePaint = new Paint();
		linePaint.setARGB(255, 255, 0, 0);
		linePaint.setStrokeWidth(2);
		linePaint.setDither(true);
		linePaint.setStyle(Style.FILL);
		linePaint.setAntiAlias(true);
		linePaint.setStrokeJoin(Paint.Join.ROUND);
		linePaint.setStrokeCap(Paint.Cap.ROUND);
	}
	
	public void add(GeoPoint p){
		points.add(p);
	}
	
	@Override
	public void draw(Canvas canvas, MapView view, boolean shadow){
		if(points.size() < 2) return;
		Point p_a = new Point();
		Point p_b = new Point();
		for(int i = 0; i < points.size()-1; i++){
			view.getProjection().toPixels(points.get(i), p_a);
			view.getProjection().toPixels(points.get(i+1), p_b);
			canvas.drawLine(p_a.x, p_a.y, p_b.x, p_b.y, linePaint);
		}
	}
	
	public int size(){
		return points.size();
	}
	
	public List<GeoPoint> getPoints(){
		return this.points;
	}
}
■画面を回転させない
Androidは画面を回転させると、onDestroyイベント→onCreateイベントの順にイベントが発生して画面がまるごと描画しなおされてしまう。
なので、画面を回転させるだけでTextViewは中身が消滅するし、Google MapにOverlayとして描画していた画像も消える。
値や描画はいいんだけど、requestLocationUpdateの取り消しができなくなって、アプリを強制終了するまでGPSマークが点きっぱなしになったりするのがまずい。
しょうがないので画面を回転させないようにした。
回転させない方法としては
八角研究所 : Android で再開する Java プログラミング(12) – Android ライフサイクルを考慮-手書きメモを作り込む
に書いてある様にAndroidManifest.xmlの manifest/application/activityのattributeとして
android:screenOrientation="portrait"
を書いておくと縦画面専用になり、回転しなくなる。
landscapeだと横になる。
理想的な方法としては、回転した後に値を復元するのがある。
画面回転時の挙動 – isherの日記が参考になる。
onSaveInstanceState と onRestoreInstanceStateを@Overrideで実装してBundleにputString/getStringして復元する。
    @Override
    protected void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);        
        bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
    }
    @Override
    protected void onRestoreInstanceState(Bundle bundle) {
        super.onRestoreInstanceState(bundle);
        this.textViewMessage.setText(bundle.getString("textViewMessage"));
    }
key-valueでbundleに保存して復元できる。
値の保存と復元ならこれでいいんだけど、GPSがどうしょうもない。センサーまわりのAPIの癖をつかまないと画面回転ができないアプリになっちゃって困る。
GpsTracker.java
package org.shokai.gpstracker;
import android.os.Bundle;
import com.google.android.maps.*;
import android.content.Context;
import android.location.*;
import android.view.*;
import android.widget.*;
import java.util.*;
public class GpsTracker extends MapActivity implements LocationListener{
	
	private MapView map;
	private TextView textViewMessage;
	private LocationManager lm;
	private MyLocationOverlay myOverlay;
	private final int zoom_default = 18;
	private boolean location_enalbed, log_enabled; // GPSとコンパスを動かしているかどうか、logを表示しているかどうか
	private LogOverlay logOverlay;
	
	private static class MenuId{
    	private static final int START_GPS = 1;
    	private static final int LAST_LOCATION = 2;
    	private static final int SET_ZOOM = 3;
    	private static final int SATELLITE_TOGGLE = 4;
    	private static final int LOG_TOGGLE = 5;
	}
	
	public GpsTracker(){
        this.location_enalbed = false;
	}
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.textViewMessage = (TextView)findViewById(R.id.textViewMessage);
        lm = (LocationManager)this.getSystemService(Context.LOCATION_SERVICE);
        map = (MapView)findViewById(R.id.mapview);
        myOverlay = new MyLocationOverlay(getApplicationContext(), map);
        myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
        map.getOverlays().add(myOverlay);
		logOverlay = new LogOverlay();
    }
	
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
      boolean supRetVal = super.onCreateOptionsMenu(menu);
      menu.add(0, MenuId.START_GPS, 0, "Start GPS");
      menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
      menu.add(0, MenuId.SET_ZOOM, 0, "Zoom");
      menu.add(0, MenuId.SATELLITE_TOGGLE, 0, "Satellite/Map");
      menu.add(0, MenuId.LOG_TOGGLE, 0, "Show Logs");
      return supRetVal;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    	switch (item.getItemId()) {
		case MenuId.START_GPS:
			if(!this.location_enalbed){
				lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, this); // 5(sec), 10(meter)
		        myOverlay.enableMyLocation();
		        myOverlay.enableCompass();
		        message("Start GPS");
		        this.location_enalbed = true;
		        item.setTitle("Stop GPS");
			}
			else{
				lm.removeUpdates(this);
				myOverlay.disableCompass();
				myOverlay.disableMyLocation();
				message("Stop GPS");
				this.location_enalbed = false;
				item.setTitle("Start GPS");
			}
			break;
		case MenuId.LAST_LOCATION:
			Location loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
			double lat = loc.getLatitude();
			double lon = loc.getLongitude();
			message("last lat:"+Double.toString(lat) + ", lon:" + Double.toString(lon));
			this.setPosition(lat, lon, this.zoom_default);
			break;
		case MenuId.SET_ZOOM:
			MapController mc = map.getController();
			mc.setZoom(this.zoom_default);
			map.getOverlays().add(logOverlay);
			break;
		case MenuId.SATELLITE_TOGGLE:
			if(map.isSatellite()){
				map.setSatellite(false);
			}
			else{
				map.setSatellite(true);
			}
			break;
		case MenuId.LOG_TOGGLE:
			if(this.log_enabled != true){
				map.getOverlays().add(logOverlay);
				item.setTitle("Hide Logs");
				message("logs: "+Integer.toString(logOverlay.size()));
				log_enabled = true;
			}
			else{
				map.getOverlays().remove(logOverlay);
				item.setTitle("Show Logs");
				log_enabled = false;
			}
			map.invalidate(); // すぐ再描画
			break;
    	}
    	return true;
    }
    
    public void setPosition(double lat, double lon, int zoom){
    	MapController mc = map.getController();
    	GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
    	logOverlay.add(p);
    	mc.setCenter(p);
    	mc.setZoom(zoom);
    	this.myOverlay.getMyLocation();
    }
	
    // zoomは変更せずに地図だけ動かす
    public void setPosition(double lat, double lon){
    	this.setPosition(lat, lon, map.getZoomLevel());
    }
    
	public void message(String mes){
		this.textViewMessage.setText(mes);
	}
	public void onLocationChanged(Location location) {
        double lat = location.getLatitude();
        double lon = location.getLongitude();
		message("lat:"+Double.toString(lat)+", lon:"+Double.toString(lon));
		this.setPosition(lat, lon);
	}
	public void onProviderDisabled(String provider) {
	}
	public void onProviderEnabled(String provider) {
	}
	public void onStatusChanged(String provider, int status, Bundle extras) {
	}
	@Override
	protected boolean isRouteDisplayed() {
		return false;
	}
	
    @Override
    protected void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);        
        bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
    }
    @Override
    protected void onRestoreInstanceState(Bundle bundle) {
        super.onRestoreInstanceState(bundle);
        this.textViewMessage.setText(bundle.getString("textViewMessage"));
    }
    
}
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:id="@+id/textViewMessage" android:text="@string/message"/>
        
<com.google.android.maps.MapView
	android:id="@+id/mapview"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:enabled="true"
	android:clickable="true"
	android:apiKey="0y0rb2n5OYga60X7toG-HzSYQh2ICnOT27xI4LA"
	/>
</LinearLayout>
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="org.shokai.gpstracker"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".GpsTracker"
                  android:label="@string/app_name"
                  android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
		<uses-library android:name="com.google.android.maps" />
    </application>
	<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    
</manifest>