In this post we’ll see how-to create a Compass App. In this tutorial we’ll use the version 2.2.2 of Android Studio. At the end of the post there is a video that shows all the operations.

Let’s setup our project. First we open Android Studio. We select “Start a new Android Studio Project” if we are in the home screen or we go on the menu File->New and we select “New project” if the program is already running. In the first box related to the creation of our project we choose the name  “Compass” in the field Application name and we click on “Next”:

In the second box we can choose which is the minimum android version that our App is able to support. We leave all as it is and we click on “Next”:

In the third box we select “Empty Activity” and we click on “Next”:

In the last box we change the name of the Activity with “Compass” and we press on “Finish”:

Now we have created our project. Before starting coding click here to download the image for our compass. The files that we have to modify are the following:

AndroidManifest.xml is the file that contains all the main characteristics of all the Activities of the App. 
SplashScreen.java is the Java code associated with the Compass Activity.
activity_compass.xml is the xml file that defines the layout of the Compass Activity.

First of all we modify the orientation of the App. We open the file AndroidManifest.xml (that is in the folder app/manifest/) and we add the following line between “.Compass”  and the character “>”:

<activityandroid:name=".Compass" android:screenOrientation="portrait">

After this operation the content of the file AndroidManifest.xml will be as it follows:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.compass" >
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name=".Compass"
            android:screenOrientation="portrait">
        <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Now we are going to modify the layout of the Compass Activity.We select the Text mode (bottom tab) of the file activity_compass.xml (that is in the folder app/res/layout/)  and we modify the properties of the TextView that we’ll use to show the Azimuth of our Compass:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:textAlignment="center"
    android:textSize="40sp"
    android:textColor="#000"
    android:textStyle="bold"
    android:text="0°"
    android:id="@+id/txt_azimuth"/>

Before adding our compass (that you have downloaded before starting coding) we have to copy it inside our project. We go into the Download folder of our browser and we copy the Compass image in the folder  app/src/main/res/drawable (that is inside our project folder). Now we can include it inside our project. Under the TextView we add an ImgView with the following properties:

<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:src="@drawable/compass"
    android:layout_below="@+id/txt_azimuth"
    android:id="@+id/img_compass"/>

After this operation the content of the file activity_compass.xml will be as it follows:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_compass"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.compass.Compass">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:textAlignment="center"
        android:textSize="40sp"
        android:textColor="#000"
        android:textStyle="bold"
        android:text="0°"
        android:id="@+id/txt_azimuth"/>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/compass"
        android:layout_below="@+id/txt_azimuth"
        android:id="@+id/img_compass"/>
</RelativeLayout>

If all your code is correct, switching to the Design mode (bottom tab) this would be your result:

Now we have to specify the behavior of the Compass Activity. We open the file Compass.java (that is in the folder app/java/com.example.compass/) and we modify the following contents:

First of all we add next to AppCompatActivity the line implements SensorEventListener, we push the Alt  and Return buttons, we select Implements Methods and, in the box that appears, we click on Ok. At this point we’ll have this code:

public class Compass extends AppCompatActivity implements SensorEventListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_compass);
    }

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {

    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {

    }
}

 As you can see we have three main methods:

onCreate is the one that is executing at the App startup.
onSensorChanged is the one executed when a sensor change its state. 
onAccuracyChanged is one executed when the sensor change accuracy but we don’t use it.

Before start implementing the methods we have to declare the variables that we’ll use all over our code. We have to write them before the line @Override of the onCreate method (we’ll see the usage of the variables going forward with the code):

 ImageView compass_img;
 TextView txt_compass;
 int mAzimuth;
 private SensorManager mSensorManager;
 private Sensor mRotationV, mAccelerometer, mMagnetometer;
 boolean haveSensor = false, haveSensor2 = false;
 float[] rMat = new float[9];
 float[] orientation = new float[3];
 private float[] mLastAccelerometer = new float[3];
 private float[] mLastMagnetometer = new float[3];
 private boolean mLastAccelerometerSet = false;
 private boolean mLastMagnetometerSet = false;

