Christophe Jacquet: Research and Teaching

Enseignement >

Tutoriel Android : recherche d'itinéraires

Mise à jour : 7 février 2014, pour Android 4.4.

Ce tutoriel vise à vous faire réaliser une petite application sur Android : la recherche d'itinéraires dans le réseau ferré régional d'Île-de-France, le RER. Un noyau fonctionnel vous est fourni, qui comprend une description partielle des lignes A et B, ainsi qu'un moteur de calcul d'itinéraires : vous pourrez ainsi vous concentrer sur la réalisation de l'interface utilisateur. Nous allons afficher la liste des étapes d'un trajet, et proposer la lecture de chacune des étapes par synthèse vocale.

Durée indicative : 2 à 4 heures.

Ce tutoriel a été créé dans le cadre de l'enseignement de « mineure » Interaction homme-machine, en troisième année à Supélec, mais il peut convenir à toute personne désireuse de se mettre au développement sur Android.

Installation des outils de développement

  1. Téléchargez le SDK Android,
  2. Décompressez l'archive ZIP sur votre disque.

C'est tout !

Présentation du kit fourni

Une archive JAR appelée transport.jar vous est fournie ; elle contient le noyau fonctionnel de calcul d'itinéraires. Les classes qu'elle contient sont organisées en deux packages :

Télécharger transport.jar.

Exemple : afficher le chemin entre Gif-sur-Yvette et Nanterre-Université

