Comme vous l’avez probablement remarqué, je suis quelqu’un d’assez perfectionniste (en tout cas sur les UIs, moins sur les fautes d’orthographes de mes articles …) qui aime utiliser et surtout concevoir des applications aux interfaces graphiques abouties ou comme j’aime le dire polished. Pour considérer une interface comme “parfaite” encore faut-il avoir une liste de l’ensemble des points gageant de la qualité d’une interface : fluidité, esthétique, ergonomie, etc. Au cours de précédents articles, j’ai déjà mentionné beaucoup de ces points “limitants” en donnant des techniques permettant de minimiser leurs impacts ou tout simplement de les supprimer :

Une UI est le produit de plusieurs acteurs. Ces acteurs peuvent être séparés en 4 grands groupes : les ergonomes (qui s’attachent à reproduire/coller voire améliorer la logique des applications Android), les graphistes (qui tentent également de suivre le design/look ‘n ‘feel Android), des intégrateurs (les développeurs utilisant le travail des 2 précédents acteurs pour concevoir l’application finale) et les testeurs (qui sont, je le déplore, souvent les utilisateurs finaux sur Android). En qualité d’ingénieur d’études sur plateformes mobiles (et donc d’intégrateur), je suis quotidiennement amené à concevoir des applications pour la plateforme Android. Il m’arrive donc de devoir intégrer des chartes graphiques dans certaines applications. Je m’aperçois, malheureusement, que les graphistes n’ont très souvent aucune ou peu de connaissances particulières concernant les contraintes inhérentes à Android et au mobile en général. Voici le “top 5″ des points sur lesquels je me bats :

  • Boutons à glossy multiples dans tous les sens. Cela donne généralement de jolis boutons mais rend totalement impossible sa transformation en 9-patches. Les graphistes ne pensent pas que le contenu peut s’agrandir (suite à l’internationalisation par exemple). En conséquence, les images sont étirées “salement” ou leur contenu déborde lorsqu’il est trop important (dans le contexte d’un bouton, je considère que l’option setEllipsize de la classe TextView n’est pas une bonne solution)
  • Utilisation abusive de la transparence. Il est vrai que les designs à la “Web 2.0″ utilisant énormément la transparence sont assez bien adaptés au mobile en terme d’esthétique. Malheureusement, qui dit transparence dit “alpha blending”. Ce processus qui consiste à déterminer la couleur finale d’un pixel transparent en fonction des couleurs des pixels sous-jacents, coûte assez cher et n’est donc pas réellement adapté à des designs en mouvement continuel (ListView par exemple).
  • Positionnement absolu des objets graphiques. Ce problème récurrent démontre tout simplement que les graphistes n’ont pas connaissance des possibilités d’Android en matière d’adaptation aux différentes tailles d’écran. Je me retrouve très souvent confronté à des designs spécialement conçus pour des résolutions de 480×320 pixels en mdpi (160dpi) et en orientation portrait. Que faire de ces designs lorsque l’écran a une résolution/densité différente ou tout simplement lorsqu’il passe en mode paysage?
  • Manque d’états sur les boutons. Les ergonomes le crient haut et fort : un retour utilisateur est indispensable lorsque ce dernier interagit avec le terminal : appui sur l’écran, utilisation de la trackball, etc. J’ai souvent accès à des chartes graphiques n’ayant qu’un seul et unique état pour chaque “contrôle” (ce que j’entends par contrôle c’est un élément graphique sur lequel l’utilisateur peut agir : EditText, Button, etc.). Comment montrer à l’utilisateur que l’appui sur le bouton a bien été pris en compte ? C’est tout simplement impossible. N’oubliez donc pas de définir l’ensemble des états qui peuvent être utilisés (notion de StateListDrawable).
  • Utilisation de dégradés. Une belle charte graphique est généralement composée de dégradés. Bien que le rendu soit parfait sur l’écran de votre ordinateur, il en va bien souvent autrement lorsque le design est intégré à une application Android. Je vous laisse regarder les images ci-dessous pour bien comprendre de quoi je parle et ce que nous allons essayer de contourner dans cet article :

