dimanche 7 mars 2010

Utiliser le senseur d'orientation Android

Les téléphones Android possèdent pour la plupart un senseur d'orientation qui permet de connaître l'orientation du téléphone dans l'espace. L'orientation du téléphone est données par trois valeurs :
  1. l'Azimtuh en degrés
    angle formé par l'axe vertical du téléphone (axe x) et la direction nord
    0° ≤ azimuth ≤ 360°
  2. le Pitch en degrés
    angle formé par l'axe horizontal du téléphone (axe y) et la position horizontal de référence
    -180° ≤ pitch ≤ 180°
  3. le Roll en degrés
    angle formé par l'axe vertical du téléphone (axe x) et la position horizontal de référence
    -90° ≤ roll ≤ 90°
Dans ce tutorial, nous allons voir comment utiliser le senseur d'orientation pour capturer et analyser la position du téléphone dans l'espace. Nous allons développer un Listener pour récupérer les changements d'orientation dans notre application :

package net.androgames.blog.sample.orientation;
 
public interface OrientationListener {
 
    public void onOrientationChanged(float azimuth, 
            float pitch, float roll);
 
    /**
     * Top side of the phone is up
     * The phone is standing on its bottom side
     */
    public void onTopUp();
 
    /**
     * Bottom side of the phone is up
     * The phone is standing on its top side
     */
    public void onBottomUp();
 
    /**
     * Right side of the phone is up
     * The phone is standing on its left side
     */
    public void onRightUp();
 
    /**
     * Left side of the phone is up
     * The phone is standing on its right side
     */
    public void onLeftUp();
 
}

Une instance de la classe SensorManager nous permet de récupérer les informations sur les changements d'orientation du téléphone. L'utilisation du senseur d'orientation ne requière aucune autorisation. La liste des senseurs d'orientation du téléphone se récupère au moyen de la constante caractérisant le type de senseur : Sensor.TYPE_ORIENTATION. S'il existe au moins un Sensor, un SensorEventListener peut alors être associé à l'un des Sensor de la liste afin de récupérer les informations sur les changements d'orientation. Il est possible de maîtriser la fréquence des mises à jour souhaitées grâce aux constantes suivantes :
  1. SensorManager.SENSOR_DELAY_FASTEST : aussi vite que possible
  2. SensorManager.SENSOR_DELAY_GAME : suffisant pour un jeu
  3. SensorManager.SENSOR_DELAY_NORMAL : fréquence normale
  4. SensorManager.SENSOR_DELAY_UI : fréquence compatible pour être traité dans l'UI Thread

Notre manager d'orientation personnalisé est codé comme suit :

package net.androgames.blog.sample.orientation;
 
import java.util.List;
 
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
 
/**
 * Android Orientation Sensor Manager Archetype
 * @author antoine vianey
 * under GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 */
public class OrientationManager {
 
    private static Sensor sensor;
    private static SensorManager sensorManager;
    // you could use an OrientationListener array instead
    // if you plans to use more than one listener
    private static OrientationListener listener;
 
    /** indicates whether or not Orientation Sensor is supported */
    private static Boolean supported;
    /** indicates whether or not Orientation Sensor is running */
    private static boolean running = false;
 
    /** Sides of the phone */
    enum Side {
        TOP,
        BOTTOM,
        LEFT,
        RIGHT;
    }
 
    /**
     * Returns true if the manager is listening to orientation changes
     */
    public static boolean isListening() {
        return running;
    }
 
    /**
     * Unregisters listeners
     */
    public static void stopListening() {
        running = false;
        try {
            if (sensorManager != null && sensorEventListener != null) {
                sensorManager.unregisterListener(sensorEventListener);
            }
        } catch (Exception e) {}
    }
 
    /**
     * Returns true if at least one Orientation sensor is available
     */
    public static boolean isSupported(Context context) {
        if (supported == null) {
            if (Orientation.getContext() != null) {
                sensorManager = (SensorManager) context
                        .getSystemService(Context.SENSOR_SERVICE);
                List<Sensor> sensors = sensorManager.getSensorList(
                        Sensor.TYPE_ORIENTATION);
                supported = new Boolean(sensors.size() > 0);
            } else {
                supported = Boolean.FALSE;
            }
        }
        return supported;
    }
 
    /**
     * Registers a listener and start listening
     */
    public static void startListening(Context context,
            OrientationListener orientationListener) {
        sensorManager = (SensorManager) context
                .getSystemService(Context.SENSOR_SERVICE);
        List<Sensor> sensors = sensorManager.getSensorList(
                Sensor.TYPE_ORIENTATION);
        if (sensors.size() > 0) {
            sensor = sensors.get(0);
            running = sensorManager.registerListener(
                    sensorEventListener, sensor, 
                    SensorManager.SENSOR_DELAY_NORMAL);
            listener = orientationListener;
        }
    }
 
