Bien, maintenant que vous avez compris le principe et l'utilité des ressources, voyons comment appliquer nos nouvelles connaissances aux interfaces graphiques. Avec la diversité des machines sous lesquelles fonctionne Android, il faut vraiment exploiter toutes les opportunités offertes par les ressources pour développer des applications qui fonctionneront sur la majorité des terminaux.
Une application Android polyvalente possède un fichier XML pour chaque type d'écran, de façon à pouvoir s'adapter. En effet, si vous développez une application uniquement à destination des petits écrans, les utilisateurs de tablettes trouveront votre travail illisible et ne l'utiliseront pas du tout. Ici on va voir un peu plus en profondeur ce que sont les vues, comment créer des ressources d'interface graphique et comment récupérer les vues dans le code Java de façon à pouvoir les manipuler.
L'interface d'Eclipse
La bonne nouvelle, c'est qu'Eclipse nous permet de créer des interfaces graphiques à la souris. Il est en effet possible d'ajouter un élément et de le positionner grâce à sa souris. La mauvaise, c'est que c'est beaucoup moins précis qu'un véritable code et qu'en plus l'outil est plutôt buggé. Tout de même, voyons voir un peu comment cela fonctionne.
Ouvrez le seul fichier qui se trouve dans le répertoire res/layout
. Il s'agit normalement du fichier activity_main.xml
. Une fois ouvert, vous devriez avoir quelque chose qui ressemble à la figure suivante.
Cet outil vous aide à mettre en place les vues directement dans le layout de l'application, représenté par la fenêtre du milieu. Comme il ne peut remplacer la manipulation de fichiers XML, je ne le présenterai pas dans les détails. En revanche, il est très pratique dès qu'il s'agit d'afficher un petit aperçu final de ce que donnera un fichier XML.
Présentation de l'outil
C'est à l'aide du menu en haut, celui visible à la figure suivante, que vous pourrez observer le résultat avec différentes options.
Ce menu est divisé en deux parties : les icônes du haut et celles du bas. Nous allons nous concentrer sur les icônes du haut pour l'instant (voir figure suivante).
- La première liste déroulante vous permet de naviguer rapidement entre les répertoires de layouts. Vous pouvez ainsi créer des versions alternatives à votre layout actuel en créant des nouveaux répertoires différenciés par leurs quantificateurs.
- La deuxième permet d'observer le résultat en fonction de différentes résolutions. Le chiffre indique la taille de la diagonale en pouces (sachant qu'un pouce fait 2,54 centimètres, la diagonale du Nexus One fait $3,7 \times 2,54 = 9 ,4$ cm) et la suite de lettres en majuscules la résolution de l'écran. Pour voir à quoi correspondent ces termes en taille réelle, n'hésitez pas à consulter cette image prise sur Wikipédia.
- La troisième permet d'observer l'interface graphique en fonction de certains facteurs. Se trouve-t-on en mode portrait ou en mode paysage ? Le périphérique est-il attaché à un matériel d'amarrage ? Enfin, fait-il jour ou nuit ?
- La suivante permet d'associer un thème à votre activité. Nous aborderons plus tard les thèmes et les styles.
- L'avant-dernière permet de choisir une langue si votre interface graphique change en fonction de la langue.
- Et enfin la dernière vérifie le comportement en fonction de la version de l'API, si vous aviez défini des quantificateurs à ce niveau-là.
Occupons-nous maintenant de la deuxième partie, tout d'abord avec les icônes de gauche, visibles à la figure suivante.
Ces boutons sont spécifiques à un composant et à son layout parent, contrairement aux boutons précédents qui étaient spécifiques à l'outil. Ainsi, si vous ne sélectionnez aucune vue, ce sera la vue racine qui sera sélectionnée par défaut. Comme les boutons changent en fonction du composant et du layout parent, je ne vais pas les présenter en détail.
Enfin l'ensemble de boutons de droite, visibles à la figure suivante.
- Le premier bouton permet de modifier l'affichage en fonction d'une résolution que vous choisirez. Très pratique pour tester, si vous n'avez pas tous les terminaux possibles.
- Le deuxième fait en sorte que l'interface graphique fasse exactement la taille de la fenêtre dans laquelle elle se trouve.
- Le suivant remet le zoom à 100%.
- Enfin les deux suivants permettent respectivement de dézoomer et de zoomer.
Rien, jamais rien ne remplacera un test sur un vrai terminal. Ne pensez pas que parce votre interface graphique est esthétique dans cet outil elle le sera aussi en vrai. Si vous n'avez pas de terminal, l'émulateur vous donnera déjà un meilleur aperçu de la situation.
Utilisation
Autant cet outil n'est pas aussi précis, pratique et surtout dénué de bugs que le XML, autant il peut s'avérer pratique pour certaines manipulations de base. Il permet par exemple de modifier les attributs d'une vue à la volée. Sur la figure suivante, vous voyez au centre de la fenêtre une activité qui ne contient qu'un TextView
. Si vous effectuez un clic droit dessus, vous pourrez voir les différentes options qui se présentent à vous, comme le montre la figure suivante.
Vous comprendrez plus tard la signification de ces termes, mais retenez bien qu'il est possible de modifier les attributs via un clic droit. Vous pouvez aussi utiliser l'encart Properties
en bas à droite (voir figure suivante).
De plus, vous pouvez placer différentes vues en cliquant dessus depuis le menu de gauche, puis en les déposant sur l'activité, comme le montre la figure suivante.
Il vous est ensuite possible de les agrandir, de les rapetisser ou de les déplacer en fonction de vos besoins, comme le montre la figure suivante.
Nous allons maintenant voir la véritable programmation graphique. Pour accéder au fichier XML correspondant à votre projet, cliquez sur le deuxième onglet activity_main.xml
.
Dans la suite du cours, je considérerai le fichier activity_main.xml
vierge de toute modification, alors si vous avez fait des manipulations vous aurez des différences avec moi.
Règles générales sur les vues
Différenciation entre un layout et un widget
Normalement, Eclipse vous a créé un fichier XML par défaut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:padding="@dimen/padding_medium" android:text="@string/hello_world" tools:context=".MainActivity" /> </RelativeLayout> |
La racine possède deux attributs similaires : xmlns:android="http://schemas.android.com/apk/res/android"
et xmlns:tools="http://schemas.android.com/tools"
. Ces deux lignes permettent d'utiliser des attributs spécifiques à Android. Si vous ne les mettez pas, vous ne pourrez pas utiliser les attributs et le fichier XML sera un fichier XML banal au lieu d'être un fichier spécifique à Android. De plus, Eclipse refusera de compiler.
On trouve ensuite une racine qui s'appelle RelativeLayout
. Vous voyez qu'elle englobe un autre nœud qui s'appelle TextView
. Ah ! Ça vous connaissez ! Comme indiqué précédemment, une interface graphique pour Android est constituée uniquement de vues. Ainsi, tous les nœuds de ces fichiers XML seront des vues.
Revenons à la première vue qui en englobe une autre. Avec Swing vous avez déjà rencontré ces objets graphiques qui englobent d'autres objets graphiques. On les appelle en anglais des layouts et en français des gabarits. Un layout est donc une vue spéciale qui peut contenir d'autres vues et qui n'est pas destinée à fournir du contenu ou des contrôles à l'utilisateur. Les layouts se contentent de disposer les vues d'une certaine façon. Les vues contenues sont les enfants, la vue englobante est le parent, comme en XML. Une vue qui ne peut pas en englober d'autres est appelée un widget (composant, en français).
Un layout hérite de ViewGroup
(classe abstraite, qu'on ne peut donc pas instancier), et ViewGroup
hérite de View
. Donc quand je dis qu'un ViewGroup
peut contenir des View
, c'est qu'il peut aussi contenir d'autres ViewGroup
!
Vous pouvez bien sûr avoir en racine un simple widget si vous souhaitez que votre mise en page consiste en cet unique widget.
Attributs en commun
Comme beaucoup de nœuds en XML, une vue peut avoir des attributs, qui permettent de moduler certains de ses aspects. Certains de ces attributs sont spécifiques à des vues, d'autres sont communs. Parmi ces derniers, les deux les plus courants sont layout_width
, qui définit la largeur que prend la vue (la place sur l'axe horizontal), et layout_height
, qui définit la hauteur qu'elle prend (la place sur l'axe vertical). Ces deux attributs peuvent prendre une valeur parmi les trois suivantes :
fill_parent
: signifie qu'elle prendra autant de place que son parent sur l'axe concerné ;wrap_content
: signifie qu'elle prendra le moins de place possible sur l'axe concerné. Par exemple si votre vue affiche une image, elle prendra à peine la taille de l'image, si elle affiche un texte, elle prendra juste la taille suffisante pour écrire le texte ;- Une valeur numérique précise avec une unité.
Je vous conseille de ne retenir que deux unités :
dp
oudip
: il s'agit d'une unité qui est indépendante de la résolution de l'écran. En effet, il existe d'autres unités comme le pixel (px) ou le millimètre (mm
), mais celles-ci varient d'un écran à l'autre… Par exemple si vous mettez une taille de 500 dp pour un widget, il aura toujours la même dimension quelque soit la taille de l'écran. Si vous mettez une dimension de 500 mm pour un widget, il sera grand pour un grand écran… et énorme pour un petit écran.sp
: cette unité respecte le même principe, sauf qu'elle est plus adaptée pour définir la taille d'une police de caractères.
Depuis l'API 8 (dans ce cours, on travaille sur l'API 7), vous pouvez remplacer fill_parent
par match_parent
. Il s'agit d'exactement la même chose, mais en plus explicite.
Il y a quelque chose que je trouve étrange : la racine de notre layout, le nœud RelativeLayout
, utilise fill_parent
en largeur et en hauteur. Or, tu nous avais dit que cet attribut signifiait qu'on prenait toute la place du parent… Mais il n'a pas de parent, puisqu'il s'agit de la racine !
C'est parce qu'on ne vous dit pas tout, on vous cache des choses, la vérité est ailleurs. En fait, même notre racine a une vue parent, c'est juste qu'on n'y a pas accès. Cette vue parent invisible prend toute la place possible dans l'écran.
Vous pouvez aussi définir une marge interne pour chaque widget, autrement dit l'espacement entre le contour de la vue et son contenu (voir figure suivante).
Ci-dessous avec l'attribut android:padding
dans le fichier XML pour définir un carré d'espacement ; la valeur sera suivie d'une unité, 10.5dp par exemple.
1 2 3 4 5 | <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="10.5dp" android:text="@string/hello" /> |
La méthode Java équivalente est public void setPadding (int left, int top, int right, int bottom)
.
1 | textView.setPadding(15, 105, 21, 105); |
En XML on peut aussi utiliser des attributs android:paddingBottom
pour définir uniquement l'espacement avec le plancher, android:paddingLeft
pour définir uniquement l'espacement entre le bord gauche du widget et le contenu, android:paddingRight
pour définir uniquement l'espacement de droite et enfin android:paddingTop
pour définir uniquement l'espacement avec le plafond.
Identifier et récupérer des vues
Identification
Vous vous rappelez certainement qu'on a dit que certaines ressources avaient un identifiant. Eh bien, il est possible d'accéder à une ressource à partir de son identifiant à l'aide de la syntaxe @X/Y
. Le @
signifie qu'on va parler d'un identifiant, le X
est la classe où se situe l'identifiant dans R.java
et enfin, le Y
sera le nom de l'identifiant. Bien sûr, la combinaison X/Y
doit pointer sur un identifiant qui existe. Reprenons notre classe créée par défaut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:padding="@dimen/padding_medium" android:text="@string/hello_world" tools:context=".MainActivity" /> </RelativeLayout> |
On devine d'après la ligne surlignée que le TextView
affichera le texte de la ressource qui se trouve dans la classe String
de R.java
et qui s'appelle hello_world
. Enfin, vous vous rappelez certainement aussi que l'on a récupéré des ressources à l'aide de l'identifiant que le fichier R.java
créait automatiquement dans le chapitre précédent. Si vous allez voir ce fichier, vous constaterez qu'il ne contient aucune mention à nos vues, juste au fichier activity_main.xml
. Eh bien, c'est tout simplement parce qu'il faut créer cet identifiant nous-mêmes (dans le fichier XML hein, ne modifiez jamais R.java
par vous-mêmes, malheureux !).
Afin de créer un identifiant, on peut rajouter à chaque vue un attribut android:id
. La valeur doit être de la forme @+X/Y
. Le +
signifie qu'on parle d'un identifiant qui n'est pas encore défini. En voyant cela, Android sait qu'il doit créer un attribut.
La syntaxe @+X/Y
est aussi utilisée pour faire référence à l'identifiant d'une vue créée plus tard dans le fichier XML.
Le X
est la classe dans laquelle sera créé l'identifiant. Si cette classe n'existe pas, alors elle sera créée. Traditionnellement, X
vaut id
, mais donnez-lui la valeur qui vous plaît. Enfin, le Y
sera le nom de l'identifiant. Cet identifiant doit être unique au sein de la classe, comme d'habitude.
Par exemple, j'ai décidé d'appeler mon TextView
« text » et de changer le padding pour qu'il vaille 25.7dp, ce qui nous donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:padding="25.7dp" android:text="@string/hello_world" tools:context=".MainActivity" /> </RelativeLayout> |
Dès que je sauvegarde, mon fichier R
sera modifié automatiquement :
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 | public final class R { public static final class attr { } public static final class dimen { public static final int padding_large=0x7f040002; public static final int padding_medium=0x7f040001; public static final int padding_small=0x7f040000; } public static final class drawable { public static final int ic_action_search=0x7f020000; public static final int ic_launcher=0x7f020001; } public static final class id { public static final int menu_settings=0x7f080000; } public static final class layout { public static final int activity_main=0x7f030000; } public static final class menu { public static final int activity_main=0x7f070000; } public static final class string { public static final int app_name=0x7f050000; public static final int hello_world=0x7f050001; public static final int menu_settings=0x7f050002; public static final int title_activity_main=0x7f050003; } public static final class style { public static final int AppTheme=0x7f060000; } } |
Instanciation des objets XML
Enfin, on peut utiliser cet identifiant dans le code, comme avec les autres identifiants. Pour cela, on utilise la méthode public View findViewById (int id)
. Attention, cette méthode renvoie une View
, il faut donc la « caster » dans le type de destination.
On caste ? Aucune idée de ce que cela peut vouloir dire !
Petit rappel en ce qui concerne la programmation objet : quand une classe Classe_1
hérite (ou dérive, on trouve les deux termes) d'une autre classe Classe_2
, il est possible d'obtenir un objet de type Classe_1
à partir d'un de Classe_2
avec le transtypage. Pour dire qu'on convertit une classe mère (Classe_2
) en sa classe fille (Classe_1
) on dit qu'on caste Classe_2
en Classe_1
, et on le fait avec la syntaxe suivante :
1 2 3 | //avec « class Class_1 extends Classe_2 » Classe_2 objetDeux = null; Classe_1 objetUn = (Classe_1) objetDeux; |
Ensuite, et c'est là que tout va devenir clair, vous pourrez déclarer que votre activité utilise comme interface graphique la vue que vous désirez à l'aide de la méthode void setContentView (View view)
. Dans l'exemple suivant, l'interface graphique est référencée par R.layout.activity_main
, il s'agit donc du layout d'identifiant main
, autrement dit celui que nous avons manipulé un peu plus tôt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class TroimsActivity extends Activity { TextView monTexte = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); monTexte = (TextView)findViewById(R.id.text); monTexte.setText("Le texte de notre TextView"); } } |
Je peux tout à fait modifier le padding a posteriori.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class TroimsActivity extends Activity { TextView monTexte = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); monTexte = (TextView)findViewById(R.id.text); // N'oubliez pas que cette fonction n'utilise que des entiers monTexte.setPadding(50, 60, 70, 90); } } |
Y a-t-il une raison pour laquelle on accède à la vue après le setContentView
?
Oui ! Essayez de le faire avant, votre application va planter.
En fait, à chaque fois qu'on récupère un objet depuis un fichier XML dans notre code Java, on procède à une opération qui s'appelle la désérialisation. Concrètement, la désérialisation, c'est transformer un objet qui n'est pas décrit en Java − dans notre cas l'objet est décrit en XML − en un objet Java réel et concret. C'est à cela que sert la fonction View findViewById (int id)
. Le problème est que cette méthode va aller chercher dans un arbre de vues, qui est créé automatiquement par l'activité. Or, cet arbre ne sera créé qu'après le setContentView
! Donc le findViewById
retournera null
puisque l'arbre n'existera pas et l'objet ne sera donc pas dans l'arbre. On va à la place utiliser la méthode static View inflate (Context context, int id, ViewGroup parent)
. Cette méthode va désérialiser l'arbre XML au lieu de l'arbre de vues qui sera créé par l'activité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import android.app.Activity; import android.os.Bundle; import android.widget.RelativeLayout; import android.widget.TextView; public class TroimsActivity extends Activity { RelativeLayout layout = null; TextView text = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // On récupère notre layout par désérialisation. La méthode inflate retourne un View // C'est pourquoi on caste (on convertit) le retour de la méthode avec le vrai type de notre layout, c'est-à-dire RelativeLayout layout = (RelativeLayout) RelativeLayout.inflate(this, R.layout.activity_main, null); // … puis on récupère TextView grâce à son identifiant text = (TextView) layout.findViewById(R.id.text); text.setText("Et cette fois, ça fonctionne !"); setContentView(layout); // On aurait très bien pu utiliser « setContentView(R.layout.activity_main) » bien sûr ! } } |
C'est un peu contraignant ! Et si on se contentait de faire un premier setContentView
pour « inflater » (désérialiser) l'arbre et récupérer la vue pour la mettre dans un second setContentView
?
Un peu comme cela, voulez-vous dire ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class TroimsActivity extends Activity { TextView monTexte = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); monTexte = (TextView)findViewById(R.id.text); monTexte.setPadding(50, 60, 70, 90); setContentView(R.layout.activity_main); } } |
Ah d'accord, comme cela l'arbre sera inflate et on n'aura pas à utiliser la méthode compliquée vue au-dessus…
C'est une idée… mais je vous répondrais que vous avez oublié l'optimisation ! Un fichier XML est très lourd à parcourir, donc construire un arbre de vues prend du temps et des ressources. À la compilation, si on détecte qu'il y a deux setContentView
dans onCreate
, eh bien on ne prendra en compte que la dernière ! Ainsi, toutes les instances de setContentView
précédant la dernière sont rendues caduques.
- Eclipse vous permet de confectionner des interfaces à la souris, mais cela ne sera jamais aussi précis que de travailler directement dans le code.
- Tous les layouts héritent de la super classe
ViewGroup
qui elle même hérite de la super classeView
. Puisque les widgets héritent aussi deView
et que lesViewGroup
peuvent contenir desView
, les layouts peuvent contenir d'autres layouts et des widgets. C'est là toute la puissance de la hiérarchisation et la confection des interfaces. View
regroupe un certain nombre de propriétés qui deviennent communes aux widgets et aux layouts.- Lorsque vous désérialisez (ou inflatez) un layout dans une activité, vous devez récupérer les widgets et les layouts pour lesquels vous désirez rajouter des fonctionnalités. Cela se fait grâce à la classe
R.java
qui liste l'ensemble des identifiants de vos ressources.