Ce premier exemple, en Java standard (indépendant d'Android), montre comment s'utilise le package de calcul d'itinéraires.

Station gif = Network.PARIS_RER.getStationForName("Gif-sur-Yvette");
Station nanterre = Network.PARIS_RER.getStationForName("Nanterre-Université");
System.out.println(Network.PARIS_RER.calculateTrip(gif, nanterre));

Conception de votre application

Votre application doit permettre de sélectionner une station de départ, une station de destination, et afficher un itinéraire possible. Pour cette première interface, on envisage les trois écrans suivants :

Screenshot Screenshot Screenshot

Android est fondé sur la notion d'activité, qui correspond à une tâche élémentaire, ce qui se traduit en général par une page écran. Notre application comportera donc trois activités, de deux sortes :

Sur Android, il est facile de faire en sorte qu'une activité donnée appelle avant son propre affichage une ou plusieurs autres activités, que l'on qualifiera de sous-activités.

Nous nous proposons donc d'organiser l'enchaînement des activités comme suit :

  1. Lancement de l'activité principale, PlanTrip (dont l'affichage correspondra à la description d'un itinéraire)
  2. Lors de l'initialisation de PlanTrip, et avant même son affichage, celle-ci lance successivement deux activités :
    • SelectStation une première fois, pour sélectionner la station de départ,
    • SelectStation une second fois, pour sélectionner la station de destination.
  3. PlanTrip finit de s'initialiser, et appelle le noyau fonctionnel pour calculer l'itinéraire (calculateTrip)
  4. PlanTrip affiche enfin son contenu, qui correspond à l'itinéraire qui vient d'être calculé

Création du projet Android

Lancez votre environnement Eclipse disposant de la chaîne d'outils Android, et créez un nouveau projet :

La copie d'écran ci-dessous résume les informations à saisir dans la boîte de dialogue :

Assistant de création de nouveau projet Android

Screenshot

Laissez tous les choix par défaut dans la boîte de dialogue suivante. Notamment, la case Create activity est cochée, ce qui créera une première activité pour notre application.

Vous êtes ensuite invité à choisir une icône pour votre application. Vous pouvez laisser le choix par défaut.

La boîte de dialogue suivante est consacrée au choix du type d'activité à créer. Le choix par défaut, Blank Activity, convient bien à notre application.

Enfin, une dernière étape vous demande de donner un nom à cette activité. Saisissez donc PlanTrip dans la champ Activity Name. Vous pouvez alors cliquer sur Finish.

La capture d'écran ci-contre montre la structure du projet nouvellement créé :

Afin de terminer la configuration de votre projet, vous allez finalement intégrer dans le projet la bibliothèque de calcul d'itinéraires, transport.jar :

La bibliothèque est alors intégrée à votre projet. Vous pouvez aisément le vérifier : faites un un clic droit sur le nom du projet dans le volet gauche, et choisissez Build PathConfigure Build Path. Dans la fenêtre qui s'ouvre, allez sur l'onglet Libraries. transport.jar doit apparaître dans la rubrique Android Private Libraries.

Prise en main

Pour prendre en main le kit de développement, vous allez commencer par ajouter un comportement très simple à votre application. L'idée serait d'avoir un champ de texte en haut de l'écran et un bouton en bas, un appui sur le bouton déclenchant l'affichage d'un message reprenant le contenu du champ texte (type « Hello Untel »).

Ouvrez le fichier res/layout/activity_plan_trip.xml. Vous devez voir un écran qui comporte la barre de titre de l'application et une zone blanche comportant juste un texte du type « Hello World! ». À droite vous est présentée la hiérarchie des widgets : par défaut, un RelativeLayout (le conteneur principal), dans lequel se trouve un TextView qui affiche le petit texte.

Cliquez sur le TextView dans l'interface. Le widget doit se sélectionner. En bas de l'écran, vous devriez avoir un onglet Properties : sélectionnez-le. Si vous ne le voyez pas allez dans les menus WindowShow ViewOther et sélectionnez GeneralProperties pour le faire apparaître.

Regardez par exemple la propriété Text, censée correspondre au texte affiché par le widget. Pourquoi voyez-vous @string/hello_world, et non pas « Hello World! » ?

Screenshot

Modifiez l'interface comme suit :

Modifiez quelques propriétés comme suit :

Il reste un dernier point important à régler : chaque widget est identifié par un identificateur unique (Id). Eclipse affecte des Id par défaut : editText1 pour la première zone de texte, button1 pour le premier bouton, etc. Or pour pouvoir faire référence facilement aux widgets depuis le code, il est préférable de choisir des identificateurs parlants. Pour chacun de vos deux widgets, modifiez dans le panneau Properties la valeur de son Id comme suit :

Au terme de ces modifications, l'interface et sa hiérarchie de widgets doivent se présenter ainsi :

Screenshot

Testez votre première application Android !

Une fenêtre d'émulation s'ouvre, dans laquelle le système Android démarre. Patience, ça peut être long ! Après le démarrage, votre application doit s'ouvrir automatiquement.

Nous voudrions maintenant que lorsque l'utilisateur clique sur le bouton, cela affiche un petit message.

Dans le corps d'une activité, il est possible de relier du code à différents événements de la vie de l'activité, en implémentant des méthodes du type onNomÉvenement. Par exemple, la méthode onCreate est appelé tout au début de la vie de l'activité.

Nous allons donc utiliser la méthode onCreate pour associer au bouton un « gestionnaire de clics », c'est-à-dire une méthode destinée à être appelée à chaque fois que l'utilisateur appuie sur le bouton. La première chose à faire est de récupérer l'objet Java de type Button correspondant au bouton. On utilise pour cela la méthode findViewById qui associe à un Id l'objet correspondant :

Button button = (Button) findViewById(R.id.buttonGo);

L'ajout d'un « gestionnaire de clics » peut se faire de la façon suivante :

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Écrire ici le code à appeler lorsque survient un clic
    }
});

