TwinXeon by Renaudet
Application graphique Android

Table des matières

Introduction

J'ai entendu parler d'Android pour la première fois lorsque je me suis intéressé à la dernière génération des baladeurs Archos (8ème génération). En effectuant quelques recherches rapides, je me suis rendu compte qu'Android proposait un modèle de développement full Java, mon langage de prédilection.
Je me suis donc acheté une tablette internet multimédia Archos 70 IT 8Go avec la ferme intention de me mettre au développement d'applications mobiles.

Aussitôt dit (presque) aussitôt fait. Je me suis donc retrouvé avec entre les mains un magnifique joujou de 250 €, équipé d'un processeur cadencé à 1 GHz, d'un écran capacitif d'une diagonale de 7" et d'une définition de 800x480 pixels en million de couleurs.
Première chose à faire : se rendre sur le site dédié au développement Android pour y glaner quelques informations sur l'art et la manière de créer les fameuses APKs, nom donné aux applications Android du fait de l'extension du nom de fichier.
Deuxième chose à faire, installer et configurer l'environnement de développement :

- Eclipse Galiléo 3.5 (environnement de développement Java Open-Source) à télécharger ici
- Android SDK 2.2 (Froyo) à télécharger ici
- Android plugin pour Eclipse IDE à télécharger ici

Le plugin pour Eclipse est très bien intégré et nécessite très peu de configuration, sinon le faire pointer sur le répertoire d'installation du SDK. Il faut ensuite lancer l'interface du Android SDK and AVD Manager pour créer et configurer au moins un device virtuel (émulateur).
Android SDK
Le SDK Android pour Windows
Comme je cible exclusivement ma tablette Archos, j'ai défini un device doté d'un écran tactile de 800x480 pixels et d'une carte SD de 128 Mo, ce qui sera suffisant puisque l'émulateur ne fera tourner que mes applications.
J'ai égalements défini d'autres périphériques comme l'inclinomètre ou la webcam, mais en fait l'émulateur est incapable de les utiliser. Ils ne seront donc là que pour s'assurer que les APIs qui les invoquent ne planteront pas.
Android device configuration
Configuration d'un device
La copie d'écran ci-dessous est une vue de l'émulateur lancé depuis le SDK and AVD Manager Android :
Android 2.2 virtual device
L'émulateur Android 2.2
Depuis l'interface Eclipse, il faut encore définir une Runtime Configuration que j'appelle Archos 70 IT. En fait, une telle configuration de lancement sera automatiquement créée lors du premier test de notre application graphique.
Android Launch Configuration
La configuration de lancement de l'émulateur sous Eclipse

Nous voici fin prêt pour créer un projet d'application Android.

Création du projet d'application Android

Depuis l'explorateur de projets Eclipse, sélectionnons via le menu popup New / Project... puis, dans l'assistant de création de projet Android / Android Project :
Android Project Wizard
L'assistant de création de projet Android
L'assistant de création de projet d'application Android s'ouvre alors. Je créé une application My Graphic's dans le projet My First Graphic Application. L'assistant me permet également de spécifier certaines valeurs propres à l'application comme la racine principale de l'espace de nommage Java (package) et le nom de l'activité principale de cette application. A ce stade, je peux cliquer sur Finish. Eclipse me créé un nouveau projet de développement Java dans le workspace, avec toutes les informations nécessaires pour en faire un projet de développement d'application Android.
New project properties
Configuration des propriétés du nouveau projet
On peut voir ci-dessous la structure, assez complexe, du projet Eclipse. On y retrouve le classique dossier des sources Java /src, ainsi qu'un dossier de sources générés /gen. On y trouve également une structure de dossiers appelée /res et contenant des fichier XML important, comme la déclaration de la structure du layout de l'application ou la déclaration de ressources texte statiques.
Workspace structure
Structure du projet dans le Workspace

C'est parti pour le développement!

