Ever wondered how a compass works? I never gave it a lot of thought until last week when my son and I were hiking in a national park, and I couldn’t get a GPS signal.
When I finally got back to civilization, I started combing the Android forums for sample digital compass code and found that most of the examples freely available used the now deprecated Sensor.TYPE_ORIENTATION. A little more digging and I discovered that Sensor.TYPE_ORIENTATION was actually never a real hardware sensor but a software service that composited the values of the accelerometer and the magnetic field sensor.
So pulling from multiple online sources including a Wikipedia article that compared radians to degrees, I managed to create a simple magnetic compass. If you are interested in turning your Android device into a compass, you can follow along with the tutorial below, or download and import the project directly into Eclipse.
1. Create a new Android project in Eclipse. Target SDK 14 (ICS) or better.
2. In your AndroidManifest.xml file, request permissions for fine and coarse locations. In this instance I am also locking the orientation to portrait mode.
AndroidManifest.xml
3. You’re going to need a pointer for your compass. I chose the image below and stuck in the /res/drawable-xhdpi folder.
4. The modified activity_main.xml file in the /res/layout folder is simply an image view centered in a relative layout.
activity_main.xml
5. We can code up our compass in the /src/MainActivity.java file. I’m not going to attempt to explain the math in the on sensor changed event because it is beyond the scope of this article and because my trigonometry is rusty and just getting it to work made my head hurt. I have, however, compared my compass with Google Maps, and it seems to consistently point north, which I consider a win. One important thing to keep in mind for the sake of preserving the device’s battery is the registering and unregistering of the sensors in the on resume and on pause, respectively.
MainActivity.java
package com.authorwjf.whichwayisup;
import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
public class MainActivity extends Activity implements SensorEventListener {
private ImageView mPointer;
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mMagnetometer;
private float[] mLastAccelerometer = new float[3];
private float[] mLastMagnetometer = new float[3];
private boolean mLastAccelerometerSet = false;
private boolean mLastMagnetometerSet = false;
private float[] mR = new float[9];
private float[] mOrientation = new float[3];
private float mCurrentDegree = 0f;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mPointer = (ImageView) findViewById(R.id.pointer);
}
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
}
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this, mAccelerometer);
mSensorManager.unregisterListener(this, mMagnetometer);
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor == mAccelerometer) {
System.arraycopy(event.values, 0, mLastAccelerometer, 0, event.values.length);
mLastAccelerometerSet = true;
} else if (event.sensor == mMagnetometer) {
System.arraycopy(event.values, 0, mLastMagnetometer, 0, event.values.length);
mLastMagnetometerSet = true;
}
if (mLastAccelerometerSet && mLastMagnetometerSet) {
SensorManager.getRotationMatrix(mR, null, mLastAccelerometer, mLastMagnetometer);
SensorManager.getOrientation(mR, mOrientation);
float azimuthInRadians = mOrientation[0];
float azimuthInDegress = (float)(Math.toDegrees(azimuthInRadians)+360)%360;
RotateAnimation ra = new RotateAnimation(
mCurrentDegree,
-azimuthInDegress,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF,
0.5f);
ra.setDuration(250);
ra.setFillAfter(true);
mPointer.startAnimation(ra);
mCurrentDegree = -azimuthInDegress;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO Auto-generated method stub
}
}
This app isn’t all that impressive on the emulator, so if you’re interested in seeing it in action go ahead and load it to an actual Android device.
Notes about accuracy
- The compass points to magnetic north, not true north. To determine the latter, you’d need to pull in your location via GPS or Wi-Fi and then calculate the offset.
- Because we are using the magnetic field sensor there is quite a bit of noise that I am not attempting to dampen.
- You’ll find this simple example works best on a level surface, preferably outdoors.
If any developers out there are savvy when it comes to trig and graphing, I’d love to read your comments on how to improve accuracy.