Vous n’arrivez pas à voir le problème qui me chagrine dans de nombreuses applications Android ? Regardons de plus près ! A un tel niveau de zoom vous ne pouvez pas le rater :

Avec les deux images ci-dessus, le problème saute aux yeux : le dégradé est discontinu et un phénomène de bandes apparait. Ce phénomène (qui n’est absolument pas voulu, à mon avis, sur les copies d’écran précédentes) est aussi appellé gradient banding en anglais.

Explication du phénomène

En informatique, une couleur est représentée sous la forme d’un ensemble de 4 valeurs représentant respectivement le pourcentage d’alpha, de rouge, de vert et de bleu. Ainsi une couleur telle que #ff0000 représente le rouge parfait alors que #770000ff indique un bleu semi transparent

La plateforme Android dispose de plusieurs modes de configuration pour représenter les couleurs. Ces modes de configuration sont plus communément appelés “palettes de couleur” (cf android.graphics.Bitmap.Config) et permettent au système de comprendre la représentation des couleurs en mémoire (nombre de bits pour chaque couleur) :

  • ARGB_8888 : Chacune des 4 composantes est codée sur 8 bits. Une couleur de la palette ARGB_8888 occupe donc 4*8 = 32 bits en mémoire. Dans cette palette de couleur, il y a 2^32 = 4 294 967 296 couleurs différentes si on compte l’alpha et 2^(3*8) = 16 777 216 si l’alpha n’est pas compté (ce qui est généralement le cas)
  • ARGB_4444 : Cette palette permet d’obtenir 2^(3*4) = 4 096 couleurs différentes
  • RGB_565 : Cette palette ne gère pas la couche alpha et permet de représenter 2^(5+6+5) = 2^(16) = 65535 couleurs différentes
  • ALPHA_8 : Ne représente que la couche alpha. Cette configuration ne nous intéresse pas dans le cadre de cet article puisqu’elle ne contient que les informations du canal alpha.

Malgré les possibilités d’Android, le matériel limite souvent les possibilités de rendu graphique. En effet, les screenshots ci-dessus montrent des images affichées en mode ARGB_8888. Pourquoi de telles “bandes” sur les dégradés? Ce problème n’est, en réalité, pas inhérent à Android mais aux terminaux mobiles qui utilisent très souvent une palette de 16 bits pour représenter les couleurs à l’écran. Notre émulateur adoré reproduit très bien ce phénomène puisqu’il utilise également une palette restreinte (16 bits). Ainsi, une image affichée dans le mode ARGB_888 n’aura à l’écran qu’un maximum de 2^16 = 65536 couleurs différentes. C’est ce faible nombre de couleurs disponibles qui provoque le gradient banding

Comment contourner le problème ?

Il existe évidemment une méthode permettant de contourner le problème. L’astuce passe par un principe vieux comme l’informatique : le tramage (ou dithering en anglais). Cette technique consiste à “mélanger” les pixels proches les uns des autres pour faire croire à un dégradé plus linéaire.

Activer le dithering par XML

Comme vous le savez, Android permet d’instancier des Drawables par l’intermédiaire de fichiers XML. Imaginons que nous disposions d’une image bitmap_to_dither.png dans res/drawable. Le code XML ci-dessous montre comment créer un BitmapDrawable avec tramage activé (grâce à la propriété android:dither) :

<?xml version="1.0" encoding="UTF-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/bitmap_to_dither"
    android:dither="true" />

De la même façon, une image nine_patch_to_dither.9.png pourra être utilisée et tramée dans un NinePatchDrawable :

<?xml version="1.0" encoding="UTF-8"?>
<nine-patch
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/nine_patch_to_dither"
    android:dither="true" />

Activer le dithering en Java

