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 !

samedi 16 janvier 2010

Comment réaliser des Live Wallpaper Android

Les Live Wallpaper sont une fonctionnalité introduite par la version 2.1 d'Android 2.1. Les Live wallpapers sont des fonds d'écran pouvant être nimés et interractifs pour la Home de votre téléphone. Dans ce tutorial, nous allons voir comment réaliser un Live Wallpaper interactif et animé.

La classe principale permettant de développer un Live Wallpaper est la classe WallpaperService. Cette classe encapsule la classe fille WallpaperService.Engine. L'Engine est l'interface entre le papier peint et l'interface utilisateur et contient la Surface sur laquelle le fond d'écran est rendu.

La première étape consiste à étendre la classe WallpaperService et son WallpaperService.Engine. Comme tout service Android, un WallpaperService doit être déclaré au niveau du fichier AndroidManifest.xml avec l'Intent android.service.wallpaper.WallpaperService afin que le système Android identifie ce service en tant que Live Wallpaper. La permission android.permission.BIND_WALLPAPER doit également être positionnée afin d'autoriser les utilisateurs à utiliser ce Live Wallpaper sur leur Home :
<service 
    android:name="LiveWallpaperService"
    android:enabled="true"
    android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:permission="android.permission.BIND_WALLPAPER">
    <intent-filter android:priority="1" >
        <action android:name="android.service.wallpaper.WallpaperService" />
    </intent-filter>
    <meta-data 
      android:name="android.service.wallpaper" 
      android:resource="@xml/wallpaper" />
</service>
Un fichier XML spécifique doit être placé dans le répertoire /res/xml/ de votre projet. Il permet de fournir une description du Live Wallpaper :
<?xml version="1.0" encoding="UTF-8"?>
<wallpaper 
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:thumbnail="@drawable/thumbnail" 
    android:description="@string/description"
    android:settingsActivity="PreferenceActivity"/>
Le fichier attrs.xml d'Android décrit les différentes valeurs descriptibles pour les Live Wallpaper :
<declare-styleable name="Wallpaper">
    <!-- Nom du composant préférences du Live Wallpaper
         permettant d'en modifier les caractéristiques. -->
    <attr name="settingsActivity" />
    <!-- Une icone a afficher dans la liste 
         des Live Wallpaper disponibles. -->
    <attr name="thumbnail" format="reference" />
    <!-- Nom de l'auteur du Live Wallpaper. -->
    <attr name="author" format="reference" />
    <!-- Description du Live Wallpaper. -->
    <attr name="description" />
</declare-styleable>
Une implémenation type du Live Wallpaper pourrait-être :
package net.androgames.blog.sample.livewallpaper;
 
import android.content.SharedPreferences;
import android.service.wallpaper.WallpaperService;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
 
/**
 * Android Live Wallpaper Archetype
 * @author antoine vianey
 * under GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 */
public class LiveWallpaperService extends WallpaperService {
 
    @Override
    public Engine onCreateEngine() {
        return new SampleEngine();
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
 
    public class SampleEngine extends Engine {
 
        private LiveWallpaperPainting painting;
 
        SampleEngine() {
            SurfaceHolder holder = getSurfaceHolder();
            painting = new LiveWallpaperPainting(holder, 
                getApplicationContext());
        }
 
        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);
            // register listeners and callbacks here
            setTouchEventsEnabled(true);
        }
 
        @Override
        public void onDestroy() {
            super.onDestroy();
            // remove listeners and callbacks here
            painting.stopPainting();
        }
 
        @Override
        public void onVisibilityChanged(boolean visible) {
            if (visible) {
                // register listeners and callbacks here
                painting.resumePainting();
            } else {
                // remove listeners and callbacks here
                painting.pausePainting();
            }
        }
 
        @Override
        public void onSurfaceChanged(SurfaceHolder holder, int format, 
                int width, int height) {
            super.onSurfaceChanged(holder, format, width, height);
            painting.setSurfaceSize(width, height);
        }
 
        @Override
        public void onSurfaceCreated(SurfaceHolder holder) {
            super.onSurfaceCreated(holder);
            // start painting
            painting.start();
        }
 
        @Override
        public void onSurfaceDestroyed(SurfaceHolder holder) {
            super.onSurfaceDestroyed(holder);
            boolean retry = true;
            painting.stopPainting();
            while (retry) {
                try {
                    painting.join();
                    retry = false;
                } catch (InterruptedException e) {}
            }
        }
 