La première chose à faire, c'est de créer une instance d'Activity. L'assistant de création de projet a normalement créé pour vous cette classe si vous avez coché la case ad-hoc.
Dans mon cas, il s'agit de la classe com.nrt.android.tst.graphics.MyGraphicsActivity.

Il faut ensuite indiquer à Android ce qu'il doit afficher, et ça, c'est du ressort d'une instance de View. Nous verrons prochainement cette classe qui, dans notre exemple s'appellera com.nrt.android.tst.graphics.MyFirstGraphicView. Pour le moment, nous associons cette vue à notre activité par un appel à setContentView() :
package com.nrt.android.tst.graphics;

import android.app.Activity;
import android.os.Bundle;

public class MyGraphicsActivity extends Activity {
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(new MyFirstGraphicView(this));
   }
}
L'instance de View maintenant.

Je la fais hériter de SurfaceView et implémenter SurfaceHolder.Callback afin de surcharger les gestionnaires d'évènements surfaceChanged, surfaceCreated et surfaceDestroyed qui nous serviront à lancer le timer de raffraichissement.
Ce gestionnaire d'évènements est installé par un appel à getHolder().addCallback(this) depuis le constructeur.

La méthode onDraw parcours une liste d'objets GraphicObject maintenue par l'application et leur délègue la gestion de leur rendu graphique (méthode GraphicObject#onPaint()) en leur passant le context (un objet Canvas). Nos objets graphiques sont ici de simples rectangles dessinés en couleurs semi-transparentes selectionnées au hasard(Paint#setAlpha()).

Le gestionnaire onTouchEvent nous permet de réagir aux interactions utilisateur. J'ai implémenté un automate à états qui réalise à peut près ceci:
(état initial)
 Lorsque l'utilisateur touche l'écran, il définit les coordonnées initiales de l'un des coin du rectangle
 Lorsque l'utilisateur déplace son doigt sur l'écran (sans l'avoir relevé), il fait varier la taille finale du rectangle. Le rectangle est entouré d'un cadre blanc et le coin du rectangle situé sous le doigt utilisateur est entouré d'un cercle rouge.
 Lorsque l'utilisateur relève son doigt, la taille finale du rectangle est fixée et le cadre blanc disparaît. Une taille minimum est nécessaire pour prévenir les créations d'objets graphiques par inadvertance

Variante :
(état initial)
 Lorsque l'utilisateur touche l'écran, on parcours la liste des objets graphiques pour en trouver un dont les coordonnées du coin inférieur droit ou du centre sont situées dans un rayon de 20 ou 50 pixels autour du point de sélection (suivant le cas). Si un objet graphique est trouvé répondant à l'un de ces critère, il passe en mode sélection
 En mode sélection, si l'utilisateur déplace son doigt sur l'écran, l'objet sélectionné est soit déplacé (sélection de son centre), soit modifié (taille).
 Lorsque l'utilisateur relève son doigt, la sélection courante est annulée
package com.nrt.android.tst.graphics;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MyFirstGraphicView extends SurfaceView implements SurfaceHolder.Callback {
   public final static int MOVE_MODE = 0;
   public final static int RESIZE_MODE = 1;
   public final static int CREATE_MODE = 2;
   public final static int NORMAL_MODE = 3;

   private GraphicThread graphicThread;
   private ArrayList objects = new ArrayList(20);
   private Paint paint;
   private GraphicObject currentObject;

   private class GraphicObject{
      float x,y,width,height;
      int color = Color.GREEN;
      int updateMode = CREATE_MODE;
      public GraphicObject(float x,float y) {
         this.x = x;
         this.y = y;
         width = x;
         height = y;
         color = color((int)Math.round(255*Math.random()),
                  (int)Math.round(255*Math.random()),
                  (int)Math.round(255*Math.random()));
      }
      public void setUpdateMode(int updateMode) {
         this.updateMode = updateMode;
      }
      public void setLimites(float a,float b) {
         if(RESIZE_MODE==updateMode ||
          CREATE_MODE==updateMode){
            width = a;
            height = b;
         }
         if(MOVE_MODE==updateMode){
            float x0 = (x+width)/2;
            float y0 = (y+height)/2;
            float dx = a-x0;
            float dy = b-y0;
            x+=dx;
            y+=dy;
            width+=dx;
            height+=dy;
         }
      }
      public void onPaint(Canvas canvas) {
         if(updateMode!=NORMAL_MODE){
            paint.setStyle(Style.STROKE);
            paint.setColor(Color.LTGRAY);
            canvas.drawRect(x-2, y-2, width+2, height+2, paint);
            if(RESIZE_MODE==updateMode){
               paint.setColor(Color.RED);
               //canvas.drawRect(width-10, height-10, width+10, height+10, paint);
               canvas.drawCircle(width, height, 10, paint);
            }
            if(MOVE_MODE==updateMode){
               paint.setColor(Color.RED);
               float x0 = (x+width)/2;
               float y0 = (y+height)/2;
               canvas.drawCircle(x0, y0, 10, paint);
            }
         }
         paint.setStyle(Style.FILL);
         paint.setColor(color);
         if(MOVE_MODE==updateMode){
            paint.setAlpha(128);
         }else{
            paint.setAlpha(190);
         }
         canvas.drawRect(x, y, width, height, paint);
      }
      public boolean getSelected(float a,float b){
         if(Math.abs(width-a)<=20 && Math.abs(height-b)<=20){
            setUpdateMode(RESIZE_MODE);
            return true;
         }
         if(Math.abs(((x+width)/2)-a)<=50 && Math.abs(((y+height)/2)-b)<=50){
            setUpdateMode(MOVE_MODE);
            return true;
         }
         return false;
      }
   }

       public MyFirstGraphicView(Context context) {
      super(context);
      getHolder().addCallback(this);
      graphicThread = new GraphicThread(getHolder(), this);
      paint = new Paint();
      paint.setStyle(Style.STROKE);
   }

   @Override
   public boolean onTouchEvent(MotionEvent event) {
      float x = event.getX();
      float y = event.getY();
      if(event.getAction()==MotionEvent.ACTION_DOWN){
         for(GraphicObject g: objects){
            if(g.getSelected(x, y)){
               currentObject = g;
            }
         }
         if(currentObject==null){
            currentObject = new GraphicObject(x, y);
            objects.add(currentObject);
         }else{
            currentObject.setLimites(x, y);
         }
      }else
         if(event.getAction()==MotionEvent.ACTION_MOVE && currentObject!=null){
            currentObject.setLimites(x, y);
         }else{
            if(event.getAction()==MotionEvent.ACTION_UP){
               currentObject.setUpdateMode(NORMAL_MODE);
               currentObject = null;
            }
         }
      return true;
   }

       @Override
   protected void onDraw(Canvas canvas) {
      Paint paint = new Paint();
      paint.setColor(Color.BLACK);
      paint.setStyle(Style.FILL);
      canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
      for(GraphicObject g: objects){
         g.onPaint(canvas);
      }
   }

   private int color(int r,int g,int b){
      return Color.argb(190, r, g, b);
   }

   @Override
   public void surfaceChanged(SurfaceHolder holder, int format, int width,
         int height) {
   }

   @Override
   public void surfaceCreated(SurfaceHolder holder) {
      graphicThread.setRunning(true);
      graphicThread.start();
   }

   @Override
   public void surfaceDestroyed(SurfaceHolder holder) {
      boolean retry = true;
      graphicThread.setRunning(false);
      while (retry) {
       try {
          graphicThread.join();
         retry = false;
       } catch (InterruptedException e) {
       }
      }
   }
}
Le gestionnaire de raffraîchissement est essentiellement une boucle sans fin qui vérouille l'instance de Canvas, invoque un raffraîchissement par invocation de la méthode onDraw() sur la vue, puis déverouille le canvas.

J'ai repris ce code d'un tutoriel du site allemand Android Development à cette adresse, mais je ne suis pas convaincu de l'utilité dun Thread tiers pour faire cela...

Je regarderai cela à tête reposée une autre fois, mais je pense que l'on peut (et l'on doit) faire mieux pour plus de fluidité.
package com.nrt.android.tst.graphics;

import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class GraphicThread extends Thread {
   SurfaceHolder surfaceHolder;
   MyFirstGraphicView panel;
   boolean isRunning;
   int waitTime = 100;

   public GraphicThread(SurfaceHolder surfaceHolder, MyFirstGraphicView panel) {
      this.surfaceHolder = surfaceHolder;
      this.panel = panel;
   }

   public void setRunning(boolean run){
      isRunning = run;
   }

   @Override
   public void run() {
      Canvas c;
      while (isRunning) {
         try {
            Thread.sleep(waitTime);
         } catch (InterruptedException e) {}
         c = null;
         try {
            c = surfaceHolder.lockCanvas(null);
            synchronized (surfaceHolder) {
               panel.onDraw(c);
            }
         } finally {
            // do this in a finally so that if an exception is thrown
            // during the above, we don't leave the Surface in an
            // inconsistent state
            if (c != null) {
               surfaceHolder.unlockCanvasAndPost(c);
            }
         }
      }
   }
}
Il ne reste plus qu'à tester notre application en lançant l'émulateur. Une fois le Device virtuel lancé et déverouillé, notre application se lance automatiquement (on peut la relancer via le menu Android).

Premier constat : l'émulateur du SDK 2.2 est buggué et limite notre application à une fenêtre verticale centrée sur l'écran de 480x800 !. Vraiment dommage.

Deuxième constat : le capacitif, c'est peut-être plus fluide et réactif, mais c'est aussi beaucoup moins précis qu'un écran résistif (d'où les tailles relativement importantes des zones de sélection dans le code ci-dessus. En moyenne, une cercle de 30 pixels centré sur la zone réactive souhaitée est un bon choix). Je ne m'en suis pas apperçu sous l'émulateur, mais bien quand j'ai exporté et installé l'application sur mon A 70 IT
MyGraphics application
L'application My Graphics dans l'émulateur

