• 20 hours
  • Medium

Free online content available in this course.

Paperback available in this course

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 2/26/19

Constitution des interfaces graphiques

Log in or subscribe for free to enjoy all this course has to offer!

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'éditeur de mise en page

Android Studio nous fournit un outil qui permet d'effectuer nos mises en page de manière graphique. Cet outil s'appelle leLayout Editor  (ou Éditeur de mise en page en français).

Ouvrez le seul fichier qui se trouve dans le répertoireres/layout. Il s'agit normalement du fichier

activity_main.xml. Une fois ouvert, vous devriez avoir quelque chose qui ressemble à la figure suivante.

Le Layout Editor
Le Layout Editor

Cet outil vous aide à mettre en place les vues directement dans le layout de l'application, représenté par la fenêtre du milieu.

Je vais vous présenter les différentes parties de cet éditeur.

Fenêtre de prévisualisation

Il s'agit de la fenêtre centrale qui contient une toile (uncanvasen anglais, retenez ce terme) et qui affiche le résultat concret de notre interface graphique. C'est dans cette fenêtre qu'on va ajouter, supprimer ou modifier des composants à intégrer dans notre interface graphique.

Cette fenêtre contient un tableau de bord :

Le menu de prévisualisation
Le tableau de bord

Ce menu contient sept boutons qui permettent trois choses :

  • Modifier l'affichage de la fenêtre de prévisualisation histoire de voir ce que donnerait l'interface graphique sur un autre type de terminal.

  • Ajouter des ressources qui permettront de gérer des situations différentes, par exemple pour gérer le terminal en mode paysage ou pour gérer d'autres langues.

  • Modifier la base de l'interface graphique (pour être exact le thème).

Voyons en détail à quoi servent ces boutons.

Icone

Utilité

d

A plusieurs utilités :

  • Si vous avez plusieurs fichiers pour cette interface graphique (différenciés par des quantificateurs) alors ce menu vous permettra d'alterner entre le fichier actuel et un autre.

  • Permet d'ajouter un fichier différencié du fichier actuel par un quantificateur pour cette interface graphique.

  • Permet d'afficher plusieurs aperçus de votre interface graphique en même temps, par exemple sur plusieurs terminaux différents ou sur plusieurs langues différentes.

d

Permet de sélectionner le terminal qui contiendra l'affichage de la prévisualisation. Précisément, on va modifier la résolution pour pouvoir constater comment votre interface graphique s'adapte. A noter que le fichier de ressource avec le quantificateur le plus adapté sera sélectionné.

d

Permet de modifier l'aperçu en fonction de trois quantificateurs :

  • Le terminal est en mode paysage ou portrait ?

  • Le périphérique est-il attaché à un dock ?

  • Est-on en mode jour ou en mode nuit ?

d

Permet de changer le thème associé à votre activité. Plus de détails sur ce sujet plus tard.

d

Fais le lien entre votre interface graphique et les activités auxquelles elle est associée.

d

Permet de choisir une langue si vous avez des quantificateurs en fonction de la langue.

d

Permet de vérifier le comportement en fonction de la version de l'API, si vous aviez défini des quantificateurs à ce niveau-là.

On trouve aussi une barre d'outils:

La barre d'outils
La barre d'outils

Cette barre permet de modifier l'affichage de la prévisualisation :

Icone

Utilité

d

Permet de remettre l'affichage à ses dimensions par défaut.

d

Permet d'agrandir la taille de l'affichage pour correspondre à la taille réelle de l'affichage.

d

Permet d'agrandir la taille de l'affichage.

d

Permet de réduire la taille de l'affichage.

d

Si vous modifiez le code XML, permet de mettre à jour l'aperçu graphique.

d

Si vous modifiez le code XML, permet de prendre une capture d'écran.

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'unTextView. 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.

Un menu apparaît lors d'un clic droit sur une vue
Un menu apparaît lors d'un clic droit sur une vue

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'encartPropertiesen bas à droite (voir figure suivante).

L'encart « Properties »
L'encart « Properties »

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 est possible de faire un cliquer/glisser
Il est possible de faire un cliquer/glisser

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.

Vous pouvez redimensionner les vues
Vous pouvez redimensionner les vues

Avant d'aller plus loin avec cet éditeur, on va parler de la programmation d'interface graphique en XML.

