Le SDK Android est fournit avec un outil merveilleux : l’émulateur! En plus de permettre à beaucoup de développeurs de tester leurs applications de façon très proche de la réalité (voir note ci-dessous) sans investir dans un vrai terminal, il offre la possibilité de tester vos programmes sur plusieurs tailles et orientation de terminaux. Par défaut, l’émulateur est livré avec des skins représentant un terminal QVGA en mode landscape et portrait. QVGA est en fait une abréviation signifiant “Quarter Video Graphics Array” et désigne une norme d’affichage dont la définition est de 320×240 pixels. La skin par défaut de l’émulateur est celle d’un terminal HVGA rouge. Le format HVGA désigne quant-à-lui une résolution de 480×320 pixels. Cette norme n’est pas sans rappeller des terminaux HTC bien connu tel que le Dream ou le Hagic :D.

Note : Un émulateur est différent d’un simulateur tout simplement par son mode de fonctionnement. Prenons l’émulateur Android, le démarrer revient à lancer tout simplement Android en traduisant les instructions ARM du système mobile en instructions de votre système (Intel). Mon expérience montre que les performances de l’émulateur et de la machine sont presques similaires. J’aurais même tendance à dire que le terminal réel tourne mieux que l’émulateur (notamment pour l’OpenGL puisque le rendu sur terminal est fait par l’accélération graphique - hardware alors que l’émulateur effectue ces opérations en software). Un simulateur, comme celui de l’iPhone par exemple, est en fait simplement un système compilé directement en utilisant les instructions de la machine de destination. Ainsi le simulateur de l’iPhone tourne vraiment très rapidement car les applications sont compilées en instructions Intel et directement interprétées par le système Mac OS X.

2009 est présenti comme “l’année d’Android”. On annonce par exemple près de 7 terminaux comme le montre cette étude. En plus des grands constructeurs de téléphones comme Samsung, HTC ou LG, d’autres entreprises (moins connues dans le monde du mobile) commencent à faire parler d’elles. En effet, certains constructeurs d’ordinateurs viennent d’annoncer la sortie de netbooks tournant sous Android.

Souhaitant voir comment “tourne” Android avec une résolution supérieure j’ai voulu créer une skin permettant de modéliser un écran supérieur à du HVGA. Ma première recherche sur la création de skins, bien que rapide, n’a pas vraiment été fructeuse puisque je n’ai rien trouver d’intéressant. Plutôt que de perdre du temps, j’ai donc préféré faire du reverse-engineering sur les skins fournies avec l’émulateur. Pour tout avouer, le reverse-engineering de la technologie n’a pas été bien difficile et seulement 2 minutes ont été nécessaires pour bien comprendre le système.

Une skin est un dossier à placer dans votre_sdkplatformsversion_androidskins dans lequel vous devez retrouver :

  • Un fichier “layout” (obligatoire) définissant le positionnement des différents éléments de votre terminal
  • Une image de fond (pas obligatoire)
  • Des images pour les “hovers” c’est à dire des images faisant office de calques lorsque votre souris passe sur un bouton

Pour expliquer le fonctionnement de ces skins, j’ai créé une skin d’un terminal VGA (640×480). Pour faire simple, nous pouvons tout simplement faire en sorte que l’émulateur affiche simplement l’écran du terminal sans aucun surplus (pas de touches, pas de coque, pas de clavier, etc.). Après avoir créer un fichier layout, utilisons l’élément display :

display {
	width  640
	height 480
	x 0
	y 0
}

On souhaite maintenant ajouter une image de fond. Il suffit donc de créer cette image de fond (que j’ai appelée background.png) et d’ajouter l’élément background :

background {
	image background.png
	x 0
	y 0
}

Pour finir, il est intéressant de faire en sorte que les boutons “changent” d’aspect lorsqu’ils sont survolés par la souris et surtout qu’ils effectuent une action lorsqu’on clique dessus. Il suffit alors d’ajouter l’élément button :

