Developpez.com

Club des développeurs et IT pro
Plus de 4 millions de visiteurs uniques par mois

Création d'une boussole

Ce tutoriel va vous présenter, de façon globale, comment créer une vue et l'animer.

14 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Ce tutoriel a pour objectif de vous permettre de créer une boussole. Celle-ci réagira en fonction de l'orientation de votre téléphone, pourvu que votre mobile soit muni d'une boussole numérique.

Outre le résultat final, l'intérêt majeur de ce tutoriel est d'aborder la création d'une vue et son animation. Nous allons ainsi aborder les principes de bases nécessaires à la création et l'animation d'une vue personnalisée, mais également les manipulations élémentaires d'un canevas sous Android.

I-A. Le résultat

Boussole finale
La boussole que nous allons créer

À la fin de ce tutoriel, vous obtiendrez la boussole ci-dessus. Celle-ci ne brille pas forcément de par son design, mais a le mérite d'être animée et de laisser libre cours à votre imagination pour l'agrémenter.

I-B. Version du SDK utilisé

Ce tutoriel a été compilé avec la version 1.5 du SDK, et le code utilisé reste inchangé jusqu'à (au moins) la version 2.1 du SDK.

II. Création de la vue

Dans cette partie, nous allons présenter uniquement la création de la vue de la boussole. Ainsi, on abordera l'héritage des classes de bases du SDK, ainsi que les méthodes élémentaires à redéfinir.

II-A. Spécification

Notre classe CompassView devra être capable d'afficher une boussole pointant son aiguille vers le nord. Pour ce faire, notre vue aura deux méthodes : la première pour indiquer l'orientation du nord, la seconde pour récupérer cette valeur. La classe possédera donc un attribut privé northOrientation qui permettra de sauvegarder en interne l'orientation du nord en degrés. Cette orientation sera comprise entre 0 et 360, et suivra la convention suivante :

Image non disponible
Convention adoptée pour l'orientation

II-B. Réalisation

Tous les éléments graphiques de bases sous Android (TextView, CheckBox, etc.) héritent de la classe View. Cette classe View est la brique de base permettant de construire l'interface homme-machine d'une application. C'est elle qui dessine sur l'écran et qui intercepte les actions de l'utilisateur (toucher une zone de l'écran par exemple). Pour créer la vue de notre boussole, nous allons créer une classe CompassView qui va hériter de la classe View. D'après les spécifications de notre vue présentées précédemment, nous pouvons aboutir à la classe incomplète suivante :

CompassView.java
Sélectionnez

public class CompassView extends View {
    //~--- fields -------------------------------------------------------------
    //Rotation vers la droite en degree pour pointer le Nord 
    private float northOrientation=0;

    //~--- constructors -------------------------------------------------------
    public CompassView(Context context) {
        super(context);
    }

    // Constructeur utilisé pour instancier la vue depuis sa
    // déclaration dans un fichier XML
    public CompassView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // idem au précédant
    public CompassView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    //~--- get methods --------------------------------------------------------

    // permet de récupérer l'orientation de la boussole
    public float getNorthOrientation() {
        return northOrientation;
    }

    //~--- set methods --------------------------------------------------------

    // permet de changer l'orientation de la boussole
    public void setNorthOrientation(float rotation) {
    	//on met à jour l'orientation uniquement si elle a changé
    	if (rotation != this.northOrientation)
    	{
    		this.northOrientation = rotation;
			//on demande à notre vue de se redessiner
			this.invalidate();
    	}
    }
}

Notre classe contient à présent les données et les méthodes élémentaires qui seront utilisées pour la manipuler. Nous allons maintenant nous concentrer sur le coeur de métier de cette classe, c'est à dire l'affichage de la boussole en elle-même. Une grande partie du code va être concentrée dans deux méthodes héritées de la classe View :

  • onMeasure;
  • onDraw.

La première méthode, onMeasure, est appelée lors de l'instanciation de notre vue par son parent. Cette méthode permet à notre vue de déclarer la taille sur l'écran qui lui est nécessaire pour se dessiner. La seconde méthode, onDraw, est appelée lorsque la vue doit dessiner son contenu, c'est dans celle-ci que nous userons du pinceau pour dessiner notre boussole.