    /**
     * The listener that listen to events from the orientation listener
     */
    private static SensorEventListener sensorEventListener = 
        new SensorEventListener() {
 
        /** The side that is currently up */
        private Side currentSide = null;
        private Side oldSide = null;
        private float azimuth;
        private float pitch;
        private float roll;
 
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
 
        public void onSensorChanged(SensorEvent event) {
 
            azimuth = event.values[0];     // azimuth
            pitch = event.values[1];     // pitch
            roll = event.values[2];        // roll
 
            if (pitch < -45 && pitch > -135) {
                // top side up
                currentSide = Side.TOP;
            } else if (pitch > 45 && pitch < 135) {
                // bottom side up
                currentSide = Side.BOTTOM;
            } else if (roll > 45) {
                // right side up
                currentSide = Side.RIGHT;
            } else if (roll < -45) {
                // left side up
                currentSide = Side.LEFT;
            }
 
            if (currentSide != null && !currentSide.equals(oldSide)) {
                switch (currentSide) {
                    case TOP : 
                        listener.onTopUp();
                        break;
                    case BOTTOM : 
                        listener.onBottomUp();
                        break;
                    case LEFT: 
                        listener.onLeftUp();
                        break;
                    case RIGHT: 
                        listener.onRightUp();
                        break;
                }
                oldSide = currentSide;
            }
 
            // forwards orientation to the OrientationListener
            listener.onOrientationChanged(azimuth, pitch, roll);
        }
 
    };
 
}

Cet OrientationManager personnalisé peut être utilisé dans n'importe quelle Activity ou Service en suivant l'exemple ci-dessous :

package net.androgames.blog.sample.orientation;
 
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
 
/**
 * Android orientation sensor tutorial
 * @author antoine vianey
 * under GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 */
public class Orientation extends Activity implements OrientationListener {
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
 
    protected void onResume() {
        super.onResume();
        if (OrientationManager.isSupported(this)) {
            OrientationManager.startListening(this, this);
        }
    }
 
    protected void onPause() {
        super.onPause();
        if (OrientationManager.isListening()) {
            OrientationManager.stopListening();
        }
 
    }
 
    @Override
    public void onOrientationChanged(float azimuth, 
            float pitch, float roll) {
        ((TextView) findViewById(R.id.azimuth)).setText(
                String.valueOf(azimuth));
        ((TextView) findViewById(R.id.pitch)).setText(
                String.valueOf(pitch));
        ((TextView) findViewById(R.id.roll)).setText(
                String.valueOf(roll));
    }
 
    @Override
    public void onBottomUp() {
        Toast.makeText(this, "Bottom UP", 1000).show();
    }
 
    @Override
    public void onLeftUp() {
        Toast.makeText(this, "Left UP", 1000).show();
    }
 
    @Override
    public void onRightUp() {
        Toast.makeText(this, "Right UP", 1000).show();
    }
 
    @Override
    public void onTopUp() {
        Toast.makeText(this, "Top UP", 1000).show();
    }
 
}

Le meilleur endroit pour associer le manager personnalisé est la méthode onResume() de l'Activity. La désassociation se fera de préférence dans la méthode onPause() de l'Activity afin de ne pas maintenir de ressources inutilisées.

Comme d'habitude, n'hésitez pas à partager vos créations utilisant le senseur d'orientation !

samedi 6 mars 2010

Utiliser le senseur d'accélération Android

Android supporte une grande variété de senseurs qui permettent d'obtenir des informations sur l'environnement, la position ou les déplacements du téléphone. Dans ce tutorial, nous allons voir comment récupérer l'accélération du téléphone pour déterminer si ce dernier est secoué par son utilisateur :

public interface ShakeListener {

    public void onShake(float speed);
    
}

Une instance de SensorManager doit être obtenue afin de connaître la liste des senseurs supportés. Aucune permission n'est requise pour utiliser les senseurs du téléphone. Pour récupérer la liste des Sensor d'accélération du téléphone, il faut demander au SensorManager la liste des senseurs caractérisés par la constante Sensor.TYPE_ACCELEROMETER. Si au moins un senseur est récupéré, il est possible de déclarer un SensorEventListener pour l'un des Sensor. Le senseur d'accélération sélectionné va alors nous informer des changements d'accélération selon l'une des fréquence suivante :
  1. SensorManager.SENSOR_DELAY_FASTEST : aussi rapidement que possible
  2. SensorManager.SENSOR_DELAY_GAME : fréquence adaptée aux jeux
  3. SensorManager.SENSOR_DELAY_NORMAL : fréquence normale
  4. SensorManager.SENSOR_DELAY_UI : fréquence adaptée pour traitement dans l'UI Thread