button {
 
  soft-left {
    image button_menu.png
    x 290
    y 491
  }
	home {
		image button.png
		x 199
		y 551
	}
	back {
		image button.png
		x 397
		y 551
	}
	dpad-up {
		image arrow_up.png
		x 272
		y 524
	}
	dpad-down {
		image arrow_down.png
		x 274
		y 579
	}
	dpad-left {
		image arrow_left.png
		x 266
		y 532
	}
	dpad-right {
		image arrow_right.png
		x 338
		y 531
	}
	dpad-center {
		image button_center.png
		x 300
		y 552
	}
	phone-dial {
		image button.png
		x 131
		y 551
	}
	phone-hangup {
		image button.png
		x 465
		y 551
	}
}

Nous venons donc de créer une skin d’un terminal Android VGA comme représenté ci-dessous. Cette skin est téléchargeable dans le zip suivant (inclut le .psd pour retoucher la skin sous photoshop).

Pour conclure, j’aimerai préciser que les différents points abordés sont assez basiques. Il est néanmoins possible de créer des skins un peu plus avancées à l’instar de la skin par défaut du terminal HVGA. On peut par exemple inclure les propriétés du réseau (élément network), un clavier virtuel, etc…

Note : Il est probable que vous rencontriez des problèmes d’affichage avec Android en résolution VGA. Je n’aime pas critiquer une technologie sans avoir rechercher d’où vient réellement le problème. Je ne m’avance donc pas sur ce qui va suivre : il semblerait que la version Cupcake d’Android ne soit pas encore tout à fait prête pour les grandes résolutions. On remarquera par exemple un bug sur les 9-patchs sur le SlidingDrawer du HomeScreen en mode portrait. D’un point de vue global, l’équipe Android a tout de même merveilleusement anticipé les différentes résolutions grâce aux layouts. Il reste néanmoins encore des petits problèmes en ce qui concerne la densité d’écran mais je suis certain que ces “problèmes” seront rapidement résolus! (à en voir les Google I/O c’est le cas ;))

Le framework Android fournit, de base, un ensemble de composants graphiques appelés widgets. Ces widgets, à l’aide de l’éditeur de layout intégré à Eclipse, permettent de créer des interfaces graphiques facilement et sans trop perdre de temps. On retrouve dans la bibliothèque d’Android (android.widget) des composants simples tels que Button, TextView ou ImageView mais également d’autres widgets considérés comme plus sophistiqués : Gallery, SlidingDrawer, DatePicker, etc.

Le développement sur Android peut parfois nécessiter l’utilisation de composants non disponibles, de base, dans android.widget. Imaginons par exemple que vous souhaitiez définir un widget de type barre de progression circulaire. Le composant ProgressBar ne permet pas d’afficher une progression de façon circulaire. Il n’existe aucun moyen d’afficher ce genre de composants et il est donc nécessaire soit d’en trouver une implémentation libre sur le web soit de le créer vous-même. L’objectif de ce tutorial est de s’initier à la création d’une View personnalisée (custom view en anglais).

Note : La définition d’une vue personnalisée aborde un ensemble de points techniques. Expliquer de façon exhaustive l’ensemble de ces points nécessite plusieurs paragraphes de description. Pour éviter de surcharger et donc de rendre illisible cette partie, j’ai préféré séparer la création de la vue en 2 parties. Cette première partie se concentrera sur la création de la vue (par le code uniquement), la gestion des évènements tactiles et du “dessin” de la vue. La seconde partie s’attachera à optimiser et surtout rendre générique (et donc réutilisable) la vue. On pourra par exemple instancier la vue via XML. Au vu de cette séparation, il est donc important de conserver à l’esprit que cette première partie n’est pas “finie”. Ne prenez pas cette partie pour modèle exact mais attendez plutôt la version finale de notre vue.

