Android2.1でGoogle Map API、メニュー画面、GPSでの位置の計測を使う方法が理解できたので作った。
GPSとって5秒おきにアップデートし、Google Mapに自分の位置を表示する。
移動に合わせて地図の中心点を動かし、移動のログを地図上に赤線で表示する。
GPS使いまくったら、電車で藤沢と日吉の間を往復する間使い続けただけでHTC Desireの電池が40%ぐらい減った。5秒間隔は狭すぎるか。
- ソースコード
- 最新版バイナリ(updateされる)
- この記事のバージョンのバイナリ
昨日テストした。日吉から藤沢方面に戻るところ。
緯度と経度のListをメモリ上に保存してあるので、線を引いて示す事ができる。
メモリ上に保存してあるだけなのでアプリを終了すると消える。でもAndroidアプリはそもそも終了しないので電源を切るまでは残る。そのうちSDカードに保存するようにしよう
Menuボタンを押すと
- GPSのon/off、最後に計測した場所に戻る
- ズームをレベル18にする(18ぐらいが人間が徒歩で使う地図として調度良い)
- 衛星写真と地図の切り替え
- ログ線の表示on/off ができる。
電車で日吉駅に入る所
電車で二俣川あたりを通り過ぎる
■GPSで位置情報を取る
橋本商会 適当なLocationProviderから位置情報を取るに書いたうち、requestLocationUpdatesを使った。
GPSまわりは、Android2.1と1.6以前でAPIがかなり違う。
ネットで検索して2009年春以前の記事を見てやってたら痛い目を見た。
出たばかりのこの本は全体的に薄く広くカバーしていて、とっかかりとして良い。ホームスクリーンウィジェットやbluetoothまで一応載っている。
レイアウトにXMLを使わず全てJavaで書いているけど。
■メニューボタン
Activityを継承しているクラスならそのまま@OverrideでonCreateOptionsMenuとonInputItemSelectedを実装すれば使える。
こんな風にクラス内クラスを作っておいて
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イベント内でitem.setTitle()を使えばボタンの文字列を変更できる。
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MenuId.START_GPS:
// 何か処理
break;
case MenuId.LAST_LOCATION:
// 何か処理
break;
}
}
■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.MapViewclickableやenabledをtrueにした。地図を指でつまんで動かせるようになる。
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"
/>
main.xmlを書き換えたら、R.javaにいつのまにかMapViewのidが入っていた。
import com.google.android.maps.*;という風にcontrollerから扱える。
MapView map = (MapView)findViewById(R.id.mapview);
これでとりあえず地図は表示できる。
■地図を操作する
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して復元する。
@Overridekey-valueでbundleに保存して復元できる。
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"));
}
値の保存と復元ならこれでいいんだけど、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>