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