I. Introduction

Un des aspects primordiaux d'Android est la quantité d'équipements différents. Ce choix du « hardware », s'il est à l'avantage de l'utilisateur (gammes de prix, hardware évolutif, etc.), cela rend la programmation d'interfaces particulièrement difficile, et on se retrouve comme tout développeur HTML à hésiter entre une interface « fixe » (quitte à perdre de l'espace), ou une interface « fluide », ardue à maintenir et programmer.

Tout, en général, commence par une question : Quelle taille fait mon écran ?
Il est très facile de répondre à côté de la plaque… En effet la réponse dépend de l'unité de mesure dans laquelle on veut cette taille, et comme nous allons le voir, l'unité la plus adaptée n'est pas toujours celle que l'on croit.

II. Les unités de mesure dans Android.

II-A. Unités Physiques

Ces unités de mesure correspondent à une dimension concrète (mesurable physiquement) :

II-A-1. Millimètres, Pouces et Points

Les trois unités principales sont les millimètres (mm), les pouces (in) et les points (pt).

Ces unités sont en fait la même « dimension », dans le sens où on peut toujours passer de l'une à l'autre avec un simple calcul :

  • in = 72 pt (il y a 72 points dans un pouce) ;
  • in = 25,4 mm (un pouce fait 2,54 cm) ;
  • mm = 2,835 pt.

Utiliser ces unités est une bonne manière de proposer une interface dont la taille visuelle est identique sur tous les équipements. Le seul handicap est que justement, cette taille fixe empêche l'utilisation d'outils d'accessibilité (interface grossie, taille de texte énorme, etc.), et surtout, il est quasiment impossible d'insérer des éléments graphiques en pixels (bitmaps par exemple) dans une interface en dimension physique sans d'odieux redimensionnements. L'utilisation de ces unités est donc fortement non recommandée.

II-A-2. Pixels

Les pixels « hardware » sont aussi une dimension physique… On peut à tout moment les compter. L'unité utilisée pour les pixels est px.

Si cette unité semble toute adaptée pour une interface (d'ailleurs les positions/tailles des vues dans Android sont dans cette unité) et en particulier pour des bitmaps, elle rend l'implémentation d'interfaces « fluides » (voir ci-dessous) quasiment impossible.

D'autre part, les écrans ayant presque toutes des densités de pixels différentes, une taille en pixel pourra être énorme sur un appareil, et tout petit sur un autre…

Même sur un même appareil, un fabricant peut tout à fait décider de modifier la résolution de l'écran, et ainsi voir le même modèle (Galaxy Note 10" par exemple) avec deux résolutions (en pixels) différentes.

Les dimensions en pixels sont donc à proscrire le plus possible.

II-B. Unités Virtuelles

Ces unités ne sont pas mesurables directement.

En connaissant le hardware il est toujours possible de convertir d'une unité physique en une unité virtuelle (ou l'inverse), mais leur principal objectif est de justement s'affranchir du hardware, et de permettre ainsi aux développeurs de supporter le maximum d'équipements sans rien modifier à leur interface.

Toutes ces unités dépendent (plus ou moins directement) de la densité en pixels de l'écran (appelée « dpi »: dots per inch, ou pixels par pouces en français).

II-B-1. « Density Independant Pixels »

Aussi appelée dip ou dp, c'est sans doute l'unité la plus utilisée dans Android. Ils correspondent à « la taille en pixels qu'aurait l'interface sur un écran à 160 dpi » (160 pixels par pouce).

px = dp * dpi / 160
ou
dp = px * 160 / dpi

Puisque sur un écran à 160 dpi, il faut 160 pixels pour faire un pouce, on pourrait aussi dire que le nombre de « dip » (ou « dp ») est la taille en 1/160ème de pouce. Hélas ce n'est pas vrai, sur les terminaux dont la densité en pixels n'est pas exactement l'une des quatre densités prédéfinies sur Android… nous verrons pourquoi plus loin.

L'énorme avantage de cette mesure, est de pouvoir calculer une taille « visuelle » quasiment identique sur tous les terminaux, en s'affranchissant entièrement de leur résolution ou de leur taille physique. Ainsi tous les terminaux auront une largeur/hauteur d'écran exprimée en « dp ».

II-B-2. « Scale Independant Pixels »

Il existe une autre unité « virtuelle », les « sp » (Scale Independant Pixels). Cette taille est basée sur les « dp », mais intègre le choix de l'utilisateur quant à la taille du texte. Cette unité est particulièrement importante, en particulier pour les utilisateurs nécessitant des aides d'accessibilité !

Par défaut, 1 dp = 1 sp, mais si l'utilisateur choisit une taille de texte différente, on aura:
dp = f * 1 sp (f étant le facteur d'agrandissement du texte).

II-C. Exemples par l'image

II-C-1. Les résultats concrets

Une image valant mieux qu'un long discours, voici quatre interfaces identiques. La première sur un écran à 160 dpi, la seconde sur un écran à 240 dpi, la troisième sur un écran à 240 dpi dont la taille de texte a été modifiée, et la dernière sur un Galaxy S2.

Image non disponible Image non disponible Image non disponible Image non disponible

II-C-2. Les DPI « Constructeur »

Comme le montrent les images, sur les densités 160 dpi & 240 dpi le résultat attendu est bien cohérent, 160 dp = 1 pouce. Mais pourquoi donc sur le Galaxy S2 cela n'est-il pas le cas ?
La réponse tient dans une autre question :

Les designers sont susceptibles, et n'aiment pas voir leur travail pourri par un redimensionnement (même très bien réalisé), et peuvent se mettre à faire grève pour un pixel de travers. Et cela se comprend ! Dans ces conditions, comment être sûr qu'une image de 64 x 64 pixels par exemple, fasse bien 64 x 64 pixels à l'écran (quand elle devra être incluse dans une vue dimensionnée en « dp ») ?

La solution de Google est simple, le constructeur de l'appareil choisit l'une des quatre densités prédéfinies : « ldpi » (120 dpi), « mdpi » (160 dpi), « hdpi » (240 dpi), « xhdpi » (320 dpi) ou « xxhdpi » (480 dpi) pour l'écran.

Une fois cette densité choisie, elle sera appliquée partout, quelle que soit la véritable densité du téléphone. Un designer pourra donc faire une image, et au pire, voire cette image avec des pixels doublés, ou divisés par deux. En aucun cas le système n'aura à effectuer un redimensionnement compliqué (et périlleux)…

De plus le designer peut fournir une version pour chacune des densités et ainsi être sûr qu'un pixel d'un bitmap correspondra toujours à un pixel à l'écran, quel que soit le terminal.

On a donc au final deux densités : une densité hardware (hsd= Hardware Screen Density) qui est la densité réelle physique de l'écran, et une densité software (dpi) qui est la densité choisie par le constructeur.

Si on prend l'exemple du Galaxy S2, celui-ci a une densité hardware de 217 dpi, et une densité software « hdpi » (soit 240 dpi).

Les fonctions de conversion pixels/dp restent inchangées… Et on a :

in = px / hsd = (dp * dpi) / (160 * hsd)

Sur le Galaxy S2, donc on a bien 1 in = 217 px = 144,6 dp

II-D. Quelle taille fait mon écran ?

Comme on l'a vu, un écran possède au moins trois tailles :

  • en pixels : 480 x 800 px pour le Galaxy S2 ;
  • en pouces : 2,2 x 3,7 in (ou 56,2 x 93,6 mm) pour le Galaxy S2 ;
  • en «dp» : 320 x 533 dp pour le Galaxy S2.

À ces trois tailles, Android va rajouter une quatrième, relative à la taille en « dp » de l'écran, afin de catégoriser le type d'écran :

  • « small », au minimum 320 dp x 426 dp ;
  • « normal » au minimum 320 dp x 470 dp ;
  • « large » au minimum 480 dp x 640 dp ;
  • « xlarge » au minimum 720 dp x 960 dp.

Notre Galaxy S2 a donc une taille « normal »… Il est bon de noter que si Samsung avait choisi une densité « mdpi » pour le téléphone, il aurait alors eu une taille « large » !

III. Réalisation d'interfaces fluides

Comment dans ces conditions réaliser une interface qui s'adapte automatiquement à l'écran ?

Tout d'abord, en choisissant les bonnes ressources…

III-A. Sélections de Ressources

III-A-1. Sélection basée sur la densité

Nous avons vu que l'écran se voit attribuer une densité (software) qui peut être « ldpi », « mdpi », « hdpi » ou « xhdpi ».

Quand une ressource est chargée, il est possible de spécifier pour quelle densité software cette ressource s'applique, et ainsi fournir une version de la ressource pour chaque densité.

Si ceci n'a aucun intérêt pour la plupart des ressources, les ressources de type « drawable » basées sur des bitmaps bénéficient directement de cette sélection, avec la possibilité pour le designer de peaufiner les redimensionnements avec tous les filtres qu'il veut.

À noter que si la ressource n'existe pas dans la densité voulue, Android va utiliser une autre densité (quitte à choisir la densité par défaut) et redimensionner la ressource automatiquement. Cette solution marche assez bien pour les images simples (produits, contacts, photos) mais assez mal pour les éléments d'interface tels que les boutons, check-mark qui nécessitent un traitement particulier des bordures / effets d'ombre…

III-A-2. Sélection basée sur la taille de l'écran (avant Honeycomb)

Comme on l'a vu, Android a aussi assigné une « taille » à l'écran : « small », « normal », « large » ou « xlarge »… Cette taille peut aussi servir de qualificatif aux ressources.

Par exemple, si on a deux modes d'interface : un mode « vertical » et un mode « horizontal » qui nécessite au moins 470 dp en largeur…

On va avoir deux « layouts ». Le layout « vertical » sera placé dans les ressources « small » et « normal-portrait ». Le layout « horizontal » sera placé dans les ressources par défaut.

Et voilà, le système va sélectionner le bon layout, et n'utiliser le mode « horizontal » que dans le cas ou l'on a 470 dp de large !

III-A-3. Sélection basée sur la taille de l'écran (depuis Honeycomb)

Depuis Android 3.0, il est possible de spécifier une ressource directement en fonction de la largeur de l'écran en dp… Le choix peut inclure l'orientation ou non…

Ainsi, disons qu'à partir de 480 dp de large, notre interface va passer sur un mode « horizontal » plutôt que vertical: on va alors spécifier une ressource avec le qualificatif « w480dp »…
Dès que la partie horizontale de l'écran dépasse 480dp, cette ressource sera choisie. Par exemple, le Galaxy S2 en mode « landscape ».

Il est possible de faire de même pour la hauteur: « h480dp » choisira la ressource dès que la hauteur dépasse 480dp… Ce qui inclut le Galaxy S2 en mode portrait.

Il est finalement possible de faire le test sur la largeur minimale de l'écran (la plus petite des dimensions) : « sw480dp » par exemple ne passera pas sur un GalaxyS2 (mais « sw320dp » oui).

« sw320dp » est donc quasiment identique au qualificatif « normal », « sw480dp » pour « large » et « sw720dp » pour « xlarge ».

III-B. Implémentation de Layout

La plupart du temps, l'implémentation va se baser sur un layout principal qui occupe tout l'espace (« fill_parent » ou « match_parent » selon la version).

Les TextView / EditText ne devraient pas être un problème… Là où l'on va commencer à se tirer les cheveux c'est quand on voudra mélanger des bitmaps et du texte.

III-B-1. Les images fixes

J'appelle ici « image » toute représentation d'une bitmap de dimension fixe, que ce soit par une ImageView ou tout autre moyen. Elle est fixe dans le sens ou sa taille (en pixel) est prédéfinie. C'est le cas, par exemple, pour l'image d'un contact, ou d'un produit. Il n'est généralement pas possible d'avoir plusieurs résolutions de l'image, et, en général, on alterne entre plusieurs images toujours de la même taille. Il y a alors deux possibilités :

La première consiste à laisser Android redimensionner l'image. On fixe alors la taille de l'ImageView à une certaine hauteur/largeur (en fonction de l'environnement autour de l'image, par exemple du texte). L'unité à utiliser est donc le « sp » pour être relatif au texte, ou « dp ».

La seconde est de fixer la taille de l'image en pixels, et de fournir les règles aux objets environnants pour s'adapter à l'ImageView (centrage, etc). Comme nous allons le voir cette solution, si elle conserve toute la qualité de l'image originale, est particulièrement inadaptée.

Imaginons que vous ayez une image de 64 x 64 pixels dans une ImageView à gauche de quatre lignes de texte (16 sp)… La taille totale du texte est de 64 sp.

Si la taille de l'ImageView est définie en « sp » (64 sp par exemple), elle est toujours identique à celle du texte, quel que soit l'écran… Et c'est à l'ImageView que vous direz comment afficher une image de 64 pixels, dans une région qui fera 48, 64, 96 ou 128 pixels de large (selon la résolution de l'écran). « Centrée », « redimensionnée », font partie des choix possibles… Mais la vue en elle-même (ImageView) fera toujours la même hauteur que les quatre lignes de texte.

Si l'ImageView est définie en pixels (64 px), sur un écran mdpi, on aura donc 64 pixels de texte et 64 pixels d'image, sur un écran ldpi le texte fera 48 pixels (plus petit que l'image), sur un écran hdpi, il fera 96 pixels (plus grand que l'image). Il faudra donc une règle pour retailler le texte par rapport à une ImageView qui fera toujours 64 pixels et qui affichera toujours l'image entièrement (mais des fois l'image prendra toute la taille de l'écran, des fois juste un tout petit coin). Autant dire qu'il va devenir compliquer de gérer proprement l'interface autour de l'image. Et on reviendra souvent à inclure l'ImageView dans un Frame, adapté à l'interface, en fournissant à Android les instructions pour placer cette ImageView… Autant dire exactement ce que fait notre première solution à base de taille en « sp »…

III-B-2. Les arrières-plans des Widgets

Un bouton, est simplement un arrière-plan qui varie en fonction de l'état (focus, appuyé, sélectionné, relâché, etc.). L'avant-plan peut être tout et n'importe quoi: du texte (classe « Button » de base), une image (« ImageButton ») ou tout un layout (aucune classe spécifique, mais n'importe quel layout est capable de réagir au « press/unpress » et donc servir de bouton pour peu que l'arrière-plan le laisse penser à l'utilisateur).

Le problème est donc de fournir un arrière-plan cohérent qui s'adapte à la taille désirée du contenu du bouton. Et comme on l'a déjà fait remarquer, retailler une image (surtout avec des bords et des ombrages) ne donne souvent pas de très bons résultats.

Nous allons voir les solutions possibles avec un bouton contenant du texte de 16 sp de haut.

Première solution: utiliser une image fixe… Cette solution est vraiment à exclure sauf dans de très très rare cas où le bouton contient lui-même une image fixe. En effet, il y a fort à parier que votre application n'utilisera pas un seul bouton, et il est plus que souhaitable que tous les boutons d'une application partagent la même imagerie (unité d'interface). La solution de l'image fixe vous forcerait à proposer quatre images (selon les quatre résolutions possibles) pour chaque taille de bouton utilisée! Ce n'est ni pratique, ni économe en taille de ressources.
Pire encore, si l'utilisateur agrandi (ou rétrécit) le texte, le bouton ne sera plus adapté à celui-ci et un redimensionnement (hasardeux pour le coup) pourrait se produire !

Deuxième solution : utiliser les « drawables vectoriels »… Ils sont particulièrement puissants sous Android (bords arrondis, dégradés, etc.). Mais malheureusement, on ne peut pas toujours tout faire avec eux. Par contre, cette solution a l'avantage ne devoir fournir qu'une définition vectorielle applicable à tous les boutons.

Dernière solution : Les 9-patch. Un 9-patch est une image séparée en neuf régions rectangulaires. Les régions dans les angles ne seront jamais redimensionnées. Dans ces régions, un pixel sera toujours un pixel à l'écran. Les régions des bords ne seront redimensionnées que dans une seule direction. La partie centrale se verra redimensionnée entièrement. Au designer de ne pas remplir cette partie avec des hautes fréquences. En général d'ailleurs la région centrale ne fait que 1 pixel (qui sera donc répété au besoin). Le 9-patch donne aussi des informations de « padding » permettant d'indiquer à quel endroit le contenu (texte ?) peut se situer.

Le 9-patch ne résout pas tout, par exemple, il n'empêche pas de fournir une version par densité… En particulier vis-à-vis du padding… Prenons un exemple simple: un 9-patch de 25 x 25 pixels, avec des régions définies comme 12-1-12 sur les deux axes, et un padding de 10 pixels sur tous les côtés…
Si nous prenons le bouton avec un texte de 16 sp, en « mdpi », le bouton final aura une hauteur de 10 px (padding) + 16 px (texte) + 10 px (padding) = 36 px de haut. Le texte occupe 44 % du bouton.
Le même bouton en « xhdpi » aura une hauteur de 10 px (padding) + 32 px (texte) + 10 px (padding) = 52 px de haut (67 % de texte).
En ldpi, on aura 10 px (padding) + 12 px (texte) + 10 px (padding) = 32 px de haut (37 % de texte).
Entre 63 % de vide (« ldpi ») et 33 % de vide (« xhdpi »), il y a une grosse différence visuelle, d'où la nécessité de fournir des versions différentes pour chaque résolution, avec des paddings redimensionnés comme il se doit…

III-B-3. Les arrières-plans des Layout

Dans le cas d'un arrière-plan général, le problème est tout autre… L'arrière-plan ne doit pas contenir de haute fréquence (afin de ne pas « divertir » l'utilisateur) et ainsi peut être re-dimensionnable sans véritable problème (même s'il est toujours possible de fournir plusieurs versions).

III-C. Conclusion

Comme nous l'avons vu, l'utilisation de « px » est une mauvaise idée dans 99% des cas. En général, la représentation visuelle d'une image se fait relativement aux éléments qui l'entourent (et l'objet qui affiche l'image aura donc une taille en « sp »). Seuls les marges et les paddings auront éventuellement défini avec une taille en « dp ».