Android 3.1のXOOMで90度ずれるという現象が発生したので,電子コンパスの方位角をSensor.TYPE_ORIENTATIONを使わずに求める方法を試しました

TYPE_ORIENTATIONを使わずに方位角を取得

地図ロイドですが,A500で電子コンパスが90度などずれるという話がありました. それで手元のXOOM (Android 3.1)で確認したところ,確かに90度ずれていました.

一体何事か!ヽ(`Д´)ノ と思って調べてみたところ,結局

  • 方位角を求めるためのSensor.TYPE_ORIENTATIONで受け取ったeventのvalues[0]が,90度ずれている

という現象が発生していたのが原因でした. AndroidでGoogleMapをヘディングアップ表示に概要を書いていますが,TYPE_ORIENTATIONのセンサーで方位を求める実装をしていたのです.

それにしてもOSのバージョンなのか機種依存なのかわかりませんが,今までは発生していなかった不具合です. 少なくとも,HT-03A(1.6),Xperia arc(2.3.3)では発生しません.

とは言っても仕方がないので対処法を考えるのですが,

  • 問題が発生する端末の場合だけ90度ずらす

という方法は,どの端末で問題が発生するのか特定できそうにないので没とし, あれこれ試したところ,Sensor.TYPE_ORIENTATIONを使わない方法だと正しい角度が取れました.

確かに,android.hardware.SensorEventの JavaDoc によると Sensor.TYPE_ORIENTATION はレガシーである,

Note: This sensor type exists for legacy reasons,
please use getRotationMatrix() in conjunction with
remapCoordinateSystem() and getOrientation() to compute these values instead.
と書かれており,今となってはあまり使わない方が良いのかも知れません.

それでこのSensor.TYPE_ORIENTATIONを使わない方法ですが,ネットを検索したところ, Sensor.TYPE_ACCELEROMETERとSensor.TYPE_MAGNETIC_FIELDを組み合わせて取得できるようです.

今回は,この方法で行った実装について説明します.

※なお以下のコードはそのままでは動作しません.説明したい内容のキモに絞った最小限の記述ですので, 実際に動くアプリにするには加筆する必要がありますのでご注意ください.

1.TYPE_ORIENTATIONを使わない方式で方位角を求める

TYPE_ACCELEROMETERとTYPE_MAGNETIC_FIELDを使う方法です. 上記のネットで検索した方法の,ほとんどそのままです.

メンバー変数

SensorEventListener mSensorEventListener;
float[] accelerometerValues = new float[3];
float[] geomagneticMatrix = new float[3];
boolean sensorReady;

センサー起動部

List<Sensor> slm = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(mSensorEventListener,
	slm.get(0), SensorManager.SENSOR_DELAY_NORMAL);

List<Sensor> sla = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(mSensorEventListener,
	sla.get(0), SensorManager.SENSOR_DELAY_NORMAL);

イベントリスナー定義部

mSensorEventListener = new SensorEventListener() {
	public void onSensorChanged(SensorEvent event) {

		switch (event.sensor.getType()) {
		case Sensor.TYPE_ACCELEROMETER:
			accelerometerValues = event.values.clone();
			break;
		case Sensor.TYPE_MAGNETIC_FIELD:
			geomagneticMatrix = event.values.clone();
			sensorReady = true;
			break;
		}

		if (geomagneticMatrix != null && accelerometerValues != null && sensorReady) {
			sensorReady = false;

			float[] R = new float[16];
			float[] I = new float[16];

			SensorManager.getRotationMatrix(R, I,
				accelerometerValues, geomagneticMatrix);

			float[] actual_orientation = new float[3];

			calcActualOrientation(R, actual_orientation);

			// 求まった方位角.ラジアンなので度に変換する
			float orientDegree = (float)Math.toDegrees(actual_orientation[0]);
			Log.d("**Test", "orientDegree=" + orientDegree);
		}
	}
	public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

子メソッド

private void calcActualOrientation(float[] R, float[] out) {
	SensorManager.getOrientation(R, out);
}
これで,TYPE_ORIENTATIONを使わない方法で無事に方位角が求まりました.

2.画面の回転を考慮する

しかしこれだと,端末を通常の置き方で使うときは正しい方位になるのですが, 端末を縦置きから横置きにしたりして画面表示の向きを切り替えると,90度とか角度が狂ってしまいます.

そこで,画面の回転への対応を入れます. 先ほどの一番下のcalcActualOrientation()メソッドを,以下のように書き換えてみます.

calcActualOrientationメソッドの改訂

private void calcActualOrientation(float[] R, float[] out) {
	Activity act = this;

	// 画面の回転状態を取得する
	int dr = getDispRotation(act);

	if (dr == Surface.ROTATION_0) {
		// 回転無し
		SensorManager.getOrientation(R, out);

	} else {
		// 回転あり
		float[] outR = new float[16];

		if (dr == Surface.ROTATION_90) {
			SensorManager.remapCoordinateSystem(R,
				SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, outR);

		} else if (dr == Surface.ROTATION_180) {
			float[] outR2 = new float[16];
			SensorManager.remapCoordinateSystem(R,
				SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, outR2);
			SensorManager.remapCoordinateSystem(outR2,
				SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, outR);

		} else if (dr == Surface.ROTATION_270) {
			SensorManager.remapCoordinateSystem(R,
				SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_MINUS_X, outR);

		}
		SensorManager.getOrientation(outR, out);
	}
}

private static int getDispRotation(Activity act) {
	Display d = act.getWindowManager().getDefaultDisplay();
	return d.getRotation();
}
getDispRotation()という新メソッドで画面の回転状態を取得し,その結果を使って SensorManager.remapCoordinateSystem で座標系変換をかけています.

getDispRotation()では最終的に, Display#getRotation というメソッドを使っています.

なお座標系変換といってもここで必要なのは,90度,180度,270度ずらすだけですので, remapCoordinateSystemを使わずに単純な引き算で実装しても良いと思います. (度に変換する前なら,もちろん度ではなくラジアン単位で引く必要があります)

3.Android 2.1以下への対応

先ほどは触れなかったのですが,Display#getRotation()というメソッドはAPI Level 8 (2.2.x)です. このようにLevel 8のメソッドをそのまま含んでいるプログラムをAndroid 2.1以下の端末で動かそうとすると, エラーになってしまいます.

そこで2.1以下の端末では,Display#getRotation()ではなく,Level 7以下でも使える Display#getOrientation を使うように対応します.(「deprecated」ですが,ここでは見なかったことにします (´∇`)