Instancier les objets par XML est une fonctionnalité très importante d’Android. Cela permet de clarifier le code Java et surtout de séparer la logique de l’aspect UI. Les exemples disponibles dans le SDK encouragent très fortement l’utilisation de fichiers XML lorsqu’il s’agit de créer un layout, un drawable, une animation, etc. Malheureusement, il arrive parfois (c’est assez rare) que certaines fonctionnalités accessibles en Java ne le soient pas en XML. Dans un tel cas, il est nécessaire de passer directement par le code Java :

Drawable d = getResources().getDrawable(R.drawable.drawable_to_dither).setDither(true);

Le code donné précédemment récupère le Drawable drawable_to_dither puis active le tramage sur ce dernier. Si on considère que d est un Drawable de type GradientDrawable (instancié par XML grâce à la balise <shape />), on s’aperçoit que c’est la seule et unique façon d’activer le dithering sur le gradient. En effet, la classe GradientDrawable ne gère pas l’attribut XML android:dither. Je ne sais pas si cette option a tout simplement été oubliée (ce qui me parait bizarre puisqu’Android existe déjà - publiquement - depuis plus de 2 ans) ou si c’est une volonté de la part de la “team Android” (dans ce cas je ne comprends pas réellement l’utilité puisque l’activation via code est possible et que le tramage ne me semble pas être une fonctionnalité très consommatrice de ressources).

Note :Android (depuis la build Eclair - 2.0) facilite grandement la tâche des développeurs puisque de nombreux Drawable ont l’option “dither” activée par défaut : BitmapDrawable, NinePatchDrawable, etc. Malheureusement, lorsqu’on considère les parts de marchés des systèmes Android à la date de rédaction de cet article (13 janvier 2010), on ne peut pas considérer l’option comme “automatique”. Un bon développeur (c’est à dire vous !) se doit de faire en sorte que l’application fonctionne parfaitement sur la totalité des systèmes postérieurs à Cupcake - 1.5. L’activation de l’option “dither” doit toujours se faire de façon manuelle.

Pré-tramer vos images

La dernière possibilité qui s’offre à vous est de pré-tramer vos images à l’aide de votre éditeur graphique préféré… Cette méthode a l’avantage de soulager légèrement (le tramage à la volée n’est pas une opération extrêmement coûteuse vu la puissance des terminaux actuels) le terminal lors du rendu. L’inconvénient réside dans la difficulté d’effectuer ce pré-tramage. J’ai longtemps cherché des méthodes performantes pour pré-tramer mes images et je pense avoir trouvé deux solutions :

  • La première consiste à séparer les 3 couches rouge, vert et bleu de l’image à tramer. Pour chacune des couches on réduit (avec l’option tramage activée) le nombre de bits autorisés pour représenter les différentes couches (en RVB_565 cela on obtient 5 bits pour le rouge, 6 bits pour le vert et 5 bits pour le bleu). On recombine enfin les 3 couches pour obtenir une image parfaitement tramée. Cette démarche est parfaitement expliquée sur ce site anglophone
  • La seconde technique est beaucoup plus simple puisqu’il s’agit d’utiliser un simple plugin Photoshop. Ce plugin ne permet pas, contrairement la méthode précédente, de choisir avec précision le nombre de bits pour chaque couche de couleur. Le plugin est téléchargeable sur le site de Telegraphics

Le tramage par l’exemple

Enfin me direz-vous ! Vous avez raison ! Fini de discuter. Passons à la pratique avec un exemple concret. L’objectif est de réaliser un splash screen qui s’adapte parfaitement à différentes résolutions, différentes orientations … en clair, un splash screen parfaitement conçu pour Android !

Note : Cet article ne traitera pas de la gestion des différentes densités. Il aurait fallu simplement remplir les dossiers res/drawable-ldpi, res/drawable-mdpi et res/drawable-hdpi avec les images adéquates. Mis à part ce point (non abordé pour faciliter la compréhension) les techniques citées ci-dessous permettent de créer un splash-screen indépendent des densités