        @Override
        public void onOffsetsChanged(float xOffset, float yOffset, 
                float xStep, float yStep, int xPixels, int yPixels) {
        }
 
        @Override
        public void onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            painting.doTouchEvent(event);
        }
 
    }
 
}
Les méthodes onCreate, onDestroy, onVisibilityChanged, onSurfaceChanged, onSurfaceCreated et onSurfaceDestroyed de l'Engine sont appelées lorsque l'état, la taille où la visibilité de la surface de rendu changent. Grâce à ces méthodes, il nous est possible d'animer le Live Wallpaper que lorsque cela est nécessaire. Les événements de toucher d'écran sont activés via la méthode setTouchEventsEnabled(true) récupérés par le callback onTouchEvent(MotionEvent event).

Le rendu du Live Wallpaper est effectué par un Thread spécialisé :
package net.androgames.blog.sample.livewallpaper;
 
import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
 
/**
 * Android Live Wallpaper painting thread Archetype
 * @author antoine vianey
 * GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 */
public class LiveWallpaperPainting extends Thread {
 
    /** Reference to the View and the context */
    private SurfaceHolder surfaceHolder;
    private Context context;
 
    /** State */
    private boolean wait;
    private boolean run;
 
    /** Dimensions */
    private int width;
    private int height;
 
    /** Time tracking */
    private long previousTime;
    private long currentTime;
 
    public LiveWallpaperPainting(SurfaceHolder surfaceHolder, 
            Context context) {
        // keep a reference of the context and the surface
        // the context is needed if you want to inflate
        // some resources from your livewallpaper .apk
        this.surfaceHolder = surfaceHolder;
        this.context = context;
        // don't animate until surface is created and displayed
        this.wait = true;
    }
 
    /**
     * Pauses the live wallpaper animation
     */
    public void pausePainting() {
        this.wait = true;
        synchronized(this) {
            this.notify();
        }
    }
 
    /**
     * Resume the live wallpaper animation
     */
    public void resumePainting() {
        this.wait = false;
        synchronized(this) {
            this.notify();
        }
    }
 
    /**
     * Stop the live wallpaper animation
     */
    public void stopPainting() {
        this.run = false;
        synchronized(this) {
            this.notify();
        }
    }
 
    @Override
    public void run() {
        this.run = true;
        Canvas c = null;
        while (run) {
            try {
                c = this.surfaceHolder.lockCanvas(null);
                synchronized (this.surfaceHolder) {
                    currentTime = System.currentTimeMillis();
                    updatePhysics();
                    doDraw(c);
                    previousTime = currentTime;
                }
            } finally {
                if (c != null) {
                    this.surfaceHolder.unlockCanvasAndPost(c);
                }
            }
            // pause if no need to animate
            synchronized (this) {
                if (wait) {
                    try {
                        wait();
                    } catch (Exception e) {}
                }
            }
        }
    }
 
    /**
     * Invoke when the surface dimension change
     */
    public void setSurfaceSize(int width, int height) {
        this.width = width;
        this.height = height;
        synchronized(this) {
            this.notify();
        }
    }
 
    /**
     * Invoke while the screen is touched
     */
    public void doTouchEvent(MotionEvent event) {
        // handle the event here
        // if there is something to animate
        // then wake up
        this.wait = false;
        synchronized(this) {
            notify();
        }
    }
 
    /**
     * Do the actual drawing stuff
     */
    private void doDraw(Canvas canvas) {}
 
    /**
     * Update the animation, sprites or whatever.
     * If there is nothing to animate set the wait
     * attribute of the thread to true
     */
    private void updatePhysics() {
        // if nothing was updated :
        // this.wait = true;
    }
 
}
Cette classe est optimisée pour n'effectuer le rendu de la surface que si celle ci est visible et qu'un élément à rendre a été déplacé. S'il n'y a rien à animer, la méthode updatePhysics indique au Thread d'attendre. La méthode doDraw(Canvas canvas) doit quand même être appelée une dernière fois car les SurfaceView utilisent deux canvas en alternance pour le rendu...

Pour autoriser la configuration d'un Live Wallpaper, il suffit de créer une PreferenceActivity et associée celle-ci au Live Wallpaper via le fichier XML de description d'un Live Wallpaper vu plus haut. Les valeurs des préférences peuvent être récupérée via un objet SharedPreference.

Le code source de ce Live Wallpaper est disponible ici : SampleLiveWallpaper.



N'hésitez pas à présenter vos réalisation en commentaire de ce post !