Notez l'utilisation d'une classe interne anonyme (ce qui se fait fréquemment, mais n'est pas obligatoire). Que doit faire le morceau de programme appelé lorsqu'un clic se produit ? Pour faire simple, nous allons afficher un petit message à l'écran.

Pour cela, nous utilisons une construction très utile, permettant d'afficher de petits messages sur l'écran de l'appareil :

Toast.makeText(PlanTrip.this, "Un message", Toast.LENGTH_LONG).show();

Ne pas oublier l'appel .show() à la fin de cette instruction, sans quoi rien ne se passera... (Erreur fréquente)

Modifier le code de PlanTrip de sorte qu'un message soit affiché lorsque l'utilisateur presse sur le bouton. Ce message devra comporter le texte contenu dans la zone de texte (Id userInput, de type EditText). Avec ce que nous venons de voir, vous êtes capables d'obtenir une référence à l'objet Java sous-jacent à la zone de texte. Sur cet objet, vous pouvez appeler la méthode getText() pour accéder au texte contenu.

Testez la nouvelle version de votre application.

Première sous-activité : choix de la station de départ

Comme indiqué en introduction, l'activité PlanTrip sera dédiée au calcul d'itinéraire et à l'affichage du résultat. Nous allons donc utiliser sa méthode onCreate pour lancer successivement les deux sous-activités liées au choix de l'origine et de la destination du trajet. Commençons par le choix de la station de départ.

Créons une seconde activité, dédiée à la sélection d'une station dans une liste. Nous l'appellerons SelectStation, et elle héritera de android.app.ListActivity, une sorte d'activité spécialisée dans les choix parmi une liste d'éléments.

Pour créer cette nouvelle activité (i.e. une classe Java) :

Nous allons ajouter la méthode onCreate de SelectStation, de façon à configurer cette nouvelle activité, notamment lui spécifier quelle est la liste à afficher lorsqu'elle démarre...

Placez-vous dans la classe SelectStation et tapez onCreate puis Ctrl+Espace. Un menu s'ouvre avec différentes possibilités. Choisissez la première. Eclipse génère un squelette pour la méthode, dont la première instruction est un appel à la version par défaut de la méthode (super.onCreate(...)). Toutes les instructions que vous ajouterez dans onCreate devront être placés après cet appel.

Screenshot

Il s'agit maintenant d'indiquer à l'activité SelectStation qu'elle doit afficher la liste de toutes les stations du RER. Il est facile d'obtenir cette dernière à partir de la bibliothèque fournie, via un appel Network.PARIS_RER.getAllStations() qui rend un objet de type List<Station>.

Sous Android (comme sous Swing), les composants d'interface (widgets) sont reliés aux données métier sous-jacentes via des adaptateurs. Plus spécifiquement, un composant de type « liste » est conçu pour utiliser un ArrayAdapter, lequel sait aller s'interfacer avec un objet List de Java.

Voici comment créer l'adaptateur dont vous avez besoin :

    ArrayAdapter<Station> adapter = new ArrayAdapter<Station>(
            this,
            android.R.layout.simple_list_item_1,
            Network.PARIS_RER.getAllStations());

Les arguments du constructeur ArrayAdapter sont les suivants :

Après avoir créé un tel adaptateur, on peut en faire l'adaptateur utilisé par l'activité liste avec l'instruction :

setListAdapter(adapter);

Modifiez le code de onCreate de façon à ce qu'elle mette en place un tel adaptateur.

Il reste une chose à faire avant que nous ayions achevé une première version de notre activité : la déclarer dans le manifeste.

Pour ajouter SelectStation au manifeste :

Screenshot

Nous possédons maitenant une activité SelectStation, qui devrait être capable d'afficher la liste des stations du réseau. Cependant, avant de pouvoir l'essayer, il faut faire en sorte que cette activité soit appelée en tant que sous-activité par notre activité principale, PlanTrip. Cet appel va être ajouté à la fin de la méthode onCreate, de façon à se faire dès que l'application démarre.

Pour démarrer une nouvelle activité, on utilise un objet Intent. Les intents sont utilisés aussi bien pour démarrer une nouvelle activité (et lui transmettre des paramètres) que pour permettre à une activité de transmettre un résultat à son activité parente.

Créer un nouvel intent pour démarrer l'activité SelectStation est très simple :

Intent intent = new Intent(PlanTrip.this, SelectStation.class);

Reste alors à effectuer le démarrage proprement dit de l'activité. Il existe plusieurs méthodes du type startActivity.... Ici, l'activité « fille » que nous lançons doit nous fournir un résultat, la station sélectionnée. Nous utilisons donc la méthode startActivityForResult, à laquelle nous devons donner un identifiant numérique, identifiant que nous utiliserons plus tard pour reconnaître le résultat que la sous-activité nous donnera.

Nous posons donc une constante

private final static int ORIGIN_STATION_SUBACTIVITY = 1;

Et l'appel à startActivityForResult s'écrit :

startActivityForResult(intent, ORIGIN_STATION_SUBACTIVITY);

Modifiez en conséquence le code de PlanTrip.

Exécutez votre programme.

Vous devriez alors voir une liste de toutes les stations connues du système. Cependant, rien ne se passe encore lorsque vous sélectionnez une station... Nous allons tout de suite aborder la réaction à ces interactions.

Réaction aux actions de l'utilisateur

Dans SelectStation, il est facile de répondre aux « clics » sur des éléments de la liste : il suffit d'ajouter une méthode onListItemClick. Son contenu sera appelé lorsqu'on appuiera sur l'un des éléments.

Ajoutez la méthode onListItemClick à la classe SelectStation. Comme précédemment, tapez le nom de la méthode directement dans la classe, puis Ctrl+Espace pour compléter et générer le squelette.

onListItemClick reçoit notamment un paramètre position : il s'agit de l'indice de l'élément sélectionné dans la liste.

Avant de poursuivre la construction de notre application, nous allons commencer par vérifier que les appuis sur des noms de stations sont bien interceptés.

Screenshot Ajoutez un affichage de message dans la méthode onListItemClick. Le message doit comporter la valeur du paramètre position. (Note : souvenez-vous de Toast.makeText...)

Essayez la nouvelle version de l'application. Lorsqu'on appuie sur un élément de la liste, on doit obtenir un affichage comme ci-contre.

En réalité, plutôt qu'afficher un message, nous souhaitons fournir un résultat à l'activité principale.

Pour attribuer un résultat à l'activité, et terminer l'activité, on utilise le code suivant :

    setResult(RESULT_OK, result);   // attribue un résultat
    finish();                       // met fin à l'activié (retour)

Dans setResult, RESULT_OK est un code conventionnel, défini au niveau d'Android, indiquant le succès de l'activité en cours. En revanche, la variable result doit contenir le résultat que nous souhaitons transmettre. Cette variable result doit être de type Intent, type que nous avons déjà manié. Nous devons donc préalablement déclarer cette variable result, et créer un nouvel intent :

Intent result = new Intent();

Le mécanisme pour transmettre des paramètres via des intents est celui des extras, des couples clé-valeur que l'on peut associer à un intent via la méthode putExtra. Dans un tel couple, la clé doit être une chaîne : il faut donc convenir entre activités de la signification des clés. Si l'on pose une constante STATION_ID :

public static final String STATION_ID = "StationID";

alors on peut utiliser putExtra ainsi :

result.putExtra(STATION_ID, position);

Modifier la méthode onListItemClick de façon à ce qu'elle transmette en résultat l'indice de l'élément choisi, et mette fin à l'activité.

Vous pouvez réexécuter votre application à ce stade. Lorsque l'on sélectionne une station, la sous-activité se termine (c'est ce que fait finish()) et l'on revient sur l'activité principale. On peut alors voir son interface (champ texte et bouton « Go »).