L’objectif de cette partie sera de créer une vue nommée TrashView. Cette dernière affichera simplement une poubelle et un fichier. Le fichier pourra être “draggué” dans la poubelle qui passera alors de l’état vide à l’état plein. Une première capture d’écran est donnée ci-dessous. Il est également possible de télécharger les sources de l’application grâce à ce zip

Comme mentionné ci-dessus, nous souhaitons créer une vue personnalisée instanciable par le code uniquement, gérant le dessin et les évènements tactiles. La gestion de ces possibilités se fait en créant une classe héritant de android.view.View puis en redéfinissant void onDraw(Canvas canvas) (gestion du dessin de la vue) et boolean onTouchEvent(MotionEvent event) (gestion des évènements tactiles). Le squelette ci-dessous montre la forme globale de notre vue personnalisée et inclut le constructeur par le code TrashView(Context context) :

public class TrashView extends View {
 
    public TrashView(Context context) {
        super(context);
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}

Notre vue nécessite, bien évidemment, plusieurs variables définies en private car elles ne reflètent aucunement l’état de la vue et permette de conserver l’encapsulation des données. Notez que les variables LOG_ENABLED et LOG_TAG sont déclarées dans une optique de débuguage optimisé et sont utilisées dans la méthode static void log(String log). Pour mieux comprendre cette technique de débuguage, je vous conseille de lire cet article rédigé par mes soins et détaillant l’astuce utilisée.

/**
 * Variable pour le débugguage
 */
private static final boolean LOG_ENABLED = true;
private static final String LOG_TAG = "TrashView";
private final int ICON_SIZE = 100;
 
/**
 * Seuil au delà duquel le fichier est considéré comme jeté à la poubelle.
 */
private static final float SURFACE_THRESHOLD = 0.6f;
 
/**
 * Bitmaps contenant les différentes images utilisées dans la vue
 */
private Bitmap mEmptyTrash;
private Bitmap mFullTrash;
private Bitmap mTextFile;
 
/**
 * Contient l'état de la poubelle : pleine ou vide
 */
private boolean mIsTrashFull;
 
/**
 * Taille des icones
 */
private int mIconsSize;
 
/**
 * Coordonnées de départ du fichier texte
 */
private float mFileStartX;
private float mFileStartY;
 
/**
 * Coordonnées précédente du fichier, actuelle du fichier et de la poubelle.
 */
private float mPreviousFileX;
private float mPreviousFileY;
private float mFileX;
private float mFileY;
private float mTrashX;
private float mTrashY;
 
/**
 * Différence sur X et Y entre le premier point appuyé et la position de l'image
 * représentant le fichier.
 */
private float mDeltaX;
private float mDeltaY;
 
private static void log(String log) {
    if (LOG_ENABLED) {
        Log.d(LOG_TAG, log);
    }
}

Les variables sont maintenant prêtes et nous pouvons donc créer le constructeur de notre vue. La vue est instanciable, pour l’instant, via le code uniquement. Nous implémentons donc le constructeur de la forme public View(Context context) créant les différentes Bitmap utilisées dans la vue :

public TrashView(Context context) {
    super(context);
    mIconsSize = ICON_SIZE;
    mEmptyTrash = prepareBitmap(getResources().getDrawable(R.drawable.empty_trash), mIconsSize,
            mIconsSize);
    mFullTrash = prepareBitmap(getResources().getDrawable(R.drawable.full_trash), mIconsSize,
            mIconsSize);
    mTextFile = prepareBitmap(getResources().getDrawable(R.drawable.txt_file), mIconsSize,
            mIconsSize);
    resetTrashView();
}
 
public void resetTrashView() {
    mIsTrashFull = false;
    mFileStartX = 10;
    mFileStartY = 20;
    mFileX = mFileStartX;
    mFileY = mFileStartY;
    mTrashX = 200;
    mTrashY = 20;
}
 
private static Bitmap prepareBitmap(Drawable drawable, int width, int height) {
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    drawable.setBounds(0, 0, width, height);
    Canvas canvas = new Canvas(bitmap);
    drawable.draw(canvas);
    return bitmap;
}

Ce code n’a rien de compliqué mais la méthode static Bitmap prepareBitmap(Drawable drawable, int width, int height) peut paraitre obscure pour les débutants. Android permet de récupérer facilement des ressources graphiques présentes dans le dossier res/drawable via le fichier R.java et sous forme d’un objet de type Drawable. Ce type, extrêmement utile puisqu’il généralise totalement la notion d’objets “dessinables” dispose en contre partie d’inconvénients : son utilisation dans un Canvas (surface sur laquelle nous dessinons notre vue) est un peu particulière et son affichage est assez lent (utilisation d’une interface et redimensionnent à la volée de l’image). Pour optimiser le dessin, il convient de déclarer des objets de type Bitmap qui sont plus “naturels” à dessiner sur un Canvas et sont également plus rapide à afficher puisque leur taille est définie de façon statique à l’initialisation. Cette méthode consiste donc simplement à mettre le contenu de drawable à la taille (width, height) dans un objet Bitmap qui sera retourné comme résultat de la méthode statique.

Modifions maintenant notre vue afin qu’elle puisse afficher les images. On corrige donc la méthode onDraw(Canvas canvas) qui est appelée par le système à la suite d’un invalidate() (méthode informant le système que la vue est “sale” et nécessite d’être mise à jour). Le contenu de cette méthode est très succinct et se passe, à mon avis, de commentaires puisqu’elle ne consiste qu’à afficher les différentes images représentée dans la vue suivant l’état actuel de la poubelle :

@Override
protected void onDraw(Canvas canvas) {
    if (mIsTrashFull) {
        canvas.drawBitmap(mFullTrash, mTrashX, mTrashY, null);
    } else {
        canvas.drawBitmap(mEmptyTrash, mTrashX, mTrashY, null);
        canvas.drawBitmap(mTextFile, mFileX, mFileY, null);
    }
}

L’objectif de cette partie est maintenant de gérer les évenements tactiles afin de faire en sorte que l’utilisateur puisse glisser le fichier sur la corbeille. Cette partie est probablement la partie la plus technique de ce tutorial. C’est probablement la raison pour laquelle il y a de nombreux points à détailler. J’ai donc préféré détailler les points techniques en commentant directement le code.

@Override
public boolean onTouchEvent(MotionEvent event) {
 
    /*
     * Récupère l'action effectuée et sa position
     */
    final int action = event.getAction();
    final float x = event.getX();
    final float y = event.getY();
 
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            /*
             * L'utilisateur vient d'appuyer sur la vue. Si l'appui
             * s'effectue sur le fichier et que la poubelles est vide on
             * peut démarrer le "dragging" en initialisant les variables
             * utiles par la suite
             */
            if (!mIsTrashFull && x >= mFileX && x <= mFileX + mIconsSize && y >= mFileY
                    && y <= mFileY + mIconsSize) {
                log("Start moving text file");
                mDeltaX = x - mFileX;
                mDeltaY = y - mFileY;
                mPreviousFileX = mFileX;
                mPreviousFileY = mFileY;
                /*
                 * On retourne true afin que l'ensemble des évènements
                 * suivants nous parviennent
                 */
                return true;
            }
            break;
 
        case MotionEvent.ACTION_MOVE:
            /*
             * L'utilisateur est en train de bouger son doigt sur l'écran.
             * On effectue simplement le déplacement de l'image du fichier
             * texte à la position du doigt
             */
            mFileX = x - mDeltaX;
            mFileY = y - mDeltaY;
            /*
             * On invalide la vue afin de dessiner l'image du fichier au
             * nouvel endroit. La méthode appellée est expliquée plus bas.
             */
            optimizedInvalidate();
            return true;
 
        case MotionEvent.ACTION_UP:
            /*
             * L'utilisateur vient de relacher la pression sur l'écran.
             */
            log("Stop moving text file");
            if (intersect(mFileX, mIconsSize, mTrashX, mIconsSize)
                    * intersect(mFileY, mIconsSize, mTrashY, mIconsSize) > mIconsSize
                    * mIconsSize * SURFACE_THRESHOLD) {
                /*
                 * Si la surface recouverte de la corbeille par le fichier
                 * est suffisante, on change l'état de la corbeille
                 */
                mIsTrashFull = true;
                log("File trashed");
            } else {
                /*
                 * Sinon on repositionne le fichier à son emplacement
                 * d'origine
                 */
                mFileX = mFileStartX;
                mFileY = mFileStartY;
            }
            /*
             * On réactualise la vue pour afficher les changements.
             */
            invalidate();
            return true;
    }
    return super.onTouchEvent(event);
}