最後のgetDispRotation()メソッドを,以下のように書き換えてみます.

getDispRotationメソッドの改訂

private static int getDispRotation(Activity act) {
	Display d = act.getWindowManager().getDefaultDisplay();
	return DispRotateGetter.getInstance().getRotate(d);
}

private static class DispRotateGetter {
	private static IDispRotateGetter getInstance() {
		if (Integer.parseInt(Build.VERSION.SDK) >= 8) {
			// for 2.2 or higher
			return new DispRotateGetterV8();
		} else {
			// for 2.1 or lower
			return new DispRotateGetterV1();
		}
	}
	private interface IDispRotateGetter {
		public int getRotate(Display d);
	}
	private static class DispRotateGetterV8 implements IDispRotateGetter {
		public int getRotate(Display d) {
			return d.getRotation();
		}
	}
	private static class DispRotateGetterV1 implements IDispRotateGetter {
		public int getRotate(Display d) {
			int r = d.getOrientation();
			return (r == 0? Surface.ROTATION_0: Surface.ROTATION_90);
		}
	}
}
何やら複雑ですが... 背景としては,その端末のLevelを超えたメソッドを呼び出しているクラスはロード時にエラーが出るため, interfaceを挟むことでそのようなクラスをロードさせないようにしている,という事情です.

2.1以下の端末ではLevel8を含むDispRotateGetterV8クラスをロードさせないことで, レベルオーバーのエラーを回避しています.

...ここまでやって,無事に手元の3台

  • HT-03A (Android 1.6)
  • Xperia arc (2.3.3)
  • XOOM (3.1)

で画面の回転を考慮に入れた方位角を取れるようになりました (´∀`)

補足

マニフェストでのバージョン定義
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="8" />
targetSdkVersionを上げないとLevel 8のメソッドは記述できないのですが,Level 3の端末でも実行するという意図です
kamolandをフォローしましょう


© 2017 KMIソフトウェア