À ce stade, la sous-activité SelectStation renvoie bien un résultat (l'indice de la station choisie dans la liste de toutes les stations), mais ce résultat n'est pas encore récupéré par l'activité principale PlanTrip. Pour récupérer des résultats, cette dernière doit mettre en place une nouvelle méthode de traitement d'événements, appelée onActivityResult.

Créez une nouvelle méthode onActivityResult dans PlanTrip en suivant la procédure « habituelle » (taper le nom ou le début du nom, puis Ctrl+Espace). Vous devez obtenir une méthode avec la signature suivante :

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {     
    ...
}

Détaillons les arguments de cette méthode :

Voici donc ce que doit faire cette méthode onActivityResult :

Screenshot Écrivez une première version du code de onActivityResult, sans pour le moment lancer la sous-activité de choix de la destination, mais en vous contentant d'afficher un message à l'écran comportant le nom de la station choisie. Cela vous permettra de vérifier que le fonctionnement est correct.

Voici comment récupérer la valeur entière associée à une clé dans l'intent data : int valeur = data.getExtras().getInt(clé);

Après sélection d'une station, vous devez obtenir le résultat ci-contre.

Choix de la station de destination

Les techniques vues jusqu'ici peuvent être appliquées de même pour le choix de la station de destination. De plus, l'activité SelectStation peut être réutilisée telle-quelle ; elle sera juste appelée deux fois en tant que sous-activité de PlanTrip.

Modifiez le code de votre application de sorte qu'après le choix de la station de départ, l'utilisateur se voit directement proposer le choix de la station de destination. Lorsque les deux choix ont été effectués, un message doit apparaître à l'écran avec le nom des deux stations choisies.

Calcul et affichage de l'itinéraire

Screenshot

À ce stade, vous devez disposer de références vers la station de départ et la station d'arrivée. Supposons que vous ayez ainsi deux variables origin et destination de type Station. La bibliothèque fournie permet de calculer facilement un itinéraire entre ces deux stations grâce à l'expression suivante :

Network.PARIS_RER.calculateTrip(origin, destination)

Le résultat est de type List<TripSection>. Un objet TripSection correspond à une étape élémentaire du trajet, d'une station de départ (getOrigin()) à une station d'arrivée (getDestination()), situées toutes deux sur la même ligne (getLine()).

Modifiez votre application de sorte qu'après le choix de la station de départ et de la station d'arrivée, le contenu du trajet calculé apparaisse dans la zone de texte de l'activité PlanTrip.

On ne cherche pas pour le moment à réaliser un affichage « sympathique », aussi souvenez-vous qu'un objet de type List<TripSection> dispose d'une méthode toString().

Le résultat doit ressembler à l'écran ci-contre.

Nous allons maintenant modifier notre interface de façon à mettre en place un affichage plus ergonomique du trajet calculé. Afficher une liste d'étapes est a priori bien adapté, aussi nous utiliserons un widget ListView.

Screenshot

Modifiez le layout de l'interface de sorte à remplacer la zone de texte (EditText) par une liste (ListView).

Pour définir le contenu d'une ListView, utilisez comme tout à l'heure la méthode setAdapter, à laquelle vous fournirez un objet de type ArrayAdapter<TripSection>. Pour le layout des items de la liste, prenez dans un premier temps simple_list_item_1 comme précédemment ; nous allons améliorer l'interface dans un instant.

Le résultat doit ressembler à la copie d'écran ci-contre.

La vue liste améliore la présentation, mais il faudrait afficher chaque widget de façon plus élégante que ce qui est fait actuellement. Pour cela, au lieu d'utiliser la présentation standard simple_list_item_1, nous allons créer notre propre présentation.

Pour commencer, il s'agit de mettre en forme cette présentation, c'est-à-dire créer un layout adéquat.

Créez un nouveau layout avec File > New > Android XML File. Sélectionnez le type Layout et appelez le fichier resultitem.xml. Gardez la disposition par défaut LinearLayout. Vous obtenez un layout vide, que vous devez compléter.

Chaque item de la liste doit afficher les caractéristiques d'une étape, donc la ligne, la gare de départ et la gare d'arrivée.

Il vous est donc suggéré de créer trois zones de texte (widgets TextView) d'identifiants @id+/TextLine, @id+/TextOrigin, @id+/TextDestination. Voici une proposition de présentation (que vous êtes libres d'adapter à votre idée) :

