Learn how to make a Compass application for Android
Smartphones have a lot of sensors letting to developers to take profit in their applications. Make a Compass application for Android is a great way to understand how they work on Android OS. To make that Compass application, we’re going to use mainly accelerometer sensor. Location service will be also used to fix compass data and also to display GPS location with latitude and longitude.
The following video shows you how to create that Compass application on Android steps by steps :
To display Compass, we have created a custom view named CompassView :
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class CompassView extends View {
private static final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int width = 0;
private int height = 0;
private Matrix matrix; // to manage rotation of the compass view
private Bitmap bitmap;
private float bearing; // rotation angle to North
public CompassView(Context context) {
super(context);
initialize();
}
public CompassView(Context context, AttributeSet attr) {
super(context, attr);
initialize();
}
private void initialize() {
matrix = new Matrix();
// create bitmap for compass icon
bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.compass_icon);
}
public void setBearing(float b) {
bearing = b;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
if (bitmapWidth > canvasWidth || bitmapHeight > canvasHeight) {
// resize bitmap to fit in canvas
bitmap = Bitmap.createScaledBitmap(bitmap,
(int) (bitmapWidth * 0.85), (int) (bitmapHeight * 0.85), true);
}
// center
int bitmapX = bitmap.getWidth() / 2;
int bitmapY = bitmap.getHeight() / 2;
int parentX = width / 2;
int parentY = height / 2;
int centerX = parentX - bitmapX;
int centerY = parentY - bitmapY;
// calculate rotation angle
int rotation = (int) (360 - bearing);
// reset matrix
matrix.reset();
matrix.setRotate(rotation, bitmapX, bitmapY);
// center bitmap on canvas
matrix.postTranslate(centerX, centerY);
// draw bitmap
canvas.drawBitmap(bitmap, matrix, paint);
}
}
As you can see, we play with a compass bitmap and use rotation to display north direction.
Compass view is used in the main activity layout in which we display also GPS location :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.ssaurel.mycompass.MainActivity" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp" >
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="@dimen/dirSize" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/latitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="30dp"
android:textSize="@dimen/coordSize" />
<TextView
android:id="@+id/longitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="30dp"
android:textSize="@dimen/coordSize" />
</LinearLayout>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.ssaurel.mycompass.CompassView
android:id="@+id/compass"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true" />
</RelativeLayout>
</LinearLayout>
Last part but may be biggest part is the use of this component in the main activity where we use sensors data and location service :
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import android.app.Activity;
import android.content.Context;
import android.hardware.GeomagneticField;
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.view.Menu;
import android.view.MenuItem;
import android.view.WindowManager;
import android.widget.TextView;
public class MainActivity extends Activity implements SensorEventListener,
LocationListener {
public static final String NA = "N/A";
public static final String FIXED = "FIXED";
// location min time
private static final int LOCATION_MIN_TIME = 30 * 1000;
// location min distance
private static final int LOCATION_MIN_DISTANCE = 10;
// Gravity for accelerometer data
private float[] gravity = new float[3];
// magnetic data
private float[] geomagnetic = new float[3];
// Rotation data
private float[] rotation = new float[9];
// orientation (azimuth, pitch, roll)
private float[] orientation = new float[3];
// smoothed values
private float[] smoothed = new float[3];
// sensor manager
private SensorManager sensorManager;
// sensor gravity
private Sensor sensorGravity;
private Sensor sensorMagnetic;
private LocationManager locationManager;
private Location currentLocation;
private GeomagneticField geomagneticField;
private double bearing = 0;
private TextView textDirection, textLat, textLong;
private CompassView compassView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textLat = (TextView) findViewById(R.id.latitude);
textLong = (TextView) findViewById(R.id.longitude);
textDirection = (TextView) findViewById(R.id.text);
compassView = (CompassView) findViewById(R.id.compass);
// keep screen light on (wake lock light)
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onStart() {
super.onStart();
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
sensorGravity = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorMagnetic = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
// listen to these sensors
sensorManager.registerListener(this, sensorGravity,
SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(this, sensorMagnetic,
SensorManager.SENSOR_DELAY_NORMAL);
// I forgot to get location manager from system service ... Ooops :D
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
// request location data
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
LOCATION_MIN_TIME, LOCATION_MIN_DISTANCE, this);
// get last known position
Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (gpsLocation != null) {
currentLocation = gpsLocation;
} else {
// try with network provider
Location networkLocation = locationManager
.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (networkLocation != null) {
currentLocation = networkLocation;
} else {
// Fix a position
currentLocation = new Location(FIXED);
currentLocation.setAltitude(1);
currentLocation.setLatitude(43.296482);
currentLocation.setLongitude(5.36978);
}
// set current location
onLocationChanged(currentLocation);
}
}
@Override
protected void onStop() {
super.onStop();
// remove listeners
sensorManager.unregisterListener(this, sensorGravity);
sensorManager.unregisterListener(this, sensorMagnetic);
locationManager.removeUpdates(this);
}
@Override
public void onLocationChanged(Location location) {
currentLocation = location;
// used to update location info on screen
updateLocation(location);
geomagneticField = new GeomagneticField(
(float) currentLocation.getLatitude(),
(float) currentLocation.getLongitude(),
(float) currentLocation.getAltitude(),
System.currentTimeMillis());
}
private void updateLocation(Location location) {
if (FIXED.equals(location.getProvider())) {
textLat.setText(NA);
textLong.setText(NA);
}
// better => make this creation outside method
DecimalFormatSymbols dfs = new DecimalFormatSymbols();
dfs.setDecimalSeparator('.');
NumberFormat formatter = new DecimalFormat("#0.00", dfs);
textLat.setText("Lat : " + formatter.format(location.getLatitude()));
textLong.setText("Long : " + formatter.format(location.getLongitude()));
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onSensorChanged(SensorEvent event) {
boolean accelOrMagnetic = false;
// get accelerometer data
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// we need to use a low pass filter to make data smoothed
smoothed = LowPassFilter.filter(event.values, gravity);
gravity[0] = smoothed[0];
gravity[1] = smoothed[1];
gravity[2] = smoothed[2];
accelOrMagnetic = true;
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
smoothed = LowPassFilter.filter(event.values, geomagnetic);
geomagnetic[0] = smoothed[0];
geomagnetic[1] = smoothed[1];
geomagnetic[2] = smoothed[2];
accelOrMagnetic = true;
}
// get rotation matrix to get gravity and magnetic data
SensorManager.getRotationMatrix(rotation, null, gravity, geomagnetic);
// get bearing to target
SensorManager.getOrientation(rotation, orientation);
// east degrees of true North
bearing = orientation[0];
// convert from radians to degrees
bearing = Math.toDegrees(bearing);
// fix difference between true North and magnetical North
if (geomagneticField != null) {
bearing += geomagneticField.getDeclination();
}
// bearing must be in 0-360
if (bearing < 0) {
bearing += 360;
}
// update compass view
compassView.setBearing((float) bearing);
if (accelOrMagnetic) {
compassView.postInvalidate();
}
updateTextDirection(bearing); // display text direction on screen
}
private void updateTextDirection(double bearing) {
int range = (int) (bearing / (360f / 16f));
String dirTxt = "";
if (range == 15 || range == 0)
dirTxt = "N";
if (range == 1 || range == 2)
dirTxt = "NE";
if (range == 3 || range == 4)
dirTxt = "E";
if (range == 5 || range == 6)
dirTxt = "SE";
if (range == 7 || range == 8)
dirTxt = "S";
if (range == 9 || range == 10)
dirTxt = "SW";
if (range == 11 || range == 12)
dirTxt = "W";
if (range == 13 || range == 14)
dirTxt = "NW";
textDirection.setText("" + ((int) bearing) + ((char) 176) + " "
+ dirTxt); // char 176 ) = degrees ...
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
if (sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD
&& accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
// manage fact that compass data are unreliable ...
// toast ? display on screen ?
}
}
}
You can find a demo of this application here on the Google Play Store : https://play.google.com/store/apps/details?id=com.ssaurel.tinycompass
5 Comments Already
Leave a Reply
You must be logged in to post a comment.



The conversion from magnetic “bearing” to true should subtract declination, not add. In California, for example, the variation is positive 13 or so. So when my compass says 13 degrees, my true heading is 360, or 0 (depending on whether you’re a pilot or a Google API programmer). Also, technically, I think you mean “heading”. Bearing means the value from an object. Heading is the value to the object. Lastly, while you’re bothering to check if the heading is less than zero, should probably check that it’s greater than 360 and if so subtract 360. Correct me if I’m wrong. But thanks for the code. Seems great otherwise.
Thanks for the details.
Hi Saul
Thanks..very useful example.. ..what low pass filter code do you use?
AndyH