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