Screenshot

Il reste maintenant à connecter cette nouvelle disposition et les données à présenter. Pour ce faire, vous allez créer un adaptateur personnalisé pour remplacer le ArrayAdapter standard.

Dans votre package fr.supelec.guiderer, faites New > Class :

Créez un constructeur public TripAdapter(Context context, int resource, List<TripSection> items) :

Surchargez la méthode public View getView(int position, View convertView, ViewGroup parent) {

Cette méthode est chargée de deux choses :

  1. Créer la vue associée à un item si elle n'existe pas encore (convertView contient la vue si elle a déjà été créée, sinon convertView vaut null).
  2. Insérer les données de l'item d'indice position dans la vue.

Vous pouvez utiliser le code suivant pour la création de la vue, ou la récupération d'une instance déjà existante :

        LinearLayout tripView;
        
        if(convertView == null) {
            tripView = new LinearLayout(getContext());
            LayoutInflater li = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            li.inflate(resource, tripView, true);
        } else {
            tripView = (LinearLayout) convertView;
        }

Ensuite, vous n'avez plus qu'à mettre le texte voulu dans vos trois zones de texte. Avec ce que vous avez déjà acquis, vous devez facilement vous en sortir.

Dans PlanTrip, pensez à modifier la création du ArrayAdapter en TripAdapter.

Ajout de la synthèse vocale

Afin de tester la synthèse vocale, nous souhaitons qu'en cliquant sur une étape d'un itinéraire, un texte soit prononcé. Pour pouvoir utiliser la synthèse vocale, il faut commencer par vérifier qu'elle est bien disponible sur notre appareil. On va donc modifier l'initialisation de PlanTrip de sorte à vérifier la présence de la synthèse en premier lieu.

À la fin de la méthode onCreate, remplacez

Intent intent = new Intent(PlanTrip.this, SelectStation.class);
startActivityForResult(intent, ORIGIN_STATION_SUBACTIVITY);

par :

Intent checkTTSIntent = new Intent();
checkTTSIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
startActivityForResult(checkTTSIntent, CHECK_TTS_SUBACTIVITY);

sans oublier d'ajouter l'attribut suivant à la class :

private final static int CHECK_TTS_SUBACTIVITY = 3;

Lorsque l'activité de vérification sera terminée, nous pourrons créer un objet TextToSpeech. Modifiez le début de la méthode onActivityResult comme suit :

if(requestCode == CHECK_TTS_SUBACTIVITY) {
	if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
		tts = new TextToSpeech(this, this);
	} else {
		Intent installTTSIntent = new Intent();
		installTTSIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
		startActivity(installTTSIntent);
	}
}