Voici un tableau résumant ce que nous venons de dire :

Catégorie Méthodes Description
Layout onMeasure(int, int) Appelée pour déterminer la taille de la vue (et de ses enfants)
Drawing onDraw(Canvas) Appelée quand la vue doit dessiner son contenu

II-B-1. La méthode onMeasure

La méthode onMeasure permet à notre vue de déclarer, à la vue qui est hiérarchiquement supérieure, la taille qui lui est nécessaire pour se dessiner à l'écran. Il faut savoir que si cette méthode n'est pas redéfinie, notre vue fera par défaut 100x100 pixels et que par conséquent, quand bien même vous dessineriez de grandes choses, vous ne verrez rien au-delà de ce carré de 100x100 pixels que forme votre vue.

Dans le cas de la boussole, nous allons laisser notre vue être un carré aussi grand que possible, c'est à dire, aussi grand que le propose la vue parente de notre boussole. Comment est-ce possible ? Il nous suffit de regarder du côté des paramètres d'appel de onMeasure. Les deux paramètres widthMeasureSpec et heightMeasureSpec contiennent l'éventuelle taille que le parent se propose d'offrir à notre vue. Vous avez bien lu "éventuelle ", car c'est à ce niveau que les choses se complexifient. Il nous faudra donc tester ces paramètres, et forcer une taille par défaut si le parent ne nous permet pas de recueillir les informations concernant la taille disponible.

CompassView.java
Sélectionnez

//~--- methods ------------------------------------------------------------
// Permet de définir la taille de notre vue
// /!\ par défaut un cadre de 100x100 si non redéfini
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int measuredWidth  = measure(widthMeasureSpec);
    int measuredHeight = measure(heightMeasureSpec);

    // Notre vue sera un carré, on garde donc le minimum
    int d = Math.min(measuredWidth, measuredHeight);

    setMeasuredDimension(d, d);
}

// Déterminer la taille de notre vue
private int measure(int measureSpec) {
    int result   = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.UNSPECIFIED) {
        // Le parent ne nous a pas donné d'indications,
		// on fixe donc une taille
        result = 150;
    } else {
        // On va prendre la taille de la vue parente
        result = specSize;
    }

    return result;
}

II-B-2. La méthode onDraw

Après avoir déclaré la taille de notre vue (via la méthode onMeasure), nous allons dessiner à l'intérieur de celle-ci en utilisant la méthode onDraw. Cette méthode sera appelée "automatiquement" lors du premier dessin de la fenêtre (ou à chaque fois que l'on appellera explicitement la méthode invalidate de notre vue).

