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)では発生しません.
とは言っても仕方がないので対処法を考えるのですが,
という方法は,どの端末で問題が発生するのか特定できそうにないので没とし,
あれこれ試したところ,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の端末でも実行するという意図です