onCreate is the first method that we implement. Inside it we instantiate the SensorManager, the TextView that will contain the azimuth and the l’ImgView that contains the Compass image. Then will be called the start() method:

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_compass);

 mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
 compass_img = (ImageView) findViewById(R.id.img_compass);
 txt_compass = (TextView) findViewById(R.id.txt_azimuth);

 start();
 }

At this point we have to implement the start() method that will grant the access of the sensors, to verify if our device supports them and to show an error message if the device doesn’t have it. We insert this method after the onAccuracyChanged() method (before closing the Compass class):

public void start() {
    if (mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) == null) {
        if ((mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null) || (mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) == null)) {
            noSensorsAlert();
        }
        else {
            mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            haveSensor = mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
            haveSensor2 = mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_UI);
        }
    }
    else{
        mRotationV = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        haveSensor = mSensorManager.registerListener(this, mRotationV, SensorManager.SENSOR_DELAY_UI);
    }
}

As you can see, first of all we verify if the devide supports the RotationVector (Compass + Gyroscope). If so we instantiate it. If not we check if our devide as the Accelerometer and the Compass. If so we instantiate the two sensor. If not we call the noSensorAlert() method to show an error message.

At this point we insert after the start() method the noSensorAlert()  method (and we press the Alt and Return buttons for every library we have to import):

public void noSensorsAlert(){
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setMessage("Your device doesn't support the Compass.")
        .setCancelable(false)
        .setNegativeButton("Close",new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                finish();
            }
        });
alertDialog.show();
}

Before implementing the onSensorChanged() method, we implement the stop()onPause() and the onResume() methods. We insert all this methods after the noSensorAlert():

public void stop() {
    if(haveSensor && haveSensor2){
      mSensorManager.unregisterListener(this,mAccelerometer);
      mSensorManager.unregisterListener(this,mMagnetometer);
    }
    else{
      if(haveSensor)
        mSensorManager.unregisterListener(this,mRotationV);
    }
 }

 @Override
 protected void onPause() {
     super.onPause();
     stop();
 }

 @Override
 protected void onResume() {
     super.onResume();
     start();
 }

Now we have to implement the onSensorChanged() method. First of all we check from which sensor we are receiving data (ROTATION_VECTOR, ACCELEROMETER or MAGNETIC_FIELD). We round the Azimuth value to an integer number and, with respect to the Azimuth, we rotate the compass img and we define in which direction we are pointing (North,West…) our device. At the end we write all in the TextView:

@Override
public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
        SensorManager.getRotationMatrixFromVector(rMat, event.values);
        mAzimuth = (int) (Math.toDegrees(SensorManager.getOrientation(rMat, orientation)[0]) + 360) % 360;
    }

    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
        System.arraycopy(event.values, 0, mLastAccelerometer, 0, event.values.length);
        mLastAccelerometerSet = true;
    } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
        System.arraycopy(event.values, 0, mLastMagnetometer, 0, event.values.length);
        mLastMagnetometerSet = true;
    }
    if (mLastAccelerometerSet && mLastMagnetometerSet) {
        SensorManager.getRotationMatrix(rMat, null, mLastAccelerometer, mLastMagnetometer);
        SensorManager.getOrientation(rMat, orientation);
        mAzimuth = (int) (Math.toDegrees(SensorManager.getOrientation(rMat, orientation)[0]) + 360) % 360;
    }

    mAzimuth = Math.round(mAzimuth);
    compass_img.setRotation(-mAzimuth);

    String where = "NW";

    if (mAzimuth >= 350 || mAzimuth <= 10)
        where = "N";
    if (mAzimuth < 350 && mAzimuth > 280)
        where = "NW";
    if (mAzimuth <= 280 && mAzimuth > 260)
        where = "W";
    if (mAzimuth <= 260 && mAzimuth > 190)
        where = "SW";
    if (mAzimuth <= 190 && mAzimuth > 170)
        where = "S";
    if (mAzimuth <= 170 && mAzimuth > 100)
        where = "SE";
    if (mAzimuth <= 100 && mAzimuth > 80)
        where = "E";
    if (mAzimuth <= 80 && mAzimuth > 10)
        where = "NE";


    txt_compass.setText(mAzimuth + "° " + where);
}