Commençons, tout d’abord, par une brève introduction de la scène. Nous sommes en train de réaliser un application qui nécessite d’avoir des design parfait. Nous décidons donc de faire appel à un graphiste freelance qui nous aide à trouver une charte graphique et surtout un splash screen attirant. Le résultat envoyé est une image de 320×480, sorte de design brut comme présenté ci-dessous :

En intégrant de façon “bête et disciplinée” les ressources graphiques reçues à l’aide d’une simple ImageView ayant l’attribut android:scaleType à fitXY, on obtient une image étirée (cela se voit surtout en orientation paysage) et donc totalement non adaptée à une utilisation sur un terminal Android :

Réaliser un bon splash screen passe donc par plusieurs étapes :

  • Commençons par “layouter” le splash screen. Ce que j’entends par “layouter” c’est de découper le splash screen en composants élémentaires qui serviront de vues et seront positionnées grâce à un simple layout (FrameLayout, LinearLayout, etc.). Dans notre exemple, on peut séparer le splash screen en 3 parties : ss_logo, ss_version et ss_author. Le positionnement s’effectue grâce à un FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<merge
	xmlns:android="http://schemas.android.com/apk/res/android">
 
	<ImageView
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:layout_gravity="top|right"
		android:src="@drawable/ss_version" />
 
	<LinearLayout
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:layout_gravity="center"
		android:orientation="vertical">
 
		<ImageView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:layout_gravity="center"
			android:src="@drawable/ss_logo" />
 
		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:layout_gravity="center"
			android:text="@string/loading" />
 
	</LinearLayout>
 
	<ImageView
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:layout_gravity="left|bottom"
		android:src="@drawable/ss_author" />
 
</merge>
  • Pour éviter le phénomène de bandes, il convient de tramer à l’aide d’une des méthodes précédemment citées. Notez qu’il n’est pas toujours nécessaire d’effectuer le tramage de vos images. Les images construitent sur une faible palette de couleurs (ss_author par exemple) n’ont pas besoin d’être tramées puisqu’elles ne peuvent pas souffrir de ce fléau de gradient banding. A contrario, ss_logo dispose de plusieurs dégradés (alpha, gris vers blanc) et son rendu est meilleur après tramage

Le fichier ss_background.xml qui permet de définir le dégradé de fond est ajouté au répertoire res/drawable

<?xml version="1.0" encoding="utf-8"?>
<shape
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
 
	<gradient
		android:startColor="#4e525c"
		android:endColor="#31343c"
		android:angle="90" />
 
</shape>