Avant de procéder à la phase du dessin, nous devons repenser notre façon de programmer : notre code doit être optimisé autant que possible, car la fluidité de notre animation dépendra de cette portion de code. Notre petit robot vert est muni d'un Garbage Collector assez simple qui, pour faire le ménage, bloque l'exécution du programme pendant 100 ms (plus spécifiquement l'exécution du thread responsable de l'interface graphique, l'UI thread). La résultante du déclenchement du Garbage Collector au moment de la procédure de dessin est un affichage saccadé (très visible dans les animations). Une téléportation des différents éléments fera place à l'animation proprement dite, ce qui n'est pas très esthétique. La règle d'or est donc d'éviter autant que possible d'instancier de nouveaux objets dans le code de la méthode onDraw.

Pour cela, nous allons commencer par créer une méthode privée initView qui sera appelée dans les constructeurs de CompassView.java et qui aura pour but d'instancier tous les objets manipulés dans la méthode onDraw. De cette manière, on instanciera tout ce dont nous aurons besoin pour dessiner, et ainsi notre méthode onDraw n'aura que peu de contenu dynamique ce qui limitera l'exécution du GC.

CompassView.java
Sélectionnez

// Constructeur par défaut de la vue
public CompassView(Context context) {
    super(context);
    initView();//faire de même pour les 2 autres constructeurs
}

Pour dessiner notre boussole, 4 objets nous sont nécessaires/indispensables. Tout d'abord nous aurons besoin d'un Path trianglePath, qui sera un ensemble de lignes formant un triangle pour dessiner la forme de nos aiguilles. Puis nous aurons besoin de circlePaint, northPaint et southPaint qui seront des " pinceaux " utilisés pour dessiner respectivement l'arrière-plan de la boussole, l'aiguille du nord et celle du sud. Nous déclarerons trois "pinceaux" différents car ils nous permettront de dessiner avec des couleurs différentes.

CompassView.java
Sélectionnez

//~--- fields -------------------------------------------------------------
private Paint circlePaint;
private Paint northPaint;
private Paint southPaint;

private Path trianglePath;

//~--- methods ------------------------------------------------------------
private void initView() {
    Resources r = this.getResources();

    // Paint pour l'arrière plan de la boussole
    circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); // Lisser les formes
    circlePaint.setColor(r.getColor(R.color.compassCircle)); // Définir la couleur

    // Paint pour les 2 aiguilles, Nord et Sud
    northPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    northPaint.setColor(r.getColor(R.color.northPointer));
    southPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    southPaint.setColor(r.getColor(R.color.southPointer));

    // Path pour dessiner les aiguilles
    trianglePath = new Path();
}

Remarque : vous pouvez constater que les couleurs ne sont pas codées en dur mais font référence à un fichier de ressources " R.color.xxx " décrit ci-dessous.

Image non disponible
color.xml
Sélectionnez

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<color name="northPointer">#FF0000</color>
	<color name="southPointer">#000000</color>
	<color name="compassCircle">#CCCCFF</color>
</resources>

Voilà, le terrain est à présent prêt et il ne nous reste plus qu'à écrire le corps de la fonction pour dessiner. onDraw possède un seul paramètre : le canevas étant l'équivalent d'une toile sur laquelle nous allons dessiner. Toutes les actions de dessins vont donc avoir lieu sur cet objet. Nous allons ainsi utiliser drawCircle pour dessiner un cercle (représentant le fond de notre boussole) et des drawPath pour dessiner des formes composées par nous-mêmes (représentant les aiguilles de notre boussole).

CompassView.java
Sélectionnez

// Appelée pour redessiner la vue
@Override
protected void onDraw(Canvas canvas) {
	//On détermine le point au centre de notre vue
    int centerX = getMeasuredWidth() / 2;
    int centerY = getMeasuredHeight() / 2;

    // On détermine le diamètre du cercle (arrière plan de la boussole)
    int radius = Math.min(centerX, centerY);

  	// On dessine un cercle avec le " pinceau " circlePaint
    canvas.drawCircle(centerX, centerY, radius, circlePaint);

    // On sauvegarde la position initiale du canevas
    canvas.save();

    // On tourne le canevas pour que le nord pointe vers le haut
    canvas.rotate(-northOrientation, centerX, centerY);

    // on créer une forme triangulaire qui part du centre du cercle et
    // pointe vers le haut
    trianglePath.reset();//RAZ du path (une seule instance)
    trianglePath.moveTo(centerX, 10);
    trianglePath.lineTo(centerX - 10, centerY);
    trianglePath.lineTo(centerX + 10, centerY);

    // On désigne l'aiguille Nord
    canvas.drawPath(trianglePath, northPaint);
    
    // On tourne notre vue de 180° pour désigner l'auguille Sud
    canvas.rotate(180, centerX, centerY);
    canvas.drawPath(trianglePath, southPaint);

    // On restaure la position initiale (inutile dans notre exemple, mais prévoyant)
    canvas.restore();
}

Sous Android, le canevas se manipule comme sous Swing, c'est-à-dire qu'il s'agit d'une toile qu'on peut déplacer un peu comme une feuille de papier. Vous remarquerez que nous avons déplacé le canevas avec "rotate", ce qui nous a permis de le faire tourner autour d'un point. Le centre choisi pour la rotation est tout simplement le centre du cercle. La première manipulation du canevas est une rotation amenant le nord en haut de notre canevas. Ensuite, nous avons simplement dessiné la première aiguille, puis nous avons fait une rotation de 180 ° pour dessiner la deuxième aiguille (celle du Sud) opposée à la première.