Dans le code donné ci-dessus, une méthode optimizedInvalidate() est appelée. Cette méthode permet d’optimiser le rafraichissement de la vue. Il aurait, en effet, été possible d’utiliser invalidate() en lieu et place d’optimizedInvalidate(). Malheureusement, invalidate() oblige le système à redessiner l’intégralité de la vue. Travaillant sur un terminal mobile, nous devons respecter des régles d’optimisations strictes. Une des principales règles, bien qu’évidente, mentionne qu’il ne faut rien faire d’inutile. Il serait donc stupide de redessiner l’intégralité de la vue alors qu’une faible partie seulement a besoin d’être actualisée. La méthode optimizedInvalidate() s’occupe d’optimiser la surface invalidée :

private void optimizedInvalidate() {
    /*
     * On met en tampon les variables de classes souvent utilisées
     * pour accélérer l'exécution
     */
    final int iconSize = mIconsSize;
    final float fileX = mFileX;
    final float fileY = mFileY;
    /*
     * On trouve les coordonnées du point supérieur gauche de la zone à
     * invalider
     */
    final int l = (int)Math.min(fileX, mPreviousFileX);
    final int t = (int)Math.min(fileY, mPreviousFileY);
    /*
     * On trouve les coordonnées du point inférieur droit de la zone à
     * invalider
     */
    final int b = (int)Math.max(fileX + iconSize, mPreviousFileX + iconSize);
    final int r = (int)Math.max(fileY + iconSize, mPreviousFileY + iconSize);
    mPreviousFileX = fileX;
    mPreviousFileY = fileY;
    /*
     * Invalide la vue en utilisant invalidate(int, int, int, int)
     */
    invalidate(l, t, b, r);
}