Il faut que vous déclariez un attribut tts dans votre classe :

private TextToSpeech tts;

Il faut aussi que votre classe implémente l'interface OnInitListener. Cela vous force à ajouter une méthode onInit :

public void onInit(int initStatus) {
	if (initStatus == TextToSpeech.SUCCESS) {
		if(tts.isLanguageAvailable(Locale.FRENCH)==TextToSpeech.LANG_AVAILABLE) {
			tts.setLanguage(Locale.FRENCH);
		}
	} else if (initStatus == TextToSpeech.ERROR) {
		Toast.makeText(this, "Synthèse vocale indisponible", Toast.LENGTH_LONG).show();
		tts = null;
	}

	// démarrage de la sélection des stations de RER...
	Intent intent = new Intent(PlanTrip.this, SelectStation.class);
	startActivityForResult(intent, ORIGIN_STATION_SUBACTIVITY);
}

Cette méthode sera appelée après initialisation du moteur de synthèse. Nous essayons de basculer en français, et nous lançons (enfin !) l'activité de sélection des stations de RER.

À ce stade, vous pouvez faire prononcer un texte donné en écrivant :

tts.speak("Un texte", TextToSpeech.QUEUE_FLUSH, null);

Par exemple, pour prononcer un texte quand on clique sur l'une des étapes d'un trajet calculé :

listStations.setOnItemClickListener(new OnItemClickListener() {

	@Override
	public void onItemClick(AdapterView<?> parent, View view,
			int position, long id) {

		tts.speak("Un texte", TextToSpeech.QUEUE_FLUSH, null);
	}
});

À ce stade, vous devez alors avoir obtenu une application conforme au cahier des charges de départ ! Félicitations !