Cyril Mottier

“It’s the little details that are vital. Little things make big things happen.” – John Wooden

Le Monde Merveilleux Des Images Extensibles (9-patchs)

Note : malgré son indéniable application à Android, cet article traite d'un concept qui n'est pas propre à Android. En effet les images étirables sont par exemple disponibles dans des formes plus ou moins similaires sous le SDK de l'iPhone (de façon beaucoup moins avancée et intégrée - cf UIImage et son message:

- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight

et dans le CSS3 par le biais de la propriété border-image (j'attends avec impatience la généralisation de cette propriété car elle signe la fin des blocs imbriqués les uns dans les autres - astuce nécessaire pour obtenir une “boite” à taille variable avec des bords arrondis en développement web).

Lors d'un précédent article, j'ai décrit un moyen de définir ses propres boutons. Cette technique reposait principalement sur l'utilisation de l'objet StateListDrawable qu'on retrouve dans le package android.graphics.drawable. En plus des différentes demandes, il me semble essentiel de bien faire connaître les différents Drawable utilisables sur Android. Il s'avère qu'Android fournit généralement l'ensemble des méthodes et objets pour faire de votre interface graphique la plus belle et la plus rapide au monde (j'exagère légèrement là :p) ! Il suffit simplement de savoir que ces objets “magiques” existent. Cet article s'attache à définir le concept de “9-patch” ou plus précisément de l'objet NinePatchDrawable

(Presque) fini la ligne de commande, l'informatique et plus particulièrement les interfaces utilisateurs d'aujourd'hui sont de plus en plus graphiques et animées. Comment concevoir, de nos jours, des interfaces du type des applications Windows 95 faites de segments et d'angles droits et où la transparence était encore une notion futuriste … c'est tout simplement impossible ! La tendance est plutôt à arrondir les angles, à jouer avec la transparence pour obtenir des effets sympas, etc. Le monde du mobile ne déroge pas à la règle puisque les utilisateurs de PC/Mac souhaitent retrouver l'ergonomie et la beauté de leurs OS “fixes” sur leur téléphone.

Les lecteurs de ce blog l'auront très probablement remarqué, je suis partisan du “apprendre par l'exemple”. Je vais donc utiliser un cas d'étude assez simple pour bien introduire le merveilleux concept d'images étirables. Pour faire en sorte que l'intégralité de l'UI de mon application s'accorde (charte graphique cohérente), j'aimerai définir une boite de dialogue uniformisée aux couleurs de l'interface. L'image ci dessous montre un des résultats que j'attends :

A première vue, ça semble très simple car il suffit simplement de mettre l'image du rectangle vert et blanc comme fond d'une simple TextView. Il ne faut néanmoins pas oublier que le contenu et la taille de la boite de dialogue peuvent changer. En utilisant cette méthode un peu rapide, on rencontre très vite un problème lors du passage en orientation paysage et/ou que le texte augmente : le rectangle est horriblement étiré et on retrouve des bords “salement” arrondis.

La seconde méthode, beaucoup plus barbare mais ayant de meilleurs résultats, consiste à séparer la boite de dialogue en différent parties (c'est exactement la même méthode utilisée en développement web lorsqu'on souhaite créer une “boite” à taille variable). Pour ce faire, il suffit de casser la boite de dialogue en composants élémentaires : les 4 coins, les 4 bords, l'image du droïd et une TextView (les plus doués remarqueront même que le modèle si dessous n'est pas adéquat car si on augmente la largeur de la boite de dialogue, le droïd est également étiré - mais l'objectif est ici de montrer que cette méthode est à proscrire). Avec beaucoup de courage et de patience, on réussit à obtenir un modèle de boite de dialogue extensible. Malheureusement cette méthode comporte de sérieux désavantages : il faut séparer le résultat en plusieurs vues (processus long et assez rébarbatif) pour aboutir à un empilement de vues assez indigeste (surtout consommateur de ressources).

La troisième méthode consiste à utiliser les … 9-patchs ! Pour résumer un 9-patch est une image sur laquelle on définit les zones qui sont extensibles. Ainsi le système, lors du dessin de l'image, peut, si nécessaire, étirer l'image à volonté en utilisant les zones précédemment citées. Dans le cas d'un rectangle simple à coins arrondis, il suffit d'informer le système que les zones extensibles se trouvent à l'intérieur du rectangle (coins arrondis exclus). Pour effectuer cette prouesse, reprenons notre exemple (qui est légèrement plus compliqué puisqu'il ne faut pas non plus étirer le droïd). On définit les zones étirables en déterminant les zones étirables sur X et sur Y (en vert). C'est le croisement de ces zones (en rose) qui indique au système la partie de l'image que sera étirée :

Android, dispose néanmoins d'avantages énormes sur ces concurrents : il est possible de définir des zones étirables multiples (comme dans l'exemple) ci dessus alors que sous l'iPhone par exemple il n'est possible de définir qu'une seule et unique zone étirable (qui doit de plus être obligatoirement centrée au milieu de l'image). Lorsque vous définissez plusieurs zones étirables, Android va étirer proportionnellement ces zones afin de conserver les proportions. Dans le cas de notre boite de dialogue, les zones extensibles font toutes les deux 4 pixels de large afin de laisser l'image du droïd au centre.

