android sdk 1.5r3 で方位によって回転するMapを作る

HT-03Aを購入して、4週間ぐらいになりますが、楽しいですね。これ。
この間旅行に行ったとき、GoogleMapを使って、すごく便利だったので、地図関係のアプリを作ろうといろいろやっています。

そういうわけで電子コンパスの情報にあわせて地図の向きが変わる簡単なアプリを作りました。
iPhoneを持っている人に見せてもらった、こういうのです。
http://www.apple.com/jp/iphone/iphone-3gs/maps-compass.html

マーケットでは、AndNavというのがすでにあるのですが、自分で作りたかったので、どうやったら作れるのかGoogle先生でいろいろ調べてみてもなかなかいい情報が無くて、結構迷いました。

http://www.anddev.org/viewtopic.php?p=15173
で、AndNavの作者さんが、MapView#onDraw() をOverrideすれば良いと書いていましたが、1.5r3ではfinalで宣言されているので上書きできない状態です。以前はそれでよかったようですが、SDKのバージョンがあがってできなくなっているようですね。
ほかには、ViewGroup#onDispatchDrawをOverrideするという情報もありましたが、これは自分は動作させられなかったです。

試行錯誤の結果、MapView#draw() をOverrideすることで、実現することができたので、書いてみようと思います。

仕組みの概要

  1. MapViewのサブクラスを定義して、drawメソッドをオーバーライドする。
  2. オーバーライドしたdrawメソッド内で、canvas#rotate()する。rotateする量は、電子コンパスの値を元にする
  3. 電子コンパスの値を取得できるようにし、値が通知された際に、MapView#invalidate()する
  4. invalidateによって、再描画(?)が発生し、drawメソッドが呼ばれ、電子コンパスの値の逆方向に地図を回転させる
    • Logを仕掛けてみると、invalidateせずともdrawは頻繁に呼ばれます。ただ、スムーズに再描画させるなために強制的にinvalidateしています

地図の回転とは別に、GPSの位置情報を検出して、マップを移動する処理も入れてあります。

実際に端末上で動作させると次のような動きになります。


adakodaさんの Android Screen Monitor を使わせていただきました。 http://www.adakoda.com/adakoda/android/asm/

コード

2009/08/13 追記

216行目(コードの一番下のほうの行)でCanvas#scale()の第3、第4引数を本来とは逆に指定していました。このため、地図の回転軸がずれて見える結果になっていました。この点を修正しています。

  • 誤り
canvas.scale(scaleFactor, scaleFactor, centerH, centerW);
  • 正しい
canvas.scale(scaleFactor, scaleFactor, centerW, centerH);
package tomo.snowbug;

import android.content.Context;
import android.graphics.Canvas;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;

public class CompasMapActivity extends MapActivity implements LocationListener,
		SensorEventListener {

	private String TAG;// LOG用のタグ名

	private MapController mapc;

	private MapView mapv;

	private SensorManager sensorMgr;

	// TYPE_ORIENTATION の sensor
	private Sensor sensor_orientation;

	// TYPE_ORIENTATION の sensorが検知した値
	private float[] orientationValues;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		prepareSensors();
		registerSensors();

		TAG = getResources().getString(R.string.app_name);
		// res/values/strings.xmlに登録されているapp_nameを参照している

		setContentView(R.layout.main);

		// 現在位置の変化を検知する。
		LocationManager l = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		l.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);

		mapv = new RotatableMapView(this, getString(R.string.map_key));

		mapc = mapv.getController();
		mapc.setZoom(15);
		mapv.setClickable(true);
		mapc.setCenter(new GeoPoint(35577737, 139736009));
		mapv.setBuiltInZoomControls(true);
		mapv.setReticleDrawMode(MapView.ReticleDrawMode.DRAW_RETICLE_OVER);

		setContentView(mapv);
	}

	/**
	 * GPSでの測位が変化した際に呼ばれる
	 */
	@Override
	public void onLocationChanged(Location location) {
		GeoPoint gp = new GeoPoint((int) (location.getLatitude() * 1E6),
				(int) (location.getLongitude() * 1E6));
		mapc.animateTo(gp);
	}

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

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

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

	@Override
	public boolean onCreateOptionsMenu(Menu m) {
		m.add(0, 1, 1, "home");
		m.add(0, 1, 2, "finish");
		return true;
	}

	@Override
	public boolean onPrepareOptionsMenu(Menu m) {
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem mItem) {

		if ("home".equals(mItem.getTitle())) {
			mapc.animateTo(new GeoPoint(35577737, 139736009));
		}
		if ("finish".equals(mItem.getTitle())) {
			finishActivity();
		}
		return true;
	}

	@Override
	public boolean onSearchRequested() {
		super.onSearchRequested();
		startSearch(null, false, null, true);
		return true;
	}

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

	@Override
	protected void onResume() {
		super.onResume();
		registerSensors();
	}

	@Override
	protected void onPause() {
		super.onPause();
		sensorMgr.unregisterListener(this);
	}

	@Override
	protected void onStop() {
		// TODO Auto-generated method stub
		super.onStop();
		finishActivity();
	}

	/**
	 * 加速度センサー変化時に呼ばれる
	 */
	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
		// TODO Auto-generated method stub
	}

	/**
	 * 傾きセンサーの変化時に呼ばれる
	 */
	@Override
	public void onSensorChanged(SensorEvent event) {
		orientationValues = event.values;
		Log.d(TAG, "mValues[" + orientationValues[0] + ", "
				+ orientationValues[1] + ", " + orientationValues[2]);
		mapv.invalidate(); // 再描画のため(MapViewのdrawが呼ばれる)
	}

	// ------------------------------------------

	private void registerSensors() {
		sensorMgr.registerListener(this, sensor_orientation,
				SensorManager.SENSOR_DELAY_UI);
		// DELAYをUIにしないと頻繁に書き換えすぎるようです
	}

	private void prepareSensors() {
		sensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
		sensor_orientation = sensorMgr.getSensorList(Sensor.TYPE_ORIENTATION)
				.get(0);
	}

	private void finishActivity() {
		sensorMgr.unregisterListener(this);
		this.finish();
	}

	/**
	 * 回転可能なMapView
	 */
	private class RotatableMapView extends MapView {

		public RotatableMapView(Context context, String apiKey) {
			super(context, apiKey);
		}

		@Override
		public void draw(Canvas canvas) {

			if (orientationValues == null) {
				super.draw(canvas);
				return;
			}
			Log.d(TAG, "draw!!!!! ");
			int h = getHeight();
			int w = getWidth();
			float centerH = h / 2;
			float centerW = w / 2;

			// あらかじめMapView.invalidate()されていることが前提
			// 再描画なので、方角とは逆にCanvasを回転させれば良い
			canvas.rotate(-orientationValues[0], centerW, centerH);

			// Canvasの大きさを拡大することで、Canvasを回転した際に隅に現れる、地図が描画されない真っ黒な領域を見せないようにしている
			final float scaleFactor = (float) (Math.sqrt(h * h + w * w) / Math
					.max(w, h));
			canvas.scale(scaleFactor, scaleFactor, centerW, centerH);
			super.draw(canvas);
		}
	}
}

課題

  1. 滑らかに地図を回転させる(ローパスフィルターのような)
  2. 地図の移動が正しくできるようにする
  3. 中心にアイコンを設置して、そのアイコンが必ず端末の上方向を向くようにする
  4. 端末が向いている方向を文字で表示する。