II-B-2-a. Canvas save et restore

Vous remarquerez que nous avons utilisé les deux méthodes save et restore de l'objet canevas qui ne sont pas utiles dans notre exemple du dessin de la boussole, mais que nous allons tout de même introduire et expliquer.

II-B-2-a-i. Intérêt

Ces méthodes permettent de sauvegarder la position initiale du canevas, ainsi toute manipulation de ce dernier (déplacement ou rotation) pourra être annulée.

II-B-2-a-ii. Fonctionnement
  • Save permet de sauvegarder la position actuelle de votre canevas (ne sauvegarde pas le contenu).
  • Restore permet de restaurer une position de votre canevas.

La sauvegarde et la restauration fonctionnent comme une pile LIFO, c'est-à-dire que si on sauvegarde une position A puis une position B, le premier appel à restore va restaurer la position B et le second la position A.

Voilà pour la petite histoire sur save et restore, retour donc à la vue de notre boussole. Notre vue est à présent finalisée, il ne nous reste plus qu'à observer le rendu.

III. Exemple d'utilisation de la vue boussole

C'est bien beau tout ce travail, mais on aimerait bien en voir la couleur, n'est-ce pas ? Pour ce faire, il suffit de créer une Activity qui va instancier sa propre interface graphique à partir d'un fichier XML.

Boussole.java
Sélectionnez

public class Boussole extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Déclarons le XML main.xml qui va contenir uniquement notre vue:

 
Sélectionnez

<?xml version="1.0" encoding="utf-8"?>
<!-Pour utiliser notre vue, il suffit de mettre le nom complet du paquetage de notre classe CompassView-->
<com.bydavy.boussole.view.CompassView
	xmlns:android="http://schemas.android.com/apk/res/android"	
	android:id="@+id/compassView"	
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"/>

Petite parenthèse : vous remarquerez que nous affichons notre vue avec la propriété xmlns :android="http://schemas.android.com/apk/res/android". Ceci vient du fait que notre Activity ne comportera que la vue de la boussole, cependant, nous pourrions très bien mettre notre vue dans un LinearLayout ou tout autre Layout.

Si nous exécutons le programme, nous devrions avoir une jolie boussole qui s'affiche sur l'écran. Pour le moment, l'aiguille du nord pointe stupidement vers le haut. Nous allons donc tenter de modifier l'orientation de l'aiguille du nord. Pour ce faire, nous allons changer le code de notre activity de la manière suivante :

Boussole.java
Sélectionnez

public class Boussole extends Activity {
	
	//La vue de notre boussole
	private CompassView compassView;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
	  //On récupère notre vue
        compassView = (CompassView)findViewById(R.id.compassView);
	  //Et on essaie de faire pointer notre aiguille du Nord au point à 45°
        compassView.setNorthOrientation(45);
    }
}
Boussole
Boussole pointant vers 45 °

On pourra remarquer que l'appel "compassView.setNorthOrientation(45)" a bien modifié l'orientation du nord signalé par notre vue. On peut donc conclure que notre classe BoussoleView.java répond bien aux spécifications que nous avions formulées. Nous pouvons maintenant la lier aux données de la boussole numérique.

IV. Mettre à jour la vue grâce à la boussole numérique