dimanche 22 novembre 2009

Créer des boutons Android personnalisés

Dans ce tutorial, nous allons voir comment il est possible de créer un style personnalisé pour les boutons d'une application Android. L'API Android fournit toutes les libertés possibles pour redéfinir l'apparence de base des différents éléments qui constituent une User Interface (UI). Dans un premier temps, nous allons créer trois NinePatch Drawables qui vont servir d'arrière plan pour nos boutons personnalisés :

Un arrière plan noir pour l'apparence de base des boutons :


Un arrière plan orange pour l'apparence des boutons lorsque ceux-ci sont pressés par l'utilisateur :


Un arrière plan rouge pour les boutons désactivés :



Ces trois arrières plans au format PNG doivent être placés dans le répertoire /res/drawables de notre application Android. L'étape suivante consiste à paramétrer l'arrière plan correspondant aux différents états de nos boutons personnalisés. Cette configuration se fait au moyen du fichier XML suivant :

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item 
    android:state_enabled="false"
        android:drawable="@drawable/btn_red" />
    <item 
        android:state_pressed="true" 
        android:state_enabled="true"
        android:drawable="@drawable/btn_orange" />
    <item 
        android:state_focused="true" 
        android:state_enabled="true"
        android:drawable="@drawable/btn_orange" />
    <item 
        android:state_enabled="true"
        android:drawable="@drawable/btn_black" />
</selector>

Chaque état possible des boutons est associé à un arrière plan. Lorsque le bouton est rendu, l'arrière plan sélectionné est le premier pour lequel l'état déclaré dans le fichier XML concorde avec celui du bouton (L'API considère le premier concordant plutôt que celui qui vérifie le plus grand nombre de critères). Dans l'exemple ci-dessous, l'arrière plan orange ne sera jamais rendu pour un bouton pressé car le premier arrière plan déclaré dans le fichier XML est associé à un état toujours vérifié lorsque le bouton est actif et pressé :

<item 
    android:state_enabled="false"
    android:drawable="@drawable/btn_red" />
<item 
    android:state_pressed="true" 
    android:state_enabled="false"
    android:drawable="@drawable/btn_orange" />

Pour que l'arrière plan orange soit rendu pour les boutons inactifs et pressés, il faut placer les conditions les plus restrictives en premier, avant celles paramétrant l'affichage de l'arrière plan rouge :

<item 
    android:state_pressed="true" 
    android:state_enabled="false"
    android:drawable="@drawable/btn_orange" />
<item 
    android:state_enabled="false"
    android:drawable="@drawable/btn_red" />

Un fois notre selector enregistré dans le fichier btn_custom.xml du répertoire /res/drawables, il nous est possible de déclarer un style applicable aux boutons de notre application Android. Les styles sont déclarables dans les fichiers /res/values/styles.xml et /res/values/themes.xml. Le premier permet de paramétrer l'apparence complète du bouton (arrière plan, police, taille et couleur du text, ...) :

<resources>
    <style name="Button" parent="@android:style/Widget.Button">
        <item name="android:gravity">center_vertical|center_horizontal</item>
        <item name="android:textColor">#FFFFFFFF</item>
        <item name="android:shadowColor">#FF000000</item>
        <item name="android:shadowDx">0</item>
        <item name="android:shadowDy">-1</item>
        <item name="android:shadowRadius">0.2</item>
        <item name="android:textSize">16dip</item>
        <item name="android:textStyle">bold</item>
        <item name="android:background">@drawable/btn_custom</item>
        <item name="android:focusable">true</item>
        <item name="android:clickable">true</item>
    </style>
</resources>

Le second est utilisé pour appliquer le style à l'ensemble des Button d'un application déclarant le thème CustomButton :

<resources>
    <style name="CustomButton" parent="android:style/Theme.NoTitleBar">
        <item name="android:buttonStyle">@style/Button</item>
    </style>
</resources>

Pour appliquer ce thème à notre application, il faut utiliser l'attribut android:theme de la balise application <application> dans le fichier AndroidManifest.xml :

<application android:icon="@drawable/icon" 
    android:label="Custom button"
    android:theme="@style/CustomButton">

Et voilà à quoi les boutons déclarés dans nos Layout vont ressembler :


Cette customisation peut s'appliquer de la même manière à l'ensemble des éléments de l'interface utilisateur : Menu, List, Checkbox, EditText, ... Cela permet de créer des applications avec une expérience utilisateur entièrement personnalisée.
Fork me on GitHub