At the end the content of the file Compass.java will be the as it follows:

package com.example.compass;

import android.content.DialogInterface;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.view.ContextThemeWrapper;
import android.widget.ImageView;
import android.widget.TextView;

public class Compass extends AppCompatActivity implements SensorEventListener {

    ImageView compass_img;
    TextView txt_compass;
    int mAzimuth;
    private SensorManager mSensorManager;
    private Sensor mRotationV, mAccelerometer, mMagnetometer;
    boolean haveSensor = false, haveSensor2 = false;
    float[] rMat = new float[9];
    float[] orientation = new float[3];
    private float[] mLastAccelerometer = new float[3];
    private float[] mLastMagnetometer = new float[3];
    private boolean mLastAccelerometerSet = false;
    private boolean mLastMagnetometerSet = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_compass);

        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        compass_img = (ImageView) findViewById(R.id.img_compass);
        txt_compass = (TextView) findViewById(R.id.txt_azimuth);

        start();
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
            SensorManager.getRotationMatrixFromVector(rMat, event.values);
            mAzimuth = (int) (Math.toDegrees(SensorManager.getOrientation(rMat, orientation)[0]) + 360) % 360;
        }

        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            System.arraycopy(event.values, 0, mLastAccelerometer, 0, event.values.length);
            mLastAccelerometerSet = true;
        } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            System.arraycopy(event.values, 0, mLastMagnetometer, 0, event.values.length);
            mLastMagnetometerSet = true;
        }
        if (mLastAccelerometerSet && mLastMagnetometerSet) {
            SensorManager.getRotationMatrix(rMat, null, mLastAccelerometer, mLastMagnetometer);
            SensorManager.getOrientation(rMat, orientation);
            mAzimuth = (int) (Math.toDegrees(SensorManager.getOrientation(rMat, orientation)[0]) + 360) % 360;
        }

        mAzimuth = Math.round(mAzimuth);
        compass_img.setRotation(-mAzimuth);

        String where = "NW";

        if (mAzimuth >= 350 || mAzimuth <= 10)
            where = "N";
        if (mAzimuth < 350 && mAzimuth > 280)
            where = "NW";
        if (mAzimuth <= 280 && mAzimuth > 260)
            where = "W";
        if (mAzimuth <= 260 && mAzimuth > 190)
            where = "SW";
        if (mAzimuth <= 190 && mAzimuth > 170)
            where = "S";
        if (mAzimuth <= 170 && mAzimuth > 100)
            where = "SE";
        if (mAzimuth <= 100 && mAzimuth > 80)
            where = "E";
        if (mAzimuth <= 80 && mAzimuth > 10)
            where = "NE";


        txt_compass.setText(mAzimuth + "° " + where);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {

    }

    public void start() {
        if (mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) == null) {
            if ((mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null) || (mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) == null)) {
                noSensorsAlert();
            }
            else {
                mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
                mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
                haveSensor = mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
                haveSensor2 = mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_UI);
            }
        }
        else{
            mRotationV = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
            haveSensor = mSensorManager.registerListener(this, mRotationV, SensorManager.SENSOR_DELAY_UI);
        }
    }

    public void noSensorsAlert(){
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
        alertDialog.setMessage("Your device doesn't support the Compass.")
                .setCancelable(false)
                .setNegativeButton("Close",new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish();
                    }
                });
        alertDialog.show();
    }

    public void stop() {
        if (haveSensor) {
            mSensorManager.unregisterListener(this, mRotationV);
        }
        else {
            mSensorManager.unregisterListener(this, mAccelerometer);
            mSensorManager.unregisterListener(this, mMagnetometer);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        stop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        start();
    }

}

In the end we press the icon  that permits to simulate our app on our smartphone or with an emulator. Final result:

Video