Une vue fonctionnelle c'est très bien, mais une vue qui montre des données utiles c'est encore mieux. Pour récupérer les informations de la boussole numérique, nous avons besoin de demander au SensorManager la liste des capteurs de type boussole dont il dispose. Celui-ci va nous retourner une liste de capteurs et nous allons garder uniquement le premier (tout appareil est muni d'une seule boussole numérique, mais le SDK nous impose ce comportement). Ensuite nous allons créer un SensorEventListener qui sera exécuté à chaque fois que notre boussole numérique connaîtra une nouvelle orientation du téléphone mobile. Pour finir, nous allons lier ce SensorEventListener à la boussole numérique. Voici de manière schématique comment les choses vont se passer :

Image non disponible

IV-A. Listener exécuté lorsque la boussole numérique possède une nouvelle orientation

Nous allons déclarer le listener :

Boussole.java
Sélectionnez

//Notre listener sur le capteur de la boussole numérique
private final SensorEventListener sensorListener = new SensorEventListener()         {
	@Override
	public void onSensorChanged(SensorEvent event) {
		compassView.setNorthOrientation(event.values[SensorManager.DATA_X]);
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
	}
};

IV-B. Lier le changement d'orientation de la boussole numérique à notre listener

Pour lier le changement d'orientation de la boussole numérique à notre listener, nous devons commencer par déclarer le gestionnaire de capteurs et un capteur qui sera la boussole numérique.

Boussole.java
Sélectionnez

//Le gestionnaire des capteurs
private SensorManager sensorManager;
//Notre capteur de la boussole numérique
private Sensor sensor;

Ensuite, nous allons demander au gestionnaire de capteurs les capteurs de type boussole dont il dispose.

Boussole.java
Sélectionnez

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    compassView = (CompassView)findViewById(R.id.compassView);
 	//Récupération du gestionnaire de capteurs
    sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
	//Demander au gestionnaire de capteur de nous retourner les capteurs de type boussole
    List<Sensor> sensors =sensorManager.getSensorList(Sensor.TYPE_ORIENTATION);
	//s'il y a plusieurs capteurs de ce type on garde uniquement le premier
    if (sensors.size() > 0) {
    	sensor = sensors.get(0);
    }
}

Pour finir, nous allons demander au gestionnaire de capteurs de lier notre SensorEventListener aux événements de la boussole numérique lorsque l'application s'affiche. Et inversement, nous allons lui demander de défaire ce lien lorsque l'on quitte l'application.

Boussole.java
Sélectionnez

@Override
protected void onResume(){
	super.onResume();
	//Lier les évènements de la boussole numérique au listener
	sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
}

@Override
protected void onStop(){
	super.onStop();
	//Retirer le lien entre le listener et les évènements de la boussole numérique
	sensorManager.unregisterListener(sensorListener);
}

Nous disposons maintenant d'une vue qui est mise à jour à chaque fois qu'une nouvelle orientation est connue par la boussole numérique.

V. Animation de la boussole

Notre application fonctionne parfaitement, mais on observe une certaine tendance à la téléportation de notre aiguille, notamment lorsqu'on fait brusquement changer le téléphone de direction. Ceci est dû au simple fait qu'on change l'orientation de l'aiguille brutalement. Ainsi, la boussole passe sans transition de l'ancienne valeur à la nouvelle. Pour remédier à ce problème, je propose qu'on fasse évoluer l'aiguille entre l'ancienne et la nouvelle orientation du nord de manière incrémentale et fluide.

V-A. La technique utilisée

Lorsqu'une nouvelle orientation sera fournie à notre vue grâce à la méthode SetNorthOrientation, nous sauvegarderons l'orientation courante et l'orientation fournie en paramètre, que nous appellerons respectivement startNorthOrientation et endNorthOrientation. Nous redessinerons ainsi la vue plusieurs fois en faisant varier NorthOrientation entre ces deux valeurs, de telle manière que NorthOrientation parte de startNorthOrientation et se rapproche de plus en plus de endNorthOrientation.

Pour redessiner périodiquement notre vue, nous devrons utiliser un Timer. Chaque exécution de la tâche du timer fera évoluer la position courante de l'aiguille et redessinera la vue de la boussole. Ce Timer sera appelé toutes les 20ms pendant 1 seconde. Ce qui signifie que l'animation durera 1 seconde et qu'au maximum il y aura 1 000/20 = 50 images par seconde pour donner l'impression d'une animation. Ne pouvant prédire exactement combien d'images par seconde seront affichées au cours de cette seconde d'animation, nous allons déterminer la position de l'aiguille à afficher en fonction du pourcentage d'évolution de notre animation. Pour produire de telles informations, nous utiliserons le temps écoulé depuis le début de l'animation.

Précédemment, j'ai dit que nous utiliserons un Timer pour dessiner périodiquement la vue. Cependant, le recours à un Timer implique par définition l'utilisation d'un nouveau thread. Un thread pour de l'animation est-ce bien raisonnable ? Développant pour téléphone portable, nous devons économiser les ressources à notre disposition or la création d'un thread pour une animation parait démesurée. Nous profiterons donc des fonctionnalités du SDK Android, et nous utiliserons un Handler. Le Handler est capable d'exécuter, en différé, un objet de type Runnable. Ce dernier sera exécuté dans le UI thread lorsque ce thread n'aura rien à faire.

Les vues sont déjà munis d'un Handler. Les appels aux méthodes post, postDelay et removeCallbacks du Handler seront donc accessible directement depuis notre classe CompassView.

V-B. Les modifications du code

Nous allons ajouter les attributs suivants à notre vue:

CompassView.java
Sélectionnez

//Délais entre chaque image
private final int DELAY = 20;
//Durée de l'animation
private final int DURATION = 1000;

private float startNorthOrientation;
private float endNorthOrientation;

//Heure de début de l'animation (ms)
private long startTime;

Et nous allons, également, changer la méthode setNorthOrientation de la manière suivante :

CompassView.java
Sélectionnez

// permet de changer l'orientation de la boussole
public void setNorthOrientation(float rotation) {

    // on met à jour l'orientation uniquement si elle a changé
    if (rotation != this.northOrientation) {
        //Arrêter l'ancienne animation
        removeCallbacks(animationTask);
    	//Position courante
        this.startNorthOrientation = this.northOrientation;
        //Position désirée
        this.endNorthOrientation   = rotation;
        //Nouvelle animation
        startTime = SystemClock.uptimeMillis();
        postDelayed(animationTask, DELAY);
    }
}

La méthode setNorthOrientation va donc sauvegarder la position courante, la nouvelle position désirée de l'aiguille, la date de début de l'animation (en ms) et va programmer une première exécution de la tâche animationTask du handler.

Voici à présent le code de la tâche chargée de l'animation :

CompassView.java
Sélectionnez

private Runnable animationTask = new Runnable() {
    public void run() {
        long curTime   = SystemClock.uptimeMillis();
        long totalTime = curTime - startTime;

        if (totalTime > DURATION) {
            // Fin de l'animation
            northOrientation = endNorthOrientation;
            removeCallbacks(animationTask);
        } else {
            //Changer la position de l'aiguille
            //Rappeler cette tâche dans DELAY ms pour dessiner la suite
            postDelayed(this, DELAY);
        }

        // Quoi qu'il arrive, on demande à notre vue de se redessiner
        invalidate();
    }
};

Vous remarquerez que dans cette tâche nous essayons de déterminer s'il y a plus d'une seconde écoulée depuis le début de l'animation. Si tel est le cas, l'animation devra être terminée, nous mettons alors l'aiguille du nord dans la position finale et nous arrêtons toutes les prochaines animations programmées s'il y en a (pas forcément utile).

Dans le cas où notre animation se déroule depuis moins d'une seconde, nous allons changer la position de l'aiguille, reprogrammer un appel à cette tâche dans DELAY ms pour continuer l'animation et enfin redessiner la vue.

Pour le moment, notre code ne fait pas encore évoluer la position de l'aiguille entre chaque appel à la tâche d'animation. Notre aiguille est seulement déplacée à sa position finale après 1 seconde. Nous allons maintenant mettre en place l'évolution de sa position. Admettons que notre aiguille soit à 5°, et que l'on désire la ramener à 13°. Lorsque notre tâche d'animation est appelée nous allons déterminer à quel pourcentage de l'animation nous nous trouvons :

CompassView.java
Sélectionnez

perCent = ((float) totalTime) / DURATION;

Et nous allons ainsi modifier la position de l'aiguille pour qu'elle soit à x perCent entre sa position initiale de 5° et sa position finale de 13°, ce qui nous donne :

CompassView.java
Sélectionnez

northOrientation = (float) (startNorthOrientation + perCent * (endNorthOrientation - startNorthOrientation));

Au final, nous arrivons donc au code suivant :

CompassView.java
Sélectionnez

if (totalTime > DURATION) {
    ?
} else {
    float perCent = ((float) totalTime) / DURATION;
    //On s'assure qu'on ne dépassera pas 1.
    perCent          = Math.min(perCent, 1);
    //On determine la nouvelle position de l'aiguille
    northOrientation = (float) (startNorthOrientation + perCent * (endNorthOrientation - startNorthOrientation));
    postDelayed(this, DELAY);
}
// Quoi qu'il arrive, on demande à notre vue de se redessiner
invalidate();

Le code étant complet, nous pouvons tester l'animation de notre boussole fraîchement créée. Vous remarquerez que l'aiguille ne se téléporte plus lors du soudain changement d'orientation. Cependant, l'aiguille évolue de manière bizarre, puisque qu'elle évolue toujours à la même vitesse pour aller de sa position de départ à sa position finale, peut importe qu'elle soit très loin du nord magnétique ou juste à côté. Pour le moment, le résultat est bien loin d'être réaliste.

V-C. Améliorer l'animation

Pour donner plus de crédibilité à notre animation nous allons essayer de trouver une fonction mathématique, non linéaire, qui évolue entre 0 et 1 (valeur de perCent). Cette fonction devra croître rapidement au voisinage de 0, puis réduire sa croissance à l'approche de 1. Pour cela, il semble intéressant d'utiliser la fonction sinus qui a le comportement recherché. Nous devrons juste modifier son amplitude pour s'assurer que pour la valeur 1 (de notre perCent) la fonction nous retourne bien 1, c'est-à-dire qu'à 100% de notre animation nous affichons bien la fin de l'animation. Ainsi, la fonction retenue sera sinus(x * 1.5).

Image non disponible
Sinus(x*1.5)

Valeurs prises entre 0 et 1 :

X 0 0.2 0.4 0.6 0.8 1
Y 0 0.29 0.56 0.78 0.93 1

Nous avons donc une forte accélération entre 0 et 0,6 ce qui permet de faire évoluer rapidement notre aiguille vers la position finale en début d'animation, alors qu'en fin d'animation, sa vitesse de déplacement va fortement s'altérer jusqu'à arriver à destination, le nord.

Nous pouvons donc remplacer notre portion de code par ceci :

CompassView.java
Sélectionnez

if (totalTime > DURATION) {
    ?
} else {
    float perCent = ((float) totalTime) / DURATION;

    // Animation plus réaliste de l'aiguille
    perCent          = (float) Math.sin(perCent * 1.5);
    perCent          = Math.min(perCent, 1);
    northOrientation = (float) (startNorthOrientation + perCent * (endNorthOrientation - startNorthOrientation));
    postDelayed(this, DELAY);
}
// Quoi qu'il arrive, on demande à notre vue de se redessiner
invalidate();

V-D. Forcer le sens de rotation de l'aiguille

Nous voici donc en présence d'une jolie boussole dont le comportement est proche de celui d'une vraie boussole, cependant, notre animation possède toujours un "bug", ou plutôt une réaction étrange. Par exemple, lorsqu'elle passe de l'orientation 5° à 350° celle-ci décide d'emprunter le chemin suivant :

Image non disponible
Rotation non réaliste

Il va nous faudra donc forcer le sens d'évolution de notre aiguille, en augmentant par exemple, de manière virtuelle, l'orientation de départ ou d'arrivée de 360°. Si nous reprenons le cas de l'exemple précédent, nous augmenterons de 360° la position de départ qui devient 365°. Ainsi notre aiguille, évoluant entre 365° et 350°, empruntera le chemin le plus court, qui se trouve également être le plus réaliste.

Voici les modifications apportées à la méthode SetNorthOrientation :

CompassView.java
Sélectionnez

// permet de changer l'orientation de la boussole
public void setNorthOrientation(float rotation) {

    // on met à jour l'orientation uniquement si elle a changé
    if (rotation != this.northOrientation) {
        //Arrêter l'ancienne animation
        removeCallbacks(animationTask);
        
    	//Position courante
        this.startNorthOrientation = this.northOrientation;
        //Position désirée
        this.endNorthOrientation   = rotation;

        //Détermination du sens de rotation de l'aiguille
        if ( ((startNorthOrientation + 180) % 360) > endNorthOrientation)
        {
        	//Rotation vers la gauche
        	if ( (startNorthOrientation - endNorthOrientation) > 180 )
        	{
        		endNorthOrientation+=360;
        	}
        } else {
        	//Rotation vers la droite
        	if ( (endNorthOrientation - startNorthOrientation) > 180 )
        	{
        		startNorthOrientation+=360;
        	}
        }
        
        //Nouvelle animation
        startTime = SystemClock.uptimeMillis();
        postDelayed(animationTask, DELAY);
    }
}

A présent, nous allons simplifier la valeur de l'orientation lorsque l'on atteint la position finale, tout simplement car l'astuce utilisée juste ci-dessus génère des valeurs supérieures à 360°, c'est-à-dire, plus d'un tour de cadran, ce qui est quelque peu gênant.

CompassView.java
Sélectionnez

private Runnable animationTask = new Runnable() {
    public void run() {
        ?
        if (totalTime > DURATION) {
            northOrientation = endNorthOrientation % 360;
            removeCallbacks(animationTask);
        } else {
            ?
        }
        ?
    }
};

V-E. Critique

Je tiens à apporter une critique à l'animation que nous avons produite. Le résultat est satisfaisant d'autant plus s'il s'agit de vos premiers pas, mais il reste améliorable. Pour comprendre pourquoi l'animation est perfectible, je tiens à revenir au fonctionnement de la boussole numérique et plus particulièrement au listener qui est lié au changement d'orientation. Lorsque vous tournez votre téléphone mobile, ce dernier ne change pas brusquement d'orientation mais détecte plusieurs états intermédiaires. Par conséquent, notre programme se voit délivrer x fois par seconde une nouvelle orientation. Vous comprendrez donc que, dans ces conditions, notre animation s'étalant sur une seconde, elle soit prise de court et ne peut se terminer complètement. Visuellement cela n'a que peu d'importance car la fin de l'animation est provoquée par l'arrivée d'une nouvelle orientation qui induit le début d'une nouvelle animation.

Une approche envisageable serait de ne plus faire évoluer l'orientation de notre aiguille entre une position de départ et celle d'arrivée, mais plutôt d'attribuer une vitesse de rotation à notre aiguille en fonction de sa distance par rapport au nord. Le résultat sera plus facilement prévisible et contrôlable, ainsi nous serons en adéquation avec la réaction réelle d'une boussole.

VI. Conclusion

La création d'une vue sous Android est assez simple. Il suffit de redéfinir les méthodes onDraw et onMeasure. L'alimentation de celle-ci grâce aux données issues des capteurs est possible grâce aux SensorEventListener. L'animation par contre à recours à des handlers qui déterminent le pourcentage d'évolution de l'animation et redessinent la vue.

En guise de conclusion à ce tutoriel, je vais revenir sur le choix de la boussole. Le résultat est une boussole réagissant en fonction de l'orientation de votre téléphone portable à la manière d'une boussole réelle. Mise à part le résultat assez plaisant et amusant, le choix de cette réalisation ne fut qu'un prétexte permettant de vous présenter la création et l'animation de vues. J'espère tout simplement avoir rempli ma mission.

VII. Remerciements

Je tiens à remercier les membres de l'équipe de Developpez.com, pixelomilcouleurs, jacques_jean, Kévin F. et Thomas G. pour leur aide à la relecture de ce tutoriel.

VIII. Liens

Voici les liens pour télécharger le code source et le package Android :


Android :
Création d'un carrousel
Création d'une boussole
Les threads composants une application
L'UI thread
  

Copyright © 2010 Davy Leggieri. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.