Le système de Google va encore plus loin en permettant de définir les zones dans lesquelles le contenu doit se positionner (c'est à dire les paddings/marges intérieures). La technique est similaire et consiste à choisir une zone de contenu dans le croisement de 2 zones respectivement sur X et Y. Cette option sur l'emplacement du contenu étant optionnelle, si vous ne définissez aucune zone, le système considèrera que la zone de contenu est l'intégralité de l'image (paddings nuls).

L'énorme avantage d'Android sur les autres systèmes est que l'information sur les zones étirables et la zone de contenu se trouve dans l'image elle même. Ainsi, il a une séparation parfaite de la partie métier et de l'interface graphique. On définit ces zones à l'aide d'un trait noir d'un pixel de large en haut et à gauche pour les zones extensibles et en bas et à droite pour la zone de contenu. Enfin, on ajoute l'extension .9.png pour informer le système que l'image est un 9-patch. Le résultat est donc le suivant :

J'ai développé un petit exemple (téléchargeable dans ce zip) afin de bien montrer la facilité d'utilisation des images extensibles sous Android :

LauncherActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.cyrilmottier.android.fancytoast;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class LauncherActivity extends Activity {

    private Toast mFancyToast;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        View fancyToast = getLayoutInflater().inflate(R.layout.fancy_toast, null);

        mFancyToast = new Toast(this);
        mFancyToast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
        mFancyToast.setDuration(Toast.LENGTH_LONG);
        mFancyToast.setView(fancyToast);

        ((TextView) mFancyToast.getView()).setText(LauncherActivity.this.getResources().getString(R.string.toast_text));

        setContentView(R.layout.main);
    }

    public void showToast(View view) {
        mFancyToast.show();
    }
}

R.layout.fancy_toast contient le code XML suivant. Remarquez l'utilisation du 9-patch fancy_toast_frame de façon totalement transparente (pas d'extension .9.png) :

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/fancy_toast_frame"
    android:gravity="center"
    android:singleLine="false"
    android:textColor="#000" />

J'aimerai terminer sur un petit logiciel fournit avec le SDK Android. En effet, avec un Drawable aussi puissant, l'équipe Android se devait de fournir un logiciel permettant de créer de telles images. Ce logiciel c'est Draw9Patch. Je ne pense pas qu'il soit nécessaire d'expliquer son utilisation ici car c'est un outil très simple et ergonomique. Pour résumer il permet, à partir d'un png de définir les zones étirables et la zone de contenu et de voir, en temps réel, le résultat de l'étirement de l'image sur les deux axes.