In questo articolo vedremo come creare un’applicazione per la Bussola. La versione di Android studio utilizzata per questo tutorial è la 2.2.2. Alla fine dell’articolo è presente un video in cui vengono spiegate tutte le operazioni svolte di seguito.

Prepariamo il nostro progetto. Per prima cosa apriamo Android Studio. Selezioniamo “Start a new Android Studio Project” se ci troviamo nella schermata iniziale oppure, se il programma è già stato avviato, andiamo nel menu File->New e clicchiamo su “New project”. Nella prima finestra di creazione del nuovo progetto definiamo il nome “Compass” nel campo Application name e clicchiamo su “Next”:

La seconda finestra ci fa scegliere la versione minima di Android con cui sarà compatibile la nostra App. Lasciamo l’impostazione di default e clicchiamo su “Next”:

Nella terza finestra selezioniamo “Empty Activity” e clicchiamo su “Next”:

Nell’ultima finestra cambiamo il nome alla nostra Activity e la chiamiamo “Compass” ed infine premiamo su “Finish”:

A questo punto verrà creato il nostro progetto. Prima di iniziare le modifiche cliccate qui per scaricare l’immagine da utilizzare per la nostra bussola. i file che andremo a modificare sono i seguenti:

AndroidManifest.xml è il file che contiene tutte le caratteristiche principali di tutte le Activity della nostra App.
Compass.java è il codice Java associato all’Activity dello Compass.
activity_compass.xml è il file xml che definisce l’aspetto (layout) dell’Activity Compass.

Per prima cosa andiamo a modificare l’orientamento della nostra App. Per farlo apriamo il file androidmanifest.xml (che si trova nella cartella app/manifest/) ed aggiungiamo questa riga tra la scritta “.Compass”  ed il carattere “>”:

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

Dopo questa operazione questo sarà il contenuto del file AndroidManifest.xml:

<?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>

Adesso andiamo a modificare il layout dell’Activity Compass. Subito ci spostiamo nella modalità text (scheda in basso)  del file activity_compass.xml (che si trova nella cartella app/res/layout/) e, per prima cosa, modifichiamo le proprietà della TextView di default che utilizzeremo per mostrare l’Azimuth (gradi rispetto al Nord) della nostra Bussola:

<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"/>

Prima di procedere con l’inserimento dell’immagine della nostra Bussola (che avete scaricato prima di iniziare) dobbiamo spostarla all’interno del nostro progetto. Per farlo andiamo a cercarla nella cartella dei Download del nostro browser e la copiamo nella cartella app/src/main/res/drawable (percorso che si trova all’interno del nostro progetto). A questo punto possiamo direttamente importarla nel nostro progetto. Sotto la nostra TextView andiamo a inserire una ImgView con le seguenti proprietà:

<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"/>

Dopo queste due operazioni questo sarà il contenuto del file activity_compass.xml:

<?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>

Se avete scritto tutto il codice correttamente, spostandovi nella modalità Design (scheda in basso) dovreste vedere questo risultato:

A questo punto ci rimane solo da modificare e specificare il comportamento dell’Activity Compass. Apriamo il file Compass.java (che si trova nella cartella app/java/com.example.compass/) e modifichiamo i seguenti contenuti:

Per prima cosa andiamo ad inserire di fianco alla scritta AppCompatActivity la scritta implements SensorEventListener, premiamo i tasti Alt  e Invio , clicchiamo su Implements Methods e, nella finestra che si è appena aperta, clicchiamo su Ok. A questo punto il nostro codice è in questa situazione:

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) {

    }
}

 Come vedete abbiamo tre metodi principali:

onCreate è il metodo principale che viene eseguito non appena viene avviata l’applicazione.
onSensorChanged è il metodo che viene eseguito non appena un qualsiasi sensore del nostro dispositivo cambia di stato.
onAccuracyChanged è il metodo che viene eseguito quando cambia la precisione della nostra misura, ma non lo utilizzeremo.

Prima di iniziare ad implementare i nostri metodi dobbiamo dichiarare le variabili che ci serviranno per tutto il codice e le andremo a scrivere prima della scritta @Override del metodo onCreate (man mano nel codice vedremo il loro utilizzo):

 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;

Il primo metodo che andiamo ad implementare è il metodo onCreate. Al suo interno instanziamo il gestore dei sensori (SensorManager) e la TextView che conterrà l’azimuth e l’ImgView che contiene l’immagine della bussola. Infine verrà richiamato il metodo start() che andremo a definire successivamente:

@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();
 }

A questi punto andiamo ad implementare il metodo start() che ci permetterà di accedere ai sensori che ci interessano, di verificare se il nostro dispositivo ne è provvisto e di mostrare un messaggio di errore in caso non lo fosse. Questo metodo lo andremo ad inserire appena sotto il metodo onAccuracyChanged() (prima della parentesi graffa che chiude la classe della nostra Activity):

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);
    }
}

Come vedete dal codice per prima cosa verifichiamo se il nostro dispositivo supporta il RotationVector che è l’insieme del sensore della Bussola e del Giroscopio. In caso affermativo lo instanziamo. In caso negativo controlliamo che il nostro dispositivo sia provvisto dell’Accelerometro e della Bussola. In caso affermativo instanziamo i due sensori, in caso negativo chiamiamo il metodo noSensorAlert() che ci mostra il messaggio di errore.

A questo punto inseriamo sotto il metodo start() il metodo noSensorAlert() andando a premere i tasti Alt e Invio per ogni libreria che ci viene richiesto di importare:

public void noSensorsAlert(){
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setMessage("Il tuo dispositivo non supporta la Bussola.")
        .setCancelable(false)
        .setNegativeButton("Chiudi",new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                finish();
            }
        });
alertDialog.show();
}

Prima di andare ad implementare il metodo onSensorChanged() implementiamo ancora il metodo stop() che ci permette di stoppare la cattura di informazioni dai sensori ed andiamo a sovrascrivere le azioni da compiere quando l’applicazione è messa in pausa con il metodo onPause() e quando l’applicazione viene ripresa con il metodo onResume(). Tutti e tre i metodi li andiamo ad inserire sotto il metodo 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();
 }

A questo punto non ci rimane che implementare il metodo onSensorChanged(). Per prima cosa controlliamo da quale sensore stiamo ricevendo le informazioni (ROTATION_VECTOR, ACCELEROMETER MAGNETIC_FIELD). Arrotondiamo il valore dell’Azimuth ad un valore intero e, in base all’Azimuth ruotiamo l’immagine della Bussola e definiamo in quale direzione stiamo puntando (Nord, Est, Nord-Est…) il nostro dispositivo. Infine visualizziamo il valore nella 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 = "NO";

    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);
}

Ricapitolando, il contenuto del file Compass.java sarà il seguente:

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 = "NO";

        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("Il tuo dispositivo non supporta la Bussola.")
                .setCancelable(false)
                .setNegativeButton("Chiudi",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();
    }

}

Per finire, testiamo la nostra applicazione cliccando sull’icona  che ci permetterà di testare la nostra applicazione o sul nostro smartphone oppure sul computer tramite un emulatore. Risultato finale:

Video: