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 base 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▲
À 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 base 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 :
II-B. Réalisation▲
Tous les éléments graphiques de base 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 :
public
class
CompassView extends
View {
//~--- fields -------------------------------------------------------------
//Rotation vers la droite en degrés 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édent
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 cœur 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.
//~--- 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.
// 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, quatre 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.
//~--- 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.
<?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).
// 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ée 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'aiguille 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.
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:
<
?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 :
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
);
}
}
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 :
IV-A. Listener exécuté lorsque la boussole numérique possède une nouvelle orientation▲
Nous allons déclarer le listener :
//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.
//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.
/** 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.
@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 200 ms pendant une seconde. Ce qui signifie que l'animation durera une seconde et qu'au maximum il y aura 1000/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à munies d'un Handler. Les appels aux méthodes post, postDelay et removeCallbacks du Handler seront donc accessibles directement depuis notre classe CompassView.
V-B. Les modifications du code▲
Nous allons ajouter les attributs suivants à notre vue :
//Délai 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 :
// 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 :
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 une 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 :
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 :
northOrientation =
(
float
) (
startNorthOrientation +
perCent *
(
endNorthOrientation -
startNorthOrientation));
Finalement, nous arrivons donc au code suivant :
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 détermine 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, puisqu'elle évolue toujours à la même vitesse pour aller de sa position de départ à sa position finale, peu 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).
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 :
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 :
Il 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 :
// 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);
}
}
À 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.
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 a 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. Mis à 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 :