Les images élémentaires du splash screen sont découpées comme suit (le fond des images est en réalité transparent - le gris a été ajouté afin de mieux voir les éléments blancs.

  • Le rendu des dégradés redimensionnables doit être fait en “software”. Dans notre exemple, le fond de notre splash screen s’effectue de façon logicielle en activant l’option android:dither
package com.cyrilmottier.android.metromap;
 
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
 
import com.cyrilmottier.android.metromap.util.Config;
 
public class SplashScreenActivity extends Activity {
 
    private final Handler mHandler = new Handler();
    private static final int SPLASH_SCREEN_DURATION = 1000;
 
    private final Runnable mPendingLauncherRunnable = new Runnable() {
        public void run() {
            Intent intent = new Intent(SplashScreenActivity.this, MetroMapActivity.class);
            startActivity(intent);
            finish();
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash_screen);
 
        mHandler.postDelayed(mPendingLauncherRunnable, SPLASH_SCREEN_DURATION);
 
        // Let's activate dithering for the background to prevent banding
        Drawable d = getResources().getDrawable(R.drawable.ss_background);
        d.setDither(true);
        findViewById(android.R.id.content).setBackgroundDrawable(d);
 
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        mHandler.removeCallbacks(mPendingLauncherRunnable);
    }
 
}

A vos Photoshop, Gimp et autres logiciels exotiques de retouche d’images. Vous avez maintenant toutes les cartes en main pour peaufiner vos UI !

Voilà l’année 2010 qui débute et je souhaitais changer légèrement les sujets traités sur ce blog. En effet, la plupart des articles que j’ai pu rédiger sont strictement techniques. Les prochains articles seront très probablement similaires mais d’autres genres de problèmatiques relatives à Android seront abordées (ergonomie, esthétique, marché, etc.). Cet article vient inaugurer ce léger changement en vous présentant une application de ma création disponible depuis hier sur l’Android Market.

Puisqu’on n’appâte pas les gens avec des cailloux (si si j’ai déjà entendu cette expression quelque part …), voici, pour commencer des copies d’écrans de l’application :

Vous l’aurez deviné, l’intérêt fonctionnel de cette application est de présenter aux utilisateurs une carte du métro parisien. Il est évident, que vous expliquer l’utilité d’une telle application serait déplacé de ma part. Je souhaitais plutôt vous faire part des raisons qui m’ont poussé à développer cette application. Si vous souhaitez la télécharger utiliser le QRCode ci-dessous :

Note : A la date de rédaction de cet article, l’application n’est pas encore entièrement compatible avec les densités différentes de 240dpi (le terminal passe automatiquement en mode “compatibilité” mais cela implique un scaling à la volée qui ralentit drastiquement l’application). Sachez que le travail est en cours et que des mises à jour seront poussées dès que possible.

MetroMap est tout d’abord née suite au concours lancé par Archos. Il y a maintenant plus d’un an, cette compagnie française a choisi d’utiliser la plateforme Android pour certains de leurs terminaux (Archos 5 IT par exemple). Cela semblait une bonne idée mais ces derniers ont décidé (je ne sais pourquoi - et je n’ai pas cherché la raison) de réinventer la roue en développant leur propre market place : AppsLib. Pour lancer AppsLib, Archos a lancé un concours de développement consistant à faire une application fonctionnelle sur Archos 5 IT. Si votre application était validée par Archos, vous receviez un bon de réduction de 220€ pour l’achat d’un Archos 5 IT.

La seconde raison qui m’a motivé c’est le manque d’applications réellement finies disponibles pour Android. Je trouve que l’Android Market regorge d’applications toutes mieux les unes que les autres (au moins d’un point de vue fonctionnel). Malheureusement, dès que je teste les applications, j’ai toujours une impression de “non fini”, de “fait à la va-vite”, de “encore 2 jours de développement et l’application est parfaite”, d’”application publiée trop tôt”, etc. J’ai voulu montrer qu’avec un peu de patience et de volonté, il est possible d’obtenir une application jolie et aboutie.

Pour finir, j’ai souhaité montrer à tous mes lecteurs que ce que je poste sur ce blog a une utilité ^^. En effet, la plupart des principes que j’ai utilisés dans MetroMap Paris ont été expliqué dans différents articles techniques publiés sur ce blog. En clair, si vous êtes bon élève voilà le genre d’applications que vous devriez pouvoir au minimum obtenir (je dis “au minimum” car je ne peux que vous souhaiter de faire mieux).

J’espère que vous trouverez cette application intéressante du point de vue UI et je ne peux que vous remercier pour les commentaires et notations que vous effectuerez sur ce blog ou l’Android Market.

J’allais oublier un dernier petit point … le point technique. Vous remarquerez que l’application affiche une image de très grande taille. Tenter de créer une Bitmap à partir d’une image de 2300×2300px ou plus se solde tout simplement sur un OutOfMemoryException (2300*2300*4 = 10,58 Mo). J’ai donc développé une API permettant de contourner le problème (principe de tuiles ou tiles). J’ai cru comprendre que c’est un problème assez récurrent parmi les développeurs Android. L’ouverture du code permettrait de présenter différentes fonctionnalités intéressantes (tiling API, bouncing flings, etc.) et je tenterai de le faire si je trouve mon code assez propre et bien conçu. Si vous êtes intéressé par une telle API, n’hésitez par à laisser un commentaire ci-dessous !