Voilà, la première ébauche de notre vue personnalisée est maintenant terminée. Vous êtes maintenant prêt(e)s à enrichir formidablement le framework Android et vos interfaces graphiques. Tous à vos SDK !

Comme vous avez dû le remarquer, je bloggue beaucoup moins en ce moment. La raison d’une telle absence est simple : je suis très occupé par mon stage de fin d’études et notamment par la rédaction d’un rapport et la préparation d’une soutenance orale.

Ce blog est normalement, au vu de son nom, orienté développement sur Android. En préparant mes slides de soutenance, j’ai souhaité y inclure un ensemble de ressources graphiques telles que le droid, les logos Android ou de l’OHA (Open Hanset Alliance), etc. A mon grand regret je n’ai pas trouvé énormément de ressources sur le net :(. Le peu de ressources disponibles est, de plus, souvent sous un format ne gérant pas la transparence (jpg ou gif) et de petite taille. En conséquence, j’ai décidé de faire mes propres images et je pense qu’il peut être intéressant de vous les donner ici. N’hésitez pas à les utiliser pour vos propres présentations !

Les images sont disponibles dans ce zip. Je tiens tout de même à préciser que les droits de chaque image vont à leurs créateurs initiaux. N’étant absolument pas familier du principe de “droit à l’image” (je n’ai jamais étudié cette particularité), je m’excuse d’avance si cette diffusion d’images n’est pas autorisée. Si tel est le cas, merci de m’en informer.