Installation de l'application sur l'Archos

Avant d'installer notre application sur l'Archos 70 IT, il faut l'exporter au format APK Android.

Pour cela, on sélectionne le projet d'application Eclipse dans l'explorateur, et on utilise le menu export. L'assistant d'export fait construire un coffre de clés, ce qui fait penser que les fichiers APK sont signés numériquement, de manière à valider les mises à jour (et donc limiter la sensibilité aux virus).
Une fois la procédure d'export déroulée jusqu'au bout, on se retrouve avec un fichier d'extension *.apk que l'on peut recopier sur une carte SD ou via connection USB sur le disque interne de l'Archos. Il suffira de sélectionner ce fichier depuis l'application Fichiers pour en déclencher l'installation.
selection de l'APK
Sélection du fichier APK depuis un répertoire partagé (Samba)
installation
Installation de l'application
MyGraphics application on Archos
L'application My Graphics sur l'Archos 70 IT
Les sources de cette application sont disponibles dans la section téléchargement en suivant ce lien.

P.S.: l'application plante aléatoirement à l'utilisation. Plus fréquemment sur l'Archos que sous l'émulateur. Je pense que cela est du à la classe GraphicThread.

Je corrigerai ça, un jour...

(Vous avez aimé cet article, vous souhaitez faire un commentaire, merci de m'en faire part en remplissant ce formulaire)

Site optimisé pour un affichage en 800x600 sous Firefox 8.x - ©Copyright 2011-2012 by Nicolas Renaudet