Pour accéder au fichier XML correspondant à votre projet, cliquez sur le deuxième ongletactivity_main.xml.

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 :

<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" ></TextView>

</RelativeLayout>

La racine possède deux attributs similaires :xmlns:android="http://schemas.android.com/apk/res/android"etxmlns: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'appelleRelativeLayout. Vous voyez qu'elle englobe un autre nœud qui s'appelleTextView. 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).

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 sontlayout_width, qui définit la largeur que prend la vue (la place sur l'axe horizontal), etlayout_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 :

  • dpoudip: 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.

Il y a quelque chose que je trouve étrange : la racine de notre layout, le nœudRelativeLayout, utilisefill_parenten 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).

Il est possible de définir une marge interne pour chaque widget
Il est possible de définir une marge interne pour chaque widget

Ci-dessous avec l'attributandroid:paddingdans le fichier XML pour définir un carré d'espacement ; la valeur sera suivie d'une unité, 10.5dp par exemple.

<TextView
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:padding="10.5dp"
  android:text="@string/hello" />

La méthode Java équivalente estpublic void setPadding (int left, int top, int right, int bottom).

textView.setPadding(15, 105, 21, 105);

En XML on peut aussi utiliser des attributsandroid:paddingBottompour définir uniquement l'espacement avec le plancher,android:paddingLeftpour définir uniquement l'espacement entre le bord gauche du widget et le contenu,android:paddingRightpour définir uniquement l'espacement de droite et enfinandroid:paddingToppour 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, leXest la classe où se situe l'identifiant dansR.javaet enfin, leYsera le nom de l'identifiant. Bien sûr, la combinaisonX/Ydoit pointer sur un identifiant qui existe. Reprenons notre classe créée par défaut :

<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" ></TextView>

</RelativeLayout>

On devine d'après la ligne surlignée que leTextViewaffichera le texte de la ressource qui se trouve dans la classeStringdeR.javaet qui s'appellehello_world. Enfin, vous vous rappelez certainement aussi que l'on a récupéré des ressources à l'aide de l'identifiant que le fichierR.javacré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 fichieractivity_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 jamaisR.javapar vous-mêmes, malheureux !).

Afin de créer un identifiant, on peut rajouter à chaque vue un attributandroid: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.

LeXest la classe dans laquelle sera créé l'identifiant. Si cette classe n'existe pas, alors elle sera créée. Traditionnellement,Xvautid, mais donnez-lui la valeur qui vous plaît. Enfin, leYsera 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 monTextView« text » et de changer le padding pour qu'il vaille 25.7dp, ce qui nous donne :

<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" ></TextView>

</RelativeLayout>

Dès que je sauvegarde, mon fichierRsera modifié automatiquement :

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éthodepublic View findViewById (int id). Attention, cette méthode renvoie uneView, 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 classeClasse_1hérite (ou dérive, on trouve les deux termes) d'une autre classeClasse_2, il est possible d'obtenir un objet de typeClasse_1à partir d'un deClasse_2avec le transtypage. Pour dire qu'on convertit une classe mère (Classe_2) en sa classe fille (Classe_1) on dit qu'on casteClasse_2enClasse_1, et on le fait avec la syntaxe suivante :

//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éthodevoid setContentView (View view). Dans l'exemple suivant, l'interface graphique est référencée parR.layout.activity_main, il s'agit donc du layout d'identifiantmain, autrement dit celui que nous avons manipulé un peu plus tôt.

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.

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 lesetContentView?

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 fonctionView 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 lesetContentView! Donc lefindViewByIdretourneranullpuisque l'arbre n'existera pas et l'objet ne sera donc pas dans l'arbre. On va à la place utiliser la méthodestatic 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é.

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 premiersetContentViewpour « inflater » (désérialiser) l'arbre et récupérer la vue pour la mettre dans un secondsetContentView?

Un peu comme cela, voulez-vous dire ?

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 deuxsetContentViewdansonCreate, eh bien on ne prendra en compte que la dernière ! Ainsi, toutes les instances desetContentViewpré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 classeViewGroupqui elle même hérite de la super classeView. Puisque les widgets héritent aussi deViewet que lesViewGrouppeuvent 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.

  • Viewregroupe 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 classeR.javaqui liste l'ensemble des identifiants de vos ressources.

Example of certificate of achievement
Example of certificate of achievement