Pour déterminer si le téléphone est secoué par son utilisateur, il est préférable d'utiliser au moins la fréquence SensorManager.SENSOR_DELAY_GAME.

Le code du détecteur de secousse est le suivant :

package net.androgames.yams.shake;

import java.util.List;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

/**
 * ShakeManager pour Android
 * @author antoine vianey
 * under GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 */
public class ShakeManager {
    
    // vitesse min pour considerer le mouvement
    private static final int SHAKE_THRESHOLD = 6000;    
    // intervalle min entre deux secousses
    private static final int MIN_INTERVAL = 1000; 
    // delais max entre deux valeurs pour considerer un meme mouvement
    private static final int MAX_DELAY = 100;   
    
    private static boolean running = false;
    private static Sensor sensor;
    private static long lastUpdate = 0;
    private static long lastShake = 0;
    private static SensorManager sensorManager;
    private static ShakeListener listener;
    
    private static Boolean supported;
    
    private static float x = 0;
    private static float y = 0;
    private static float z = 0;
    private static float lastX = 0;
    private static float lastY = 0;
    private static float lastZ = 0;
    
    public static boolean isListening() {
        return running;
    }
    
    /**
     * Arrete l'ecoute des secousses
     */
    public static void stopListening() {
        running = false;
        try {
            if (sensorManager != null && sensorEventListener != null) {
                sensorManager.unregisterListener(sensorEventListener);
            }
        } catch (Exception e) {}
    }
    
    /**
     * Retourne true si la fonctionnalite est supportee
     * @param context
     * @return
     */
    public static boolean isSupported(Context context) {
        if (supported == null) {
            if (context != null) {
                sensorManager = (SensorManager) context.getSystemService(
                        Context.SENSOR_SERVICE);
                List<Sensor> sensors = sensorManager.getSensorList(
                        Sensor.TYPE_ACCELEROMETER);
                supported = new Boolean(sensors.size() > 0);
            } else {
                supported = Boolean.FALSE;
            }
        }
        return supported;
    }
    
    /**
     * Demarre l'ecoute des secousses du telephone
     * @param shakeListener
     * @param context
     */
    public static void startListening(ShakeListener shakeListener, 
            Context context) {
        sensorManager = (SensorManager) context.getSystemService(
                Context.SENSOR_SERVICE);
        List<Sensor> sensors = sensorManager.getSensorList(
                Sensor.TYPE_ACCELEROMETER);
        if (sensors.size() > 0) {
            sensor = sensors.get(0);
        }
        sensorManager.registerListener(sensorEventListener, sensor, 
                SensorManager.SENSOR_DELAY_GAME);
        listener = shakeListener;
    }

    private static SensorEventListener sensorEventListener = 
        new SensorEventListener() {

        private long now, timeDiff;
        private float speed;
        
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
        
        public void onSensorChanged(SensorEvent event) {
            now = System.currentTimeMillis();
            
            x = event.values[0];
            y = event.values[1];
            z = event.values[2];
            
            if (lastUpdate == 0) {
                lastUpdate = now;
                lastShake = now;
                lastX = x;
                lastY = y;
                lastZ = z;
            } else {
                timeDiff = now - lastUpdate;
                if (timeDiff > 0 && timeDiff < MAX_DELAY) {
                    speed = Math.abs(x + y + z - lastX - lastY - lastZ) 
                                / timeDiff * 10000;
                    if (speed > SHAKE_THRESHOLD) {
                        if (now - lastShake >= MIN_INTERVAL) {
                            listener.onShake(speed);
                            lastShake = now;
                            now = 0;
                        }
                    }
                }
                lastX = x;
                lastY = y;
                lastZ = z;
                lastUpdate = now;
            }
                
        }
        
    };

}

Dans le cas d'un SensorEvent retourné par un Sensor de type Sensor.TYPE_ACCELEROMETER, les informations retournées représentent l'accélération du téléphone dans un système de coordonnées cartésiens. Pour un téléphone au repos en position horizontale, les valeurs retournées doivent-être :
  1. 0 m/s2 selon l'axe x
  2. 0 m/s2 selon l'axe y
  3. 9,80665 m/s2 selon l'axe z (attraction terrestre)

D'un événement à l'autre, les coordonnées de l'accélération sont stockées afin de détecter les changements soudains d'accélération et déclencher l'événement onShake lorsque le threshold est atteint.

Vous pouvez également implémenter vos propres méthodes de détection de mouvements et nous les présenter ici même.

Amusez vous bien !
Fork me on GitHub