Développement sous Android

Chapitres traités   

Android est une plateforme ouverte pour le développement de mobiles (smartphones). C'est la première plateforme pour appareils mobiles qui soit réellement ouverte et complète avec tout le logiciel nécessaire au fonctionnement d'un téléphone mobile mais sans les obstacles propriétaires qui ont entravé l'innovation sur ces derniers.

Le nom Android est le nom donné à la pile applicative conçue par Google et spécialement optimisée pour les appareils contraints. Les terminaux mobiles étant sévèrement contraints par leur faible puissance de calcul, une autonomie en énergie réduite, des mécanismes de saisie limités (surface d'affichage faible, interfaçage entre l'homme et la machine peu évolué, etc.) ou des capacités mémoires restreintes, Android s'avère parfaitement conçu pour cette utilisation.

Aujourd'hui, Android est principalement utilisé dans les téléphones portables. Ceci dit, la philosophie du système d'exploitation étant de s'abstraire au maximum du matériel sur lequel il tourne (type de processeur, taille d'écran, capteurs, etc.), il se retrouve dans des environnements totalement variés : les téléphones mobiles bien sûr, mais également les tablettes, les netbooks, les GPS, les lecteurs multimédias ou mêmes les télévisions, boxes internet et autres appareils mobiles.

Pour programmer des applications Android, vous devez au moins connaître les bases de Java. En effet, la programmation Android utilise la syntaxe de ce langage, plus une bibliothèque de classes s'apparentant à un sous-ensemble de la bibliothèque de Java SE (avec des extensions spécifiques). Si vous n'avez jamais programmé en Java, initiez-vous à ce langage avant de vous plonger dans la programmation Android.

 

Choix du chapitre Architecture générale d'Android

Contrairement à une idée reçue (probablement due à l'utilisation du terme abusif "Android" pour désigner le framework), cette pile logicielle n'est pas seulement composée d'applications et d'un framework de développement. Android est en réalité un ensemble de technologies, qui prennent leur départ depuis le système d'exploitation pour finir dans les applications disponibles pour les utilisateurs. Présenté simplement, Android est une combinaison de trois composants :

  1. Un système d'exploitation open-source libre pour les appareils mobiles.
  2. Une plateforme de développement open-source pour la création d'applications pour mobiles.
  3. Des équipements, en particulier des téléphones portables, exécutant le système d'exploitation Android ainsi que les applications développées pour lui.

Plus précisément, Android est constitué de plusieurs parties dépendantes et nécessaires.

  1. Un modèle de conception matérielle de référence décrivant les capacités nécessaires à un appareil mobile pour supporter la pile logicielle.
  2. Un système d'exploitation Linux assurant l'interface de bas-niveau avec le matériel, la gestion de la mémoire et le contrôle des processus, le tout optimisé pour les appareils mobiles.
  3. Des bibliothèques open-source pour le développement d'application, comme SQLite, Webkit, OpenGL ainsi qu'un gestionnaire de médias.
  4. Un moteur d'exécution et d'hébergement des applications d'Android incluant la machine virtuelle Dalvik et les bibliothèques de base fournissant les fonctionnalités spécifiques à Android. Ce moteur est conçu avec un soucis de compacité et d'efficacité pour un usage sur des appareils mobiles.
  5. Un framework applicatif qui expose de façon agnostique les services du système à la couche applicative, comme le Window Manager (gestionnaire de fenêtrage) et le Location Manager (gestionnaire de géolocalisation), les Content Providers (gestionnaires de contenu), la téléphonie et les capteurs.
  6. Un framework pour l'interface utilisateur utilisé pour héberger et lancer les applications.
  7. Des applications préinstallées et faisant partie de la pile.
  8. Un kit de développement logiciel utilisé pour créer les applications et comprenant des outils, des plugins et de la documentation.

Les applications

Android est fourni avec un ensemble de programmes de base (également nommés applications natives) permettant d'accéder à des fonctionnalités comme les courriels, les SMS, le téléphone, le calendrier, les photos, les cartes géographiques, le Web, etc.

Ces applications sont développées à l'aide du langage de programmation Java. Pour l'utilisateur final, c'est la seule couche accessible et visible. Il est bien sûr possible d'installer des applications tierces de provenance variées : Android Market, site Internet et, bien sûr, directement par le biais de votre ordinateur (sujet de notre étude).

La composition exacte des applications disponibles sur les nouveaux téléphones Android peut varier selon les fabriquants ou les opérateurs. Le fait qu'Android soit open-source signifie que les opérateurs peuvent personnaliser l'interface utilisateur et les applications fournies avec l'appareil Android.

Il est important de noter que, pour les appareils compatibles, la plateforme sous-jacente et le SDK restent cohérents entre les variations des fabriquants ou des opérateurs. L'apparence de l'interface utilisateur peut varier mais vos applications fonctionnereont de la même façon sur tous les appareils Android.

Les bibliothèques

En interne, Android inclut un ensemble de bibliothèques C et C++ utilisées par de nombreux composants de la plateforme Android. Ces bibliothèques sont en réalité accessibles au développeur par l'intermédiaire de framework Android.

En effet, le framework Android effectue, de façon interne, des appels à des fonctions C/C++ beaucoup plus rapides à exécuter que des méthodes Java standard. La technologie Java Native Interface (JNI) permet d'effectuer des échanges entre le code Java et le code C et C++.

Le framework et le SDK d'Android

Android permet aux développeur de créer leurs propres applications. Pour ce faire; les programmeurs ont accès aux mêmes API que celles utilisées par les applications natives d'Android. C'est à ce niveau de l'architecture que nous allons travailler lors de cette étude. L'intérêt principal d'Android comme environnement de développement réside dans les API fournies.

En tant que plateforme neutre, Android vous donne la possibilité de créer des applications qui seront intégrées au téléphone autant que celles fournies nativement. La liste qui suit met en lumière les fonctionnalités d'Android les plus notables.

  1. Aucun frais de licence, distribution ou développement et aucun processus d'approbation préalable à la distribution d'applications.
  2. Accès au matériel WI-FI.
  3. Réseaux GSM, EDGE et 3G pour la téléphonie et le transfert de données, vous permettant de passer ou de recevoir des appels ou des SMS et d'envoyer ou de recevoir des données sur les réseaux mobiles.
  4. API complètes pour les services de géolocalisation comme le GPS.
  5. Contrôle complet du matériel multimédia, y compris la lecture et l'enregistrement vidéo ou sonore.
  6. API pour l'utilisation des capteurs comme l'accéléromètre et la boussole.
  7. Bibliothèque Bluetooth pour le transfert de données local.
  8. Passage de message par IPC.
  9. Partage des données stockées.
  10. Applications et processus en arrière-plan.
  11. Widgets d'écran d'accueil, Live Folders (dossiers dynamiques) et Live Wallpaper (papier peint dynamique).
  12. Possibilté d'intégrer les résultats de recherche d'une application dans le système de recherche.
  13. Nagigateur open-source HTML5 basé sur Webkit.
  14. Support complet des applications intégrant une interaction utilisateur par le biais de cartes géographiques.
  15. Graphiques s'appuyant sur l'accélération matérielle et optimisés pour les mobiles, incluant une bibliothèque de graphisme 2D ainsi que le support des graphismes 3D avec OpenGL ES 2.0.
  16. Bibliothèques multimédias pour l'exécution et l'enregistrement de différents formats audio et vidéo ou d'images fixes.
  17. Framework applicatif encourageant la réutilisation de composants et le remplacement des applications natives.

Accès au matériel, y compris appareil photo, GPS et accéléromètre

Android comprend des bibliothèques d'API simplifiant le développement relatif aux composants matériels. Ceci grantit que vous n'aurez pas besoin de créer des implémentations spécifiques de votre logiciel pour des appariels différents et que vous pourrez donc créer des applications Android fonctionnant sur n'importe quel appareil supportant la pile logicielle Android.

Le SDK Android inclut des API pour le matériel de géolocalisation (comme le GPS), l'appareil photo, les composants audio, les connexions réseau, le WI-FI, Bluetooth, les accéléromètres, l'écran tactile et la gestion de l'alimentation.

Service natifs Google Maps de géocodage et géolocalisation

Le support natif des cartes permet de créer un éventail d'applications s'appuyant sur la mobilité des appareils Android. Android permet la création d'activités incluant des cartes interactives Google au sein de l'interface utilisateur avec un accès complet à ces cartes, contrôlables par programme et supportant les annotations grâce aux bibliothèques graphiques complètes d'Android.

Les services de géolocalisation Android gèrent des technologies comme le GPS ou la technologie GSM cellulaire Google pour déterminer la position courante de l'appareil. Ces services s'abstraient de la technologie spécifique de localisation plutôt que choisir une technologie particulière. Ils impliquent également que vos applications basées sur la géolocalisation fonctionneront quelle que soit la technologie supportée par le télephone.

Afin de combiner cartes et géolocalisation, Android inclut une API de géocodage et de géocodage inverse permettant de déterminer des coordonnées géographiques à partir d'une adresse et inversement.

Services d'arriere-plan

Android supporte les applications et services s'exécutant de façon invisible en tâche de fond. Les appareils modernes sont par nature des appareils multifonctions. Cependant, leurs écrans de taille limités font qu'une seule application peut généralement être visible à un moment donné.

Les services d'arrière-plan permettent de créer des composants applicatifs invisibles exécutant des traitements automatiques sans action de l'utilisateur. L'exécution en tâche de fond autorise vos applications à être pilotées par des événements et à supporter des mises à jour régulières, ce qui est parfait pour surveiller des scores de jeux, pour générer des alertes de géolocalisation ou pour donner la priorité à des appels entrants ou à des SMS afin de les visualiser.

Base de données SQLite pour le stockage et l'extraction de données

Un stockage et une extraction de données rapides et efficace sont essentiels sur des appareils dont la capacité de stockage est limitée du fait de leur nature compacte.

Android fournit une base de données relationnelle légère à chaque application utilisant SQLite. Vos applications peuvent tirer profit de ce moteur de base de données pour stocker des données de manière sûre et efficace.

Par défaut, la base de données de chaque application est confinée, ce qui signifie que son contenu n'est accessible qu'à l'application qui l'a créée, mais les Content Providers fournissent un mécanisme permettant de gérer le partage de cette base.

Données partagées et communication interapplications

Android inclut trois techniques permettant de transmettre des informations depuis vos applicatiosn afin de les utiliser ailleurs : les Notifications, les Intents et les Content Providers.

  1. Les Notifications sont les moyen standard par lesquels un appareil mobile alerte traditionnellement les utilisateurs. A l'aide de l'API, vous pourrez déclencher des alertes sonores, provoquer une vibration ou faire clignoter la diode de l'appareil ainsi que contrôler les icônes de notification de la barre de statut.
  2. Les Intents fournissent un mécanisme de passage de message dans et entre des applications. Vous pourrez à l'aide d'Intents diffuser l'action souhaitée (émission d'un appel ou l'édition d'un contact par exemple) au sein du système afin que d'autres applicationsla prennent en charge.
  3. Les Content providers donnent un accès contrôlé aux bases de données privées de vos applications. Les données stockées par les applications natives, comme le gestionnaire de contacts, sont exposées comme des Content providers et vous pouvez donc créer vos propres applications pour lire et modifier ces données.

Utilisation des widgets, des Live Folders et du Live Wallpaper pour améliorer l'écran d'accueil

Les widgets, les Live Folders et le Live Wallpaper permettent de créer des composants applicatifs dynamiques ouvrant une fenêtre vers vos applications ou offrant une information utile et opportune directement sur l'écran d'accueil.

Si vous donnez la possibilité aux utilisateurs d'interagir avec vos applications directement depuis l'écran d'accueil, ils pourront accéder instantanément à des informations intéressantes sans pour autant devoir ouvrir une application qui sera ainsi accessible par un raccourci dynamique.

Support multimédia étendu et graphiques 2D/3D

De pus grands écrans ainsi que des affichages lumineux et en haute résolution ont permis de transformer les mobiles en dispositifs multimédias. Pour vous permettre de tirer parti au maximum du matériel disponible, Android fournit des bibliothèques graphiques pour les dessins 2D ainsi que les graphiques 3D avec OpenGL.

Android offre également des bibliothèques complètes de gestion des images fixes, des vidéos et des fichiers audio, y compris les formats MPEG4, H.264, MP3, AAC, AMR, JPG, PNG et GIF.

Mémoire optimisée et gestion des processus

La gestion de la mémoire et des processus sous Android est quelque peu inhabituelle. Tout comme Java et .NET, Android utilise son propre moteur d'exécution et sa propre machine virtuelle pour gérer la mémoire d'une application. Mais contrairement à ces deux framework, le moteur d'exécution d'Android gère également les durées de vie des processus.

Android garantit la réactivité des applications en stoppant et en tuant les processus lorsqu'il est nécessaire de libérer des ressources pour les applications de priorité plus élevée.

Dans ce contexte, la plus haute priorité est donnée à l'application avec laquelle l'utilisateur est en train d'interagir. S'assurer que vos applications sont prêtes pour une fin d'exécution rapide tout en restant capable de réagir, et les mettre à jour ou les redémarrer en arrière-plan si nécessaire, est à prendre attentivement en compte dans un environnement qui ne permet pas à celles-ci de contrôler leur propre durée de vie.

Le moteur d'exécution d'Android - La machine virtuelle Dalvik

Chaque application Android tourne dans son propre processus et avec sa propre instance de machine virtuelle Dalvik. Dalvik est une implémentation de machine virtuelle ayant été conçue pour optimiser l'exécution multiple de machines virtuelles. Elle exécute du bytecode qui lui est dédié : le bytecode dex.

Cette particularité d'Android en fait un système unique, loin des systèmes Linux traditionnels que beaucoup avaient pu rencontrer auparavant. L'un des éléments clés d'Android est constitué par cette machine virtuelle Dalvik. Au lieu d'utiliser une JVM classique comme Java ME (Java Mobile Edition), Android utilise sa propre VM spécifique, conçue pour garantir que des instances multiples seront efficacement exécutées sur un appareil.

La machine Dalvik utilise le noyau Linux sous-jacent de l'appareil pour gérer les fonctionnalités de bas-niveau y compris la sécurité, le threading et la gestion de processus et de la mémoire.

Il est également possible d'écrire des applications en C/C++ exécutées directement par le système d'exploitation Linux. Bien que vous puissiez le faire, ce ne sera pas nécessaire dans la plupart des cas. Si la vitesse et l'efficacité de C/C++ sont requises pour votre application, Android fournit maintenant un kit de développement natif (NDK). Ce NDK est conçu pour vous permettre de créer des bibliothèques C++ à l'aide des bibliothèques libc et libm et vus donner un accès natif à OpenGL.

Tous les matériels et services système Android sont gérés à l'aide de Dalvik. En utilisant une machine virtuelle pour exécuter une application, les développeurs ont accès à une couche d'abstraction qui garantit qu'ils n'auront jamais à se préoccuper d'une implémentation matérielle particulière.

La machine virtuelle Dalvik exécute des fichiers exécutables Dalvik, un format optimisé pour garantir une empreinte mémoire minimale. Vous créerez des exécutables .dex en transformant des classes Java compilées à l'aide des outils fournis dans le SDK.

 

Choix du chapitre Outils de développement

Après toutes ces considérations théoriques, je vous propose maintenant de connaître tous les outils nécessaires à l'élaboration des différents projets que nous allons mettre en oeuvre sous Android. Pour réaliser tous ces projets vous avez besoin :

  1. SDK Android : http://developer.android.com/sdk/index.html : Ce SDK inclut toutes les bibliothèques Android, la documentation complète ainsi que d'excellents exemples d'applications. Il inclut également des outils d'aide à l'écriture et au débogage d'applications comme L'Android Emulator pour exécuter vos projets et le DDMS (Dalvik Debug Monitoring Service) pour vous aider à les déboguer.
  2. JDK Java SE : http://java.sun.com/javase/downloads/index.jsp : Le code Android est écrit en Java et les bibliothèques Android incluent la plupart des caractéristiques des API de Java de base. Avant qu'ils ne puissent s'exécuter, vos projets devront cependant être traduits en bytecode Dalvik. Vous profitez donc des bénéfices de Java, alors que vos applications sont exécutées par une machine virtuelle optimisée pour les appareils mobiles.
  3. Netbeans : http://netbeans.org/downloads/index.html : A moins que vous ne soyez masochiste, vous utiliserez un IDE Java pour simplifier vos développements. C'est très souvent Eclipse qui est sité en premier lieu. Pour ma part, je préfère largement utiliser Netbeans.
  4. Plugin Netbeans : http://kenai.com/projects/nbandroid/downloads : Le plugin nbandroid pour Netbeans simplifie vos développements Android en intégrant les outils de développement y compris l'émulateur et le convertisseur .class-to-.dex directement dans l'IDE. Bien que le plugin nbandroid ne soit pas indispensable, il simplifie et accélère la création, les tests et le débogage de vos applications. Si vous n'avez pas Internet, voici la version 1.7 de ce plugin.

Les applications Android étant exécutées par la machine virtuelle Dalvik, vous pourrez les écrire sur n'importe quelle plateforme supportant les outils de développement, c'est-à-dire : Linux, Mac OS X et Windows.

Installation du SDK

Le SDK Android est complètement ouvert. Il n'y a aucun frais de téléchragement ou d'utilisation des API et Google ne facture ni ne contrôle la distribution de vos programmes sur Android Marquet.

Le SDK se présente sous la forme d'un fichier ZIP contenant uniquement la dernière version des outils de développement Android. Pour Windows, il est maintenant préférable de télécharger directement l'installateur avec l'extension .exe. Pour les autres plateformes, installez-le en décompressant le fichier dans un nouveau dossier. Notez l'emplacement, vous en aurez besoin plus tard.

Afin de gérer au mieux la variété des versions des systèmes d'exploitation, de SDK et de terminaux, un outil, Android SDK and AVD Manager, a été mis en place. Ce petit utilitaire permet de récupérer les différentes entités nécessaires au développement sur Android :

Effectivement, avant de pouvoir commencer à développer, vous devez ajouter au moins une plateforme SDK grâce à ce petit utilitaire. Exécutez sous Windows l'exécutable SDK Setup ou sous Mac OS ou Linux l'exécutable android, situé dans le sous-dossier tools. Sélectionnez dans la partie gauche Available packages puis dans la partie droit Sites, packages and archives les versions de plateforme SDK que vous suohitez installer.

Les plateformes sélectionnées seront téléchargées dans le dossier d'installation de votre SDK. Elles contiendront les bibliothèques d'API, la documentation et plusieurs exemples d'applications.

Android Virtual Device et Emulator Android

L'AVD (Android Virtual Device) est utilisé pour simuler les versions de logiciel et les spécifications de matériels disponibles sur différents appareils. Vous pouvez ainsi tester votre application sur plusieurs plateformes matérielles sans avoir à acheter les téléphones correspondants.

Le SDK Android n'inclut aucun appareil virtuel préconfiguré et vous devrez en créer au moins un avant de pouvoir acheter les téléphones correspondant.

Nous l'avons déjà vu, Android cible un très large ensemble de terminaux. Afin de gérer au mieux cette diversité, Google a développé un système permettant d'émuler un large panel de terminaux. Un AVD est, en réalité, un terminal virtuel disposant de plusieurs propriétés qui lui sont propres : taille d'écran, version du système, présence ou non d'une carte SD, etc.

Lorsque l'émulateur démarre, il utilise un AVD et s'adapte aux différentes propriétés de ce dernier. Lorsque nous débutons le développement sur Android, il peut être nécessaire de créer plusieurs AVD (notamment pour les développeurs ne disposant pas de terminal Android.

L'émulateur est le parfait outil de test et de débogage de vos applications. Il est une implémentation de la machine virtuelle Dalvik, faisant de celle-ci une plateforme exécutant les applications Android comme le ferait n'importe quel téléphone Android. Etant découplé de tout matériel, c'est une excellente base de test de vos applications.

L'émulateur n'implémente pas encore toutes les caractéristiques des matériels mobiles supportées par Android. Ne sont pas disponibles : l'appareil photo, le vibreur, les diodes, les appels téléphoniques réels, l'accéléromètre, les connexions USB, l'enregistrement audio et le niveau de charge de la batterie.

Installation du plugin de Netbeans

Pour développer nos applications Android, nous allons nous servir de l'IDE Netbeans. Afin que cet environnement de développement soit capable de générer automatiquement des projets pour Android, il est nécessaire d'installer le plugin correspondant, nommé nbandroid.

Ce plugin est en rélité une archive ZIP qu'il suffit de décompresser à l'endroit voulu. Cette archive dispose d'un certain nombre de fichiers de type nbm qui sont interprété par Netbeans comme un ensemble de plugins :

  1. Pour ajouter le plugin nbandroid, il suffit de sélectionner la rubrique Plug-ins du menu Outils :

  2. Dans la boîte de dialogue correspondante, choisissez l'onglet Téléchargés et cliquez sur le bouton Ajouter des Plug-ins. Spécifiez ensuite le répertoire où se situe la décompression de l'archive nbandroid-1.7.zip :

Création et lancement d'un projet Android sous Netbeans

Une fois que le plugin est correctement installé, vous pouvez maintenant construire de nouveaux projets prévus spécifiquement pour Android et de les exécuter directement sur le vrai téléphone ou avec l'émulateur en choisissant l'AVD qui vous intéresse. Voici la procédure à suivre :

  1. Cliquez sur l'icône habituelle correspondant à la création d'un nouveau projet et sélectionnez le type de projet :

  2. Dans la deuxième étape de cette boîte de dialogue, vous devez spécifier le nom de votre projet, de l'activité (voir au chapitre suivant), la version du système d'exploitation utilisé par votre mobile.

    Attention, le nom du paquetage est particulier puisqu'il correspond à l'identifiant de votre application. Cet identifiant doit être unique, notamment si vous souhaitez envoyer votre application sur l'Android Marquet. Il est de coutume de représenter le paquetage de base sous la forme code_pays.nom_de_société.nom_du_produit.

  3. Il suffit de compléter le code source de votre activité, de générer vos layout, etc.

  4. Une fois que votre projet est constitué, vous pouvez l'exécuter soit directement sur votre mobile ou de prendre la version émulée :

  5. Pour que votre mobile accepte le déploiement de votre application, il faut le régler en conséquence. Pour cela, dans le menu Paramètres, sélectionner successivement Applications suivi de Développement et cocher ensuite toutes les cases concernées.

 

Choix du chapitre Contenu d'un programme Android

Le développeur d'une application classique est "le seul maître à bord". Il peut ouvrir la fenêtre principale de son programme, ses fenêtres filles, les boîtes de dialogue, comme il le souhaite. De son point de vue, il est seul au monde ; il tire parti des fonctionnalités du système d'exploitation mais ne s'occupe pas des autres programmes susceptibles de s'exécuter en même temps que son programme. S'il doit interagir avec d'autres applications, il passe généralement par une API, comme JDBC ou via des sockets.

Android utilise les mêmes concepts, mais proposés de façon différente, avec une structure permettant de mieux protéger le fonctionnement des téléphones. Les applications Android sont constituées de composant à fort couplage, liés par un manifeste d'applications qui décrit chacun d'eux et comment ils interagissent ainsi que les métadonnées de l'application, y compris ses exigences en matière de plateforme et de matériel. Les septs composants suivants sont les briques de vos applications :

  1. Les activités : Activites : Ce sont les briques de base de l'interface utilisateur. Vous pouvez considérer une activité comme l'équivalent Android de la fenêtre ou de la boîte de dialogue d'une application classique. Bien que les activités puissent ne pas avoir d'interface utilisateur, un code "invisible" sera délivré le plus souvent sous la forme de fournisseur de contenus (content provider) ou de service.

    C'est la généralement la couche présentation de votre application. Chaque écran est une extension de la classe Activity. Les activités utilisent les Views pour former les interfaces utilisateur graphiques qui afficheront l'information et répondront aux actions.

  2. Les fournisseurs de contenu : Content Providers : Ils offrent un niveau d'abstraction pour toutes les données stockées sur le terminal et accessibles aux différentes applications.

    Sources de données partageable. Les fournisseurs de contenu servent à gérer et à partager les bases de données des applications. Ils sont le moyen privilégié de partager des données entre applications. Cela signifie que vous pouvez configurer vos propres fournisseurs de contenu pour autoriser l'accès à d'autres applications et utiliser ceux qu'elles exposent. Les appareils Android comptent plusieurs fournisseurs de contenu natifs qui exposent les bases de données très utiles comme les médias ou les contacts.

  3. Les services : Les activités et les fournisseurs de contenus ont une durée de vie limitée et peuvent être éteints à tout moment. Les services sont en revanche conçus pour durer et, si nécessaire, indépendamment de toute activité. Vous pouvez, par exemple, utiliser un service pour vérifier les mises à jour d'un flux RSS ou pour jouer de la musique, même si l'activité de contrôle n'est plus en cours d'exécution.

    Travailleurs de l'ombre. Ces composants tournent en arrière-plan, mettent à jour vos sources de données ainsi que les activités visibles et déclenchent des notifications. Ils servent à exécuter des traitements continus même lorsque les activités de vos applications ne sont pas actives ou visibles.

  4. Les intensions : Intents : Ce sont des messages système émis par le terminal pour prévénir les applications de la survenue de différents événements, que ce soit une modification matérielle (comme l'insertion d'une carte SD) ou l'arrivée de données (telle la réception d'un SMS), en passant par les événements des applications elles-mêmes (votre activité a été lancée à partir du menu principal du terminal, par exemple). Vous pouvez non seulement répondre aux intensions, mais également créer les vôtres afin de lancer d'autres activités ou pour vous prévenir qu'une situation particulière a lieu (vous pouvez, par exemple, émettre l'intension X lorsque l'utilisateur est à moins de 100 mètres d'un emplacement Y).

    Framework de communication interapplications. Les intensions servent à diffuser des messages au sein du système ou à destination d'une activité ou d'un service cible, indiquant l'intension d'exécuter une action. Le système détermine alors la ou les cibles qui effectuerons l'action.

  5. Les consommateurs de messages : Broadcast Receivers : Consommateurs des messages diffusés par les intensions. Si vous créez et enregistrer un Broadcast Receiver, votre application pourra écouter les diffusions des intensions répondant à certains critères spécifiques de filtre. Les consommateurs de messages démarreront automatiquement votre application pour répondre à une intension entrant et sont donc parfait pour créer des applications basées sur des événements.
  6. Composants d'application visuels : Widgets : Les composants d'applications visuels peuvent ajoutés à l'écran d'accueil. Les widgets, qui sont une variant particulière des consommateurs de messages, permettent de créer des composants interactifs et dynamiques incorporables dans l'écran d'accueil.
  7. Les notifications : Framework de notifications aux utilisateurs. Les notifications permettent d'envoyer un signal aux utilisateurs sans dérober le focus ni interrompre leurs activités en cours. Elles sont le moyen privilégié d'attirer l'attention de l'uilisateur à partir d'un service ou d'un consommateur de messages. Lorsqu'un appareil reçoit par exemple un message texte ou un appel entrant, il vous alerte en clignotant, en émettant un son, en affichant une icône ou un message. Vous pouvez déclencher les mêmes événements depuis vos applications à l'aide des notifications.

En découplant les composants applicatifs, vous pourrez par exemple partager et échanger les fournisseurs de contenu, les services et mêmes les activitées avec d'autres applications, les vôtres comme celles de tiers.

Permissions

Certaines opérations sont réalisables à condition d'en obtenir la permission. Ces actions sont de plusieurs formes :

  1. opérations pouvant entraîner un surcoût (connexion, échnage de données, envoi de SMS par exemple) ;
  2. utilisation de données personnelles (accès à vos contacts, à votre compte Google, exploitation de vos informations linguistiques entre autres) ;
  3. accès au matériel du téléphone (prise de clichés, écriture sur la carte mémoire ... ).

Si vous utilisez les fonctionnalités liées à de telles permissions, vous devrez déclarer leur utilisationdans le fichier de configuration qui décrit l'application. A l'installation de votre application, l'utilisateur disposera d'un récapitulatif de toutes les permissions demandées poiur que l'application fonctionne. Il pourra alors choisir de continuer ou d'interrompre l'installation en connaissance de cause.

 

Choix du chapitre Création et structure d'un projet Android

Après toutes ces considérations techniques, nous allons maintenant rentrer dans le vif du sujet et développer une application Android. Bien entendu, pour notre première application, nous allons réaliser une activité rudimentaire qui consiste à afficher un simple bouton dont l'intitulé change après chaque clic et qui indique l'heure actuelle du jour.

Structure d'un projet

Le système de construction d'un programme Android est organisé sous la forme d'une arborescence de répertoires associé au projet, exactement comme n'importe quel projet Java. Les détails, cependant, sont spécifiques à Android. Lorsque vous créez un projet Android, plusieurs éléments sont placés dans le répertoire racine du projet :

  1. AndroidManifest.xml : est un fichier XML qui décrit l'application à construire et les composants la constituant - activités, services, etc.
  2. build.xml : est un script Ant permettant de compiler l'application et de l'installer sur le terminal.
  3. default.properties et local.properties : sont les fichiers de propriétés utilisés durant la phase de compilation en respectant le script précédent.
  4. assets : répertoire qui contient toutes les ressources brutes (raw bytes) ne nécessitant aucun traitement par Android. A la différence des ressources placées dans le répertoire res, les ressources brutes seront accessibles grâce à un flux de données et non grâce à la classe R décrite plus loin.
  5. bin : répertoire qui contient l'ensemble de l'application compilée.
  6. gen : répertoire qui contient les sources automatiquement produit par les outils de compilation d'Android afin d'assister le développeur. Si vous supprimer un fichier dans ce répertoire, les outils d'Android s'empresseront aussitôt de le régénérer.

    Attention, si le projet contient des erreurs ne permettant pas de compilation, aucun fichier ne sera généré. Par conséquent, si effectivement vous ne trouvez pas pas la bonne version de fichier ou s'il n'est pas généré automatiquement, vérifiez bien que l'ensemble du projet compile correctement.

  7. libs : répertoire qui contient les bibliothèques tierces qui sont nécessaires à l'application.
  8. src : répertoire qui contient l'ensemble des sources du projet. C'est essentiellement dans ce répertoire que vous allez ajouter et modifier le code source de l'application.
  9. res : répertoire qui contient les ressources telles que les images, les icônes, les dispositions de l'interface graphique (layouts), etc. Ces ressources seront assemblées avec le code Java compilé et seront accessibles au moyen d'une classe R également autogénérée.

Tout cela parait bien compliqué, mais dans la pratique, c'est très simple. Nous allons nous renseigner progressivement sur chacun de ces points. Sachez toutefois que grâce à Netbeans et son plugin pour Android, tous ces éléments sont automatiquement générés avec des valeurs par défaut. Dans notre premier projet, seule l'activité est à modifier.

Nous allons mettre en place notre projet à l'aide de Netbeans. Nous nommons ce projet Heure, sachant que vous devez spécifier le paquetage suivant les critères que nous avons évoqué plus haut, donc ici fr.manu.heure.

Lorsque vous créez un nouveau projet, vous constaterez alors que l'arborescence src contient le hiérarchie des répertoires définis par le paquetage ainsi que le squelette de la classe que vous avez choisi, qui est une sous-classe d'Activity représentant l'activité principale de votre application, comme ci-dessous :

(exemple de squelette) fr.manu.heure.NouvelleActivité
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;

public class NouvelleActivité extends Activity {
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      // ToDo add your GUI initialization code here        
   }
}

Vous pouvez bien sûr modifier ce fichier, comme nous l'avons fait dans l'image globale du projet, et même en ajouter d'autres à l'arborescence src selon les besoins de votre application.

La première fois que vous compilerez le projet, la chaîne de production d'Android créera automatiquement le fichier R.java dans le paquetage de l'activité "principale". Ce fichier contient un certain nombre de définitions de constantes liées aux différentes ressources mise en oeuvre dans l'arborescence res.

fr.manu.heure.R.java
/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package fr.manu.heure;

public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class layout {
        public static final int main=0x7f030000;
    }
    public static final class string {
        public static final int app_name=0x7f040000;
    }
}

Lors de nos différents projets, nous rencontrerons de nombreuses références à cette classe particulière. Par exemple, nous désignerons l'identifiant de la couche présentation principale (la vue) d'un projet à l'aide de l'écriture suivant : R.layout.main. Attention, comme cela est précisé dans le source, ne modifier pas R.java vous même, laissez les outils d'Android le faire pour vous.

Les ressources

Comme nous l'avons déjà indiqué, le répertoire res contient les ressources, c'est-à-dire des fichiers statiques fournis avec l'application, soit sous forme initiale soit, parfois, sous une forme prétraitée. Parmi les sous-répertoire de res, citons :

  1. res/drawable : ce dossier contient l'ensemble des ressources graphiques mise en oeuvre dans le cadre de votre projet, des images (PNG, JPEG, etc.) ou des vidéos.
  2. res/layout : ce dossier contient les fichiers décrivant la composition des interfaces (les vues) de l'application, décrites en format XML .
  3. res/menu : ce dossier contient la description XML des menus de l'application.
  4. res/raw : ce dossier contient les ressources autres que celles décrites ci-dessus qui seront empaquetées sans aucun traitement spécifique.
  5. res/values : ce dossier contient des messages, des dimensions, les couleurs, etc. prédéfinis qui sont très facile de modifier par la suite. Cela permet de découpler le code principal des différents réglages et interventions utltérieures possibles.
  6. res/xml : ce dossier contient les autres fichiers XML généraux que vous souhaitez soumettre.

Nous présenterons l'utilité de ces différents répertoires, et bien d'autres, dans la suite de cette étude. Ces différentes ressources sont pris en compte grâce à la classe R que nous venons de décrire au moyen d'une référence particulière à chacune d'entre elle.

Résultat de la compilation

Lorsque vous compilez un projet, le résultat est placé dans le répertoire bin, sous la racine de l'arborescence du projet :

  1. bin/classes : contient toutes les classes Java compilées de votre application.
  2. bin/classes.dex : contient l'exécutable créé à partir de ces classes compilées. C'est cet exécutable qui est lancé à partir de la machine virtuelle Dalvik.
  3. bin/votre_application.ap_ : contient les ressources complètes de votre application, sous la forme d'un fichier ZIP.
  4. bin/votre_application-debug.apk ou bin/votre_application-unsigned.apk : est la véritable application Android déployable sur votre mobile.

    Le fichier .apk est une archive ZIP contenant le fichier .dex, la version compilée des ressources (resources.arsc), les éventuelles ressources non compilées (celles qui se trouvent sous res/raw, par exemple) et le fichier AndroidManifest.xml. Cette archive est signée : la partie -debug du nom du fichier indique qu'elle l'a été à l'aide d'une clé de débogage qui fonctionne avec l'émulateur alors que -unsigned précise que l'application a été construite pour être déployée : l'archive APK doit alors être signée à l'aide de jarsigner et d'une clé officielle.

Constitution d'une application Android

Nous venons de passer pas mal de temps sur la structure d'un projet Android découvert notamment au travers de Netbeans. Nous allons définir maintenant et résumer une partie de nos connaissances sur ce qu'est une application Android.

Une application Android est un assemblage de composants liés grâce à un fichier de configuration. Nous allons découvrir chaque pièce de notre puzzle applicatif, comment le fichier de configuration de l'application la décrit et comment toutes les pièces interagissent entre elles. Avant de rentrer dans le détail d'une application Android et de son projet associé, nous allons revenir sur différents concepts fondamentaux :

  1. Les activités : une activité peut être assimilé à un écran structuré par un ensemble de vues et de contrôles composant son interface de façon logique ; elle est composée d'une hiérarchie de vues contenant elles-mêmes d'autres vues. Une activité est, par exemple, un formulaire d'ajout de contacts ou encore un plan Google Maps sur lequel vous ajoutez de l'information. Une application comportant plusieurs écrans, possèdera donc autant d'activités.
  2. Les vues (et leur mise en page) : Les vues sont les éléments de l'interface graphique que l'utilisateur voit et sur lesquels il pourra agir. Les vues contiennent des composants, organisés selon diverses mises en page (les uns à la suite des autres, en grille ...).
  3. Les contrôles : les contrôles (boutons, champs de saisie, case à cocher, etc.) sont eux-même un sous-ensemble des vues. Ils ont besoin d'accéder aux textes et aux images qu'ils affichent (par exemple, un bouton représentant un téléphone aura besoin de l'image du téléphone correspondante). Ces textes et ces images seront puisés dans les fichiers ressources de l'application.
  4. Les ressources : Comme nous l'avons déjà indiqué, le répertoire res contient les ressources, c'est-à-dire des fichiers statiques fournis avec l'application.
  5. Le fichier de configuration appelé également manifeste : A côté de tous ces éléments, se trouve un fichier XML qui sert à la configuration de l'application. Ce fichier est indispensable à chaque application qui décrit : le point d'entrée de votre application (quel code doit être exécuté au démarrage de l'application), quels composants constituent ce programme, les permissions nécessaires à l'exécution du programme (accès à Internet, accès à l'appareil photo, etc.).

Contenu du manifeste

Le point de départ de toute application Android est son fichier manifeste, AndroidManifeste.xml, qui se trouve directement dans la racine du projet. C'est dans ce fichier que nous déclarons ce que contiendra l'application - les activités, les services, etc.

Nous y distinguons également la façon dont ces composants seront reliés au système Android lui-même en précisant, par exemple, l'activité (ou les activités) qui doivent apparaître dans le menu principal du terminal. Ce menu est également appelé "lanceur" (launcher).

La création d'une application produit automatiquement un manifeste de base. Pour une application simple, qui offre uniquement une seule activité, ce manifeste automatique conviendra sûrement ou nécessitera éventuellement quelques modifications mineures.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.manu.heure"
      android:versionCode="1"
      android:versionName="1.0">

    <application android:label="@string/app_name" android:icon="@drawable/icon">
        <activity android:name="MainActivity"
                         android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>   
  1. Tous les fichiers manifestes ont pour racine <manifest>. Comme d'habitude, cet élément comprend la déclaration d'un espace de nom, ici, logiquement, android.
  2. L'essentiel des informations que vous devez fournir à cet élément est l'attribut package, ici fr.manu.heure. C'est là que vous pouvez indiquer le nom du paquetage Java qui sera considéré comme la base de votre application. Ce qui est intéressant par cette démarche, c'est que dans la suite de votre fichier, vous pourrez alors simplement utiliser le symbole point pour désigner ce paquetage : si vous devez par exemple faire référence à fr.manu.heure.Heure dans le manifeste, il suffira d'écrire .Heure puisque fr.manu.heure est défini comme le paquetage de l'application.
  3. Sous l'élément <manifest>, vous trouverez les éléments suivants :
    • <uses-permission> indiquant les permissions dont a besoin votre application pour fonctionner correctement :
      <uses-permission android:name="android.permission.ACCESS_GPS" />
    • <permission> déclarant les permissions que les activités ou les services peuvent exiger des autres appplications pour utiliser les données et le code de l'application.
    • <instrumentation> qui indique le code qui devrait être appelé pour les événements systèmes essentiels, comme le lancement des activités. Ces éléments sont utilisés pour la journalisation ou la surveillance.
    • <uses-library> pour relier les composants facultatifs d'Android, comme les services de géolocalisation.
    • <uses-sdk> indiquant la version du SDK Android avec laquelle a été construite l'application.
    • <application> qui définit le coeur de l'application décrite par le manifeste.

Le plat de résistance du fichier manifeste est décrit par le fils de l'élément <application>. Par défaut, la création d'un nouveau projet Android n'indique qu'un seul élément <activity>.

  1. Cet élément <activity> fournit l'attribut android:name pour la classe qui implémente l'activité, l'attribut android:label pour le nom affiché de l'activité et souvent, un élément fils <indent-filter> décrivant les conditions sous lesquelles cette activité s'affichera.
  2. L'élément <activity> de base configure votre activité pour qu'elle apparaisse dans le lanceur sous forme d'une icône afin que les utilisateurs puissent l'exécuter simplement. Comme nous le verrons plus tard, un même projet peut définir plusieurs activités.
  3. Il peut y avoir plusieurs éléments <provider> indiquant les fournisseurs de contenus (content providers) - les composants qui fournissent les données à vos activités et, avec votre permission, aux activités d'autres applications du terminal. Ces éléments encapsulent les bases de données ou les autres stockages de données en une API unique que toute application peut ensuite utiliser.
  4. Enfin, il peut y avoir un ou plusieurs éléments <service> décrivant les services, c'est-à-dire les parties de code qui peuvent fonctionner indépendamment de toute activité et en permanence. L'exemple classique est celui du lecteur MP3, qui permet de continuer à écouter de la musique, même si l'utilisateur ouvre d'autres activités et que l'interface utilisateur n'est pas affichée au premier plan (sujet traité ultérieurement).

Android, comme la plupart des systèmes d'exploitation, est régulièrement amélioré, ce qui donne lieu à de nouvelles versions du système. Certains de ces changements affectent le SDK car de nouveaux paramètres, classes ou méthodes apparaissent, qui n'existaient pas dans les versions précédentes.

  1. Pour être sûr que votre application ne s'exécutera que pour une version précise (ou supérieure) d'un environnement Android, ajoutez un élément <uses-sdk> comme fils de l'élément racine <manifest> du fichier AndroidManifest.xml. <uses-sdk> dispose de l'attribut minSdkVersion, indiquant la version minimale du SDK exigée par votre application.
  2. Si vous omettez <uses-sdk>, cela revient à annoncer que votre application fonctionnera sur toutes les versions existantes du SDK et vous devrez évidemment tester que c'est bien le cas.

Si vous compter distribuer votre application via la boutique Android ou d'autres supports, vous devrez sûrement ajouter les attributs android:versionCode et android:versionName à l'élément <manifest> afin de faciliter le processus de mise à jour des applications.

  1. android:versionName : est une chaîne de caractères lisible représentant le nom ou le numéro de version de votre application. Vous pouvez, par exemple, utiliser des valeurs comme "3.0", "System V", "5000" ou "3.1" en fonction de vos préférences.
  2. android:versionCode : est un entier censé représenter le numéro de version de l'application. Le système l'utilise pour savoir si votre version est la plus récente qu'une autre - "plus récent" étant défini par "la valeur d'android:versionCode est plus élévée". Pour obtenir cette valeur, vous pouvez convertir le contenu d'android:versionName en nombre ou simplement incrémenter la valeur à chaque nouvelle version.

Après toutes ces petites considérations techniques, voici les petites modifications que nous pouvons éventuellement apporter sur le manifeste de base généré automatiquement par Netbeans :

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.manu.heure"
      android:versionCode="1"
      android:versionName="1.0">
         
    <uses-sdk android:minSdkVersion="2" />
    <application android:label="Heure" android:icon="@drawable/icon">
        <activity android:name=".Heure"  android:label="Heure">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
Qu'est-ce qu'une activité Android ?

Nous avons passé pas mal de temps à décrire toute l'infrastructure d'une application Android. Nous allons maintenant rentrer dans le vif du sujet en décrivant comment créer notre propre activité, ce qui sera très souvent le cas dans les différentes applications que vous allez développer.

Une activité, comme nous l'avons déjà décrit, peut être assimilée à un écran qu'une application propose à son utilisateur. L'activité permet ainsi à l'utilisateur d'interagir avec l'application. Comme expliqué précédemment, plusieurs activités peuvent composer une même application. Pour chaque écran de votre application, vous devrez donc créer une activité.

Il est préconisé de n'avoir qu'une interface visuelle par activité afin de faciliter la cinématique de l'application. En effet, la gestion du passage d'une activité à l'autre se voit ainsi mieux maîtrisée lors de l'évolution du cycle de vie de l'activité. La transition entre deux écrans correspond au lancement d'une activité ou au retour sur une activité placée en arrière plan.

Une activité est composée de deux volets :

  1. La logique de l'activité et la gestion du cycle de vie de l'activité qui sont implémentés en Java dans une classe héritant de Activity.
  2. L'interface utilisateur, qui pourra être définie soit dans le code de l'activité soit de façon plus générale dans un fichier XML placé dans les ressources de l'application.
Cycle de vie d'une activité

Tout ce que nous avons vu en parlant du cycle de vie d'une application, au tout début de notre étude, notamment sur la gestion des processus en fonction des resssources, a un impact direct sur les activités et notamment sur leur cycle de vie.

Comme le montre le diagramme UML suivant, une activité possède des méthodes de rappel (callback) appelées tout au long de son cycle de vie. Ces méthodes permettent de définir des passages d'un état à l'autre de l'activité par rapport à l'évolution des ressources et du comportement du système et de l'utilisateur. Le développeur peut ainsi définir des comportements particuliers à chaque état de celle-ci.

  1. L'activité démarre : la méthode onCreate() est appelée.
  2. L'activité est active (démarrée) : activité visible qui détient le focus et attend les entrées utilisateur. C'est l'appel à la méthode onResume(), à la création ou à la reprise après la pause qui permet à l'activité d'être dans cet état. Elle est ensuite mise en pause quand une autre activité devient active grâce à la méthode onPause().
  3. Activité suspendue (en pause) : activité au moins en partie visible à l'écran mais qui ne détient pas le focus. La méthode onPause() est invoquée pour entrer dans cet état et les méthodes onResume() ou onStop() permettent d'en sortir.
  4. Activité arrêtée : pendant l'utilisation d'une activité, l'utilisateur presse la touche Accueil, ou bien l'application téléphone, qualifiée comme prioritaire, interrompt l'activité en cours et prend en compte l'appel entrant. L'activité est arrêtée par l'appel de la méthode onStop(). Le développeur détermine l'impact sur l'interface utilisateur, par exemple la mise en pause d'une animation puisque l'activité n'est plus visible.
  5. L'activité redémarre : une fois l'appel téléphonique terminé, le système réveille l'activité précédemment mise en pause en appelant successivement les méthodes onRestart() et onStart().
  6. Destruction de l'activité : si l'activité reste trop longtemps en pause, le système a besoin de mémoire, il détruit l'activité au moyen de la méthode onDestroy().
  7. Activité partiellement visible : les méthodes onPause() et onResume() rajoutent un état à l'activité, puisqu'ils interviennent dans le cas d'activité partiellement visibles, mais qui n'ont pas le focus. La méthode onPause() implique également que la vie de cette application n'est plus une priorité du système. Donc, si celui-ci a besoin de mémoire, l'activité peut être fermée. Ainsi, il est préférable, lorsque nous utilisons cette méthode, de sauvegarder l'état de l'activité dans le cas où l'utilisateur souhaiterait y revenir avec la touche Accueil.

Vous avez ci-dessous un cas d'école qui permet de bien maîtriser toutes ces méthodes de rappel. Deux nouvelles peuvent être utilisées afin de sauvegarder et de restaurer automatiquement des données importantes.

(ensemble des méthodes de rappel) fr.manu.heure.NouvelleActivité
import android.app.Activity;
import android.os.Bundle;

public class NouvelleActivité extends Activity {

   /** 
    * Appelée lorsque l'activitée est créée.
    * Permet de restaurer l'état de l'interface utilisateur
    * grâce au paramètre icicle.
    */
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      // Placez votre code ici      
   }

   /**
    * Appelée lorsque l'activité a fini son cycle de vie.
    * C'est ici que nous placerons notre code de libération de mémoire,
    * fermeture de fichiers et autres opérations de nettoyage.
    */
   @Override
   protected void onDestroy() {
      super.onDestroy();
      // Placez votre code ici 
   }

   /**
    * Appelée lorsque l'activité démarre. Permet d'initialiser les contrôles.
    */
   @Override
   protected void onStart() {
      super.onStart();
      // Placez votre code ici 
   }

   /**
    * Appelée lorsque l'activité passe en arrière plan.
    * Libérez les écouteurs, arrêter les threads,
    * votre activité peut disparaître de la mémoire.
    */   
   @Override
   protected void onStop() {
      super.onStop();
      // Placez votre code ici 
   }
   
   /**
    * Appelée lorsque l'activité est suspendue.
    * Stoppez les actions qui consomment des ressources.
    * L'activité va passer en arrière-plan.
    */
   @Override
   protected void onPause() {
      super.onPause();
      // Placez votre code ici 
   }

   /**
    * Appelée lorsque l'activité sort de l'état de veille
    */
   @Override
   protected void onRestart() {
      super.onRestart();
      // Placez votre code ici 
   }

   /**
    * Appelée après le démarrage ou une pause.
    * Relancez les oprations arrêtées (threads).
    * Mettez à jour votre application et vérifiez vos écouteurs.
    */
   @Override
   protected void onResume() {
      super.onResume();
      // Placez votre code ici 
   }

   /**
    * Appelée lorsque l'activité termine son cycle visible.
    * Sauvez les données importantes
    */
   @Override
   protected void onSaveInstanceState(Bundle outState) {
      super.onSaveInstanceState(outState);
      // Placez votre code ici 
   }

   /**
    * Appelée après onCreate().
    * Les données sont rechargées et
    * l'interface utilisateur est restaurée dans le bon état.
    */
   @Override
   protected void onRestoreInstanceState(Bundle savedInstanceState) {
      super.onRestoreInstanceState(savedInstanceState);
      // Placez votre code ici 
   }
}
Les vues

Les vues sont les briques de construction de l'interface graphique d'une activité Android. Les objets View représentent des éléments à l'écran qui permettent d'interagir avec l'utilisateur via un mécanisme d'événements.

  1. Plus concrètement, chaque écran Android contient un arbre d'éléments de type View (descendants) dont chaque élément est différent de par ses propriétés de forme, de taille, de type d'intervention, etc.
  2. Bien que la plupart des éléments dont nous avons besoin - textes, boutons, zone de saisie, etc. - soient fournis par la plateforme (composants spécifiques), il est tout à fait possible de créer des éléments personnalisés.
  3. Les vues peuvent être disposées dans une activité et donc à l'écran soit par une description XML, soit directement dans le code Java.
  4. Nous allons aborder tout un chapitre qui traite des vues les plus utilisées, notamment ce qui s'appelle les widgets, comme les boutons, les cases à cocher, les zones de saisies, etc. voilà, à l'attendant la javadoc des composants officiels relatifs aux widgets.
Développement final de notre projet

Comme c'est souvent le cas au début, nous sommes passé par beaucoup d'explications, qui étaient bien sûr nécessaires pour bien comprendre et maîtriser l'informatique embarquée sur mobile. Nous allons enfin définir le code Java de notre activité principale (la seule ici) pour finalement commencer et terminer notre application.

fr.manu.heure.Heure.java
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.Button;
import java.util.Date;

public class Heure extends Activity implements View.OnClickListener {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      bouton = new Button(this);
      bouton.setOnClickListener(this);
      donnerHeure();
      setContentView(bouton);
   }

   public void onClick(View arg0) {
      donnerHeure();
   }

   private void donnerHeure() {
      CharSequence texte = DateFormat.format("EEEE, dd MMMM, hh:mm", new Date());
      bouton.setText(texte);
   }
}

Pour notre premier projet, tout notre travail porte sur l'élaboration de cette seule activité. Nous allons donc prendre le temps de disséquer cette classe Java et proposer en conséquence un certain nombre de remarques :

  1. La toute première remarque, c'est que le code est extrêmement réduit vu ce que nous arrivons déjà à faire.
  2. La déclaration du paquetage doit être identique à celle que vous avez utilisée pour définir le projet.
  3. Comme pour tout projet Java, vous devez importer les classes auxquelles vous faites référence. La plupart de celles qui sont spécifiques à Android se trouvent dans le paquetage android.
  4. Notez bien que toutes les classes de Java SE standard ne sont pas utilisables par les programmes Android surtout, bien entendu, les composants graphiques issus de Swing comme JButton, JLabel, etc. qui sont plus adaptés aux grands écrans des ordinateurs de bureau ou des ordinateurs portables.
  5. Les activités doivent être des classes publiques héritées, directement ou indirectement, de la classe android.app.Activity.
  6. Comme toute classe Java, il est bien sûr possible qu'elle possède ses propres attributs. Ici, nous avons déclaré un nouvel attribut bouton issu de la classe android.widget.Button.
  7. Un bouton, comme vous pouvez le constater d'après le nom du paquetage, est un widget Android. Les widgets sont des éléments d'interface graphique que vous pouvez utiliser dans une application Android. Ils sont l'équivalent des composants Swing proposés par le Java SE standard.
  8. Tous les widgets dérivent de la classe de base View. Bien que nous construisons généralement l'interface graphique à partir d'une arborescence de vue, pour notre toute première application nous n'utiliserons ici qu'une seule vue.
  9. Dans notre classe Heure, nous retrouvons la méthode de rappel onCreate(), qui comme ce sera souvent le cas est la seule à prendre en compte pour l'instant.
  10. La méthode onCreate() est appelée lorsque l'activité est lancée. La première chose à faire est d'établir le chaînage vers la superclasse afin de réaliser l'initialisation de l'activité android de base, grâce au mot réservé super que nous connaissons déjà.
  11. Nous créons ensuite l'instance du bouton à l'aide de l'opérateur new. Attention, il existe ici une différence fondamentale avec les composants Swing du Java SE standard.

    Avec Swing, il est effectivement possible de créer les instances directement au moment de la déclaration des attributs en spécifiant le constructeur désiré (plusieurs existent) pour par exemple proposer un intitulé à un JButton.

    Avec les widgets, c'est totalement différent, les constructeurs proposés attendent au moins un paramètre qui spécifie le contexte, c'est-à-dire en gros le conteneur qui va prendre en compte ce composant visuel, ici par exemple l'activité. C'est la raison pour laquelle nous sommes obligé de créer l'instance du bouton uniquement après la création de l'activité elle-même, donc dans la méthode onCreate(), puisque que nous passons en paramètre l'opérateur this.

  12. Nous verrons qu'il existe une autre solution beaucoup plus simple, mais nous pouvons proposer une gestion événementielle à l'image de ce que nous faisons avec la Java SE standard.

    Rappelez-vous, avec Swing, un clic sur un JButton déclenche un ActionEvent qui est transmis automatiquement à l'ActionListener configuré pour ce bouton. La méthode actionPerformed() est alors exécutée.

    Avec Android, en revanche, un clic sur un bouton provoque l'appel de la méthode onClick() sur l'instance OnClickListener configuré pour le bouton. L'écouteur reçoit la vue qui a déclenché le clic (ici, il s'agit du bouton).

  13. Je rappelle que pour une gestion événementielle classique en Java, il est nécessaire de respecter trois critères :
    1. La source de l'événement : ici, c'est le bouton, il doit donc faire appel à la méthode setOnClicListener() en désignant l'écouteur qui prend en compte cet événement.
    2. La gestion de l'événement : vous spécifiez l'objet qui est capable d'être à l'écoute de l'événement à prendre en compte. Ici, c'est l'activité elle-même qui implémente l'interface OnClickListener intégrée dans la classe View.
    3. L'action proposée en conséquence : lorsque qu'une classe implémente une interface, elle est dans l'obligation de respecter le contrat prévue par cette dernière, c'est-à-dire de redéfinir la ou les méthodes déclarées par l'interface. Ici, il s'agit de la méthode onClic(). C'est justement cette dernière qui sera automatiquement lancée lorsqu'un clic sur le bouton interviendra.
  14. J'ai rajouté une méthode personnelle donnerHeure(), qui est d'ailleurs privée, qui s'occupe de changer l'intitulé du bouton, au moyen de la méthode setText() que nous connaissons déjà, en proposant la date et l'heure actuelle avec un formatage spécifique.
  15. Attention, les chaînes de caractères sous Android sont implémentées avec la classe CharSequence et non pas avec la classe String.
  16. Pour que la vue soit effectivement visible, il est nécessaire de le préciser au moyen de la méthode setContentView(). Ici, par simplicité, nous demandons à ce que ce soit le bouton qui représente toute la vue. Voici d'ailleurs le résultat obtenu :

Dernières petites modifications

Le projet que nous venons de réaliser est déjà opérationnel puisque à chaque fois que nous cliquons sur le bouton, nous voyons bien l'heure changer. Toutefois, ce bouton est un peu gros puisqu'il prend toute la surface de l'activité. Je préfèrerais avoir un bouton plus classique qui, certe prend toute la largeur, mais qui conserve une hauteur normale pour un bouton, comme cela était prévu à l'origine. Voici donc la modification à apporter :

fr.manu.heure.Heure.java
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import java.util.Date;

public class Heure extends Activity implements View.OnClickListener {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      bouton = new Button(this);
      bouton.setOnClickListener(this);
      donnerHeure();
      LayoutParams dispositions = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
      addContentView(bouton, dispositions);
   }

   public void onClick(View arg0) {
      donnerHeure();
   }

   private void donnerHeure() {
      CharSequence texte = DateFormat.format("EEEE, dd MMMM, hh:mm", new Date());
      bouton.setText(texte);
   }
}
  1. Comme pour le Java SE standard qui possède des gestionnaires de disposition comme BorderLayout, FlowLayout, etc., Android possède les siens que nous découvrirons bientôt en détail. Il est possible d'avoir une disposition assez générique au moyen de la classe LayoutParams en spécifiant la stratégie à suivre sur la largeur et sur la hauteur.
  2. Si nous désirons qu'une dimension prenne le maximum de place possible, il suffit de proposer la constante LayoutParams.FILL_PARENT. Comme son nom l'évoque, le composant qui prend en compte ce critère se développe pour remplir le conteneur parent.
  3. Si nous désirons qu'une dimension s'adapte automatiquement au contenu de la vue, nous prenons cette fois-ci la constante LayoutParams.WRAP_CONTENT. Cette fois-ci, c'est le composant lui-même qui impose la taille minimale à adopter.
  4. Enfin, au lieu d'utiliser la méthode setContentView(), nous prenons maintenant plutôt la méthode addContentView() qui est capable de prendre en compte le nouveau gestionnaire de disposition.

Nous avons enfin terminé notre premier projet. Nous avons passé pas mal de temps à le faire malgré le peu de code à écrire, mais cela nous a permis de comprendre bien des techniques particulières dans cette technologie Android.

 

Choix du chapitre Création d'interfaces utilisateur

Une application qui plait aux utilisateurs dispose forcément d'une interface utilisateur ergonomique et esthétique. En effet, les interfaces graphiques prennent une place importante dans le choix des applications par les utilisateurs, tant dans l'implémentation de concepts innovants qu'au niveau de l'ergonomie.

Comme sur bien des plates-formes, les interfaces d'applications Android sont organisés en vues et gabarits (disposition automatique des composants), avec néanmoins quelques spécificités.

Le concept d'interface

Une interface graphique n'est pas une image statique mais un ensemble de composants graphiques, qui peuvent être des boutons, du texte, mais aussi des groupements d'autres composants graphiques, pour lesquels nous pouvons définir des attributs communs (taille, couleur, positionnement, etc.).

  1. Ainsi, l'écran suivant :

  2. Correspond en réalité à un assemblage de composants avec une structure imbriquée :

  3. L'imbrication de l'ensemble de ces composants graphiques se fait, sous Android, sous la forme d'un arbre, en une structure hiérarchique :

Il peut n'y avoir qu'un seul composant, comme des milliers, selon l'interface que vous souhaitez représenter. Dans l'arbre ci-dessus, par exemple, les composants ont été organisés en 3 parties (haut, milieu et bas).

Sous Android, vous pouvez décrire vos interfaces utilisateur de deux façons différentes :
  1. Avec une description déclarative XML, ou
  2. Directement dans le code d'une activité en utilisant les classes adéquates, comme nous l'avons fait lors de l'élaboration de notre premier projet.

La façon la plus simple de réaliser une interface utilisateur est sans contexte la méthode déclarative XML qui permet de bien séparer la vue du traitement logique sous-jacent. Il suffira de créer un ou plusieurs fichiers XML qui devront être placées dans le dossier res/layout.

En effet, un document XML est tout à fait adapté à la structuration sous forme d'arborescence, avec un enchaînement de balises englobantes, ou balises mères, et de balises feuilles ou filles.

Les vues

Une interface graphique est composée d'une multitude de composants graphiques : les vues. Sous Android, une vue est représentée, comme son nom l'indique, par la classe View. Tous les composants graphiques (boutons, images, case à cocher, etc.) d'Android héritent tous de cette classe View et sont communément appelées des widgets.

Bien entendu, et comme très souvent, la classe View utilisée comme telle, ne permet pas de développer des interfaces très avancées. Cette classe est en effet tellement générique qu'elle n'a que peu d'intérêt si elle est employée comme telle. Vous utiliserez donc systématiquement une de ses classes filles, Button, ImageView, CheckBox, etc. Vous avez ci-dessous la hiérarchie de quelques vues qui sont souvent utilisées :

Il faut bien garder à l'esprit, à l'image du Java SE standard, que lorsque nous créons une vue, nous créons systématiquement un objet graphique. Voici d'ailleurs ci-dessous les caractéristiques intéressantes de quelques unes de ces vues, de ces widgets :

  1. TextView : Cette vue permet d'afficher un texte à l'écran et gère de nombreuses fonctionnalités relatives à l'affichage du texte : couleur, police, taille, graisse, etc.
  2. Button : Comme son nom l'indique, cette classe représente un bouton. Puisque cette classe hérite de TextView, elle dispose de toutes les capacités de sa classe parente. La seule particularité d'une vue Button vis-àvis d'une vue TextView est son apparence. Un Button est en fait un texte affiché dans un bouton (rectangle gris aux coins arrondis) changeant automatiquement d'apparence lorsque l'utilisateur interagit avec ce dernier (clic, focus).
  3. EditText : Cette vue permet à l'utilisateur d'entrer un texte que l'application pourra récupérer et utiliser par la suite.
  4. ImageView : Avec TextView, ImageView est l'une des vues les plus utilisées dans les interfaces graphiques. Elle permet d'afficher une image et dispose de plusieurs options intéressantes : teinte, redimensionnement, etc.

Il existe, bien évidemment, d'autres vues plus évoluées comme les ImageButton, AnalogClock ou ProgressBar par exemple. Nous aurons l'occasion de développer ces widgets lors d'un prochain chapitre.

L'utilisation et le positionnement des vues dans une activité se fera la plupart du temps en utilisant une mise en page qui sera composée par un ou plusieurs gabarits de vues.

Positionner les vues avec les gabarits

Une interface graphique n'est pas uniquement composée de vues "feuilles". Il existe, en effet, des vues particulières permettant de contenir d'autres vues et de les positionner : les gabarits. Nous les appelons également par leur nom anglais : layouts. Avec Java SE, nous connaissons bien ce genre de technique qui consiste à positionner et dimensionner les composants automatiquement à l'aide de gestionnaires de dispositions.

Ces vues particulières, ces gabarits, héritent toutes de la classe racine ViewGroup qui hérite elle-même de la classe View. Un gabarit est donc un conteneur qui aide à positionner les objets, qu'il s'agisse de vues ou d'autres gabarits au sein de votre interface. Vous pouvez imbriquer des gabarits les uns dans les autres, ce qui vous permettra de créer des mises en forme évoluées. Nous parlerons ainsi de gabarit parent et de gabarit enfant (ou plus généralement d'éléments enfants voire simplement d'enfants), le gabarit enfant étant inclus dans le gabarit parent.

Comme nous l'avons dit plus haut, vous pouvez décrire vos interfaces utilisateur soit par une déclaration XML, soit directement dans le code d'une activité en utilisant les classes adéquates. Dans les deux cas, vous pouvez utiliser différents types de gabarits. En fonction du type choisi, les vues et les gabarits seront disposés différemment :

  1. LinearLayout : Ce gabarit aligne l'ensemble de ses enfants dans une direction unique, horizontale ou verticale. Les éléments se succèdent ensuite de gauche à droite ou de haut en bas. Ce gabarit n'affiche donc qu'un seul élément par ligne ou colonne (selon l'orientation choisie : horizontale ou verticale).
  2. FrameLayout : C'est le plus basique des gabarits. Ce gabarit empile les éléments fils les uns sur les autres tel une accumulation de calques. Par défaut, toutes les vues filles s'alignent sur le coin supérieur gauche, mais il est possible de modifier le positionnement avec le paramètre android:layout_gravity.
  3. RelativeLayout : Ses enfants sont positionnés les uns par rapport aux autres, le premier enfant servant de référence aux autres. Ce gabarit a l'avantage de permettre un positionnement précis et intelligent tout en minimisant le nombre de vues utilisées.
  4. TableLayout : Permet de positionner vos vues en lignes et colonnes à l'instar d'un tableau.
  5. Gallery : Affiche une ligne unique d'objets dans une liste déroulante horizontale.

Nous prendrons le temps de développer chacun de ces gabarits particuliers dans un chapitre qui leur seront consacrés.
.

Utilisation des layouts XML

Bien qu'il soit techniquement possible de créer et d'attacher des composants widgets à une activité en utilisant uniquement du code Java comme nous l'avons fait dans le chapitre précédent, nous préférons généralement employer un fichier de positionnement (layout) codé en XML.

L'instanciation dynamique des widgets est réservé aux scénarii plus complexes, où les widgets ne sont pas connus au moment de la compilation (lorsqu'il faut, par exemple, remplir une colonne de boutons radio en fonction de données récupérées sur Internet).

Nous allons justement reprendre le projet précédent afin que vous discerniez bien la différence d'approche entre les deux solutions. Nous allons cette fois-ci proposer un codage XML pour créer le bouton de soumission qui nous propose l'heure en temps réel à chaque clic venant de l'utilisateur.

Cette approche va nous permettre de savoir comment sont agencées les vues des activités Android.

Une des grandes forces d'Android est de permettre la définition d'interfaces graphiques dans un format XML. Cette définition se réalise dans des fichiers XML où nous spécifions des relations existant entre les widgets, ainsi qu'avec leurs conteneurs. C'est une ressource stockée dans le dossier prévu à cet effet : res/layout. A ce sujet, grâce à Netbeans, vous disposez déjà d'un fichier de disposition des composants main.xml qu'il suffit de modifier.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/bouton"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:onClick="changerHeure"
/>

Chaque fichier XML contient une arborescence précisant le placement des widgets et les conteneurs qui composent une vue. Les attributs de ces éléments sont des propriétés qui décrivent l'aspect d'un widget ou le comportement d'un conteneur. Nous allons décrire le document XML ci-dessus :

  1. Chaque vue possède des attributs spécifiques, et d'autres communs à tous les widgets (mais aussi aux gabarits). Parmi les propriétés communes, vous trouverez layout_width et layout_height. Ces propriétés sont obligatoires et existent pour toutes les vues. Celles-ci permettent de spécifier le comportement du remplissage en largeur et en hauteur.

    Il est possible de proposer des valeurs spécifiques sur ces deux attributs, avec les unités de mesure que vous désirez, comme le pixel par exemple. Vous pouvez également proposer les valeurs constantes suivantes : fill_parent et wrap_content.

  2. La valeur fill_parent spécifie que toute la vue va automatiquement prendre la même taille que son parent, ici donc toute la largeur de l'écran, puisque ce denier est le parent de la vue.
  3. La valeur wrap_content force la vue à prendre la taille de son contenu. Ici, la vue ne prend que la place qui lui est juste nécessaire sur la hauteur du widget.
  4. De nombreux widgets et conteneurs peuvent apparaître que dans le fichier de positionnement et ne seront pas utilisés par votre code Java. Le plus souvent un simple label statique (TextView), par exemple, n'a besoin d'être dans le fichier XML que pour indiquer l'emplacement où il doit apparaître dans l'interface. Ce type d'élément n'a pas besoin d'être identifier pour être utilisé dans le code Java puisque tout est déjà spécifié.
  5. Ici, par contre, dans le code Java, nous avons besoin d'accéder au bouton déclaré dans ce document XML afin de pouvoir changer son texte, en temps réel, à chaque clic sur ce dernier. Nous avons donc besoin de l'identifier afin de permettre cet accès. Comme nous nous en doutons, cet identifiant doit être unique afin d'être sûr d'agir sur le widget voulu.
  6. Chaque vue Android peut ainsi disposer d'un identifiant défini grâce à l'attribut android:id. La convention consiste à utiliser le format @+id/nom_uniquenom_unique représente le nom de l'objet que vous souhaitez voir apparaître et utiliser dans le code Java.
  7. Ici, le nom choisi est bouton, vu l'écriture suivante : @+id/bouton. En réalité, toutes les déclarations des vues et définitions des propriétés internes ont une conséquence sur la génération automatique de la classe R qui prend en compte nos différents choix. La plupart du temps, cela se traduit par la déclaration de constantes dont les valeurs sont choisies automatiquement. Pour revenir à notre exemple, l'identifiant bouton correspond à l'attribut qui prend la valeur entière unique 0x7f050000 dans la classe id (correspondant à la propriété id de la classe Button).
    fr.manu.heure.R.java
    /* AUTO-GENERATED FILE.  DO NOT MODIFY.
     *
     * This class was automatically generated by the
     * aapt tool from the resource data it found.  It
     * should not be modified by hand.
     */
    
    package fr.manu.heure;
    
    public final class R {
        public static final class attr {
        }
        public static final class drawable {
            public static final int icon=0x7f020000;
        }
        public static final class id {
            public static final int bouton=0x7f050000;   // constante entière qui identifie le widget correspondant au bouton déclaré dans main.xml
        }
        public static final class layout {
            public static final int main=0x7f030000;     // constante entière qui identifie le fichier principal des disposition des composants
        }
        public static final class string {
            public static final int app_name=0x7f040000;
        }
    }
  8. La version d'Android 1.6 introduit un attribut XML particulièrement utile : android:onClick. Cet attribut prend une chaîne de caractères représentant le nom de la méthode de l'activité à appeler. Ainsi, un clic sur la vue, ici le bouton, portant l'attribut android:onClick="changerHeure", exécutera la méthode public void changerHeure(View v) (où v représente la vue qui est la source de l'événement). Cet attribut évite au développeur les opérations rébarbatives permettant d'assigner un écouteur à une vue. Le code Java va ainsi devenir beaucoup plus simple et plus facile à écrire.
Les unités de mesure

Android gère de nombreuses unités de mesure lorsqu'il s'agit de définir la taille d'un élément (taille d'une vue, d'un texte, d'un gabarit, etc.). Il est important de bien comprendre ce que chacune d'entre elles représente :

  1. Pixel (px) : correspond à un pixel à l'écran.
  2. Millimètre (mm) : Basée sur la taille physique à l'écran
  3. Pouce (in) : Unité également basée sur la taille physique de l'écran. Unité, je le rappelle correspondant à 2,54 cm.
  4. Point (pt) : 1/72 d'un pouce.
  5. Pixel à densité indépendante (dp ou dip) : Une unité relative se basant sur la densité de l'écran (la densité d'un écran définit le nombre de pixels physique existant sur une longueur donnée). Sur Android, la densité de référence est 160 dpi (160 pixels/pouce). Ainsi, sur un écran de 160 dpi, 1 dp équivaut à un pixel. Le rapport entre dp et pixel change automatiquement en fonction de la densité actuelle de l'écran. Ainsi, 2 dp sur un écran de 160 dip équivaut à 2 px, mais à 3 px (2 * 240/160) sur un écran de 240 dip.

    L'intérêt de cette unité est de permettre un rendu physique (taille physique réelle) toujours similaire quelle que soit la densité de l'écran. C'est l'unité la plus utilisée sous Android et c'est celle que nous emploierons majoritairement par la suite.

    Les moniteurs d'ordinateur ont généralement des densités comprises entre 96 et 120 dpi. C'est ce qui explique que l'émulateur paraît physiquement plus grand que les terminaux réels qui ont souvent des densités supérieurs à 120 dpi.

  6. Pixels à taille indépendante (sp) : équivaut au dp, mais inclut en plus un possible agrandissement en fonction des préférences utilisateurs. C'est l'unité recommandée pour définir la taille d'un texte.

    Un utilisateur malvoyant pourrait ainsi augmenter la taille des textes utilisant l'unité de mesure sp.

Définition d'un identifiant

Comme nous venons de le voir, Android propose la notion d'identifiant, largement utilisé en informatique, notamment pour les bases de données. Les identifiants permettent de définir, de manière unique, une entité, quelle qu'elle soit. Chaque vue Android peut ainsi disposer d'un identifiant défini grâce à l'attribut XML android:id.

Nous avons découvert une syntaxe un peu particulière pour définir cet attribut. Toutefois, elle prend tout son sens après avoir décrit l'intérêt de ces symboles. Ainsi, lorsque nous écrivons @+id/bouton, voici à quoi cela correspond :

  1. @ : Ce caractère spécial dans les layouts Android exprime une indirection. Ainsi, le système comprend que la ressource pointée par l'identifiant n'est pas définie dans le fichier courant, mais dans un fichier externe.
  2. + : Ce caractère permet d'indiquer à Android que l'identifiant peut être ajouté à la liste des identifiants de l'application si cela n'est pas déjà le cas.
  3. id/ : En conjonction avec le @, le préfixe id/ permet d'obliger Android à regarder dans la liste des identifiant déjà déclarés (par +).
  4. bouton : c'est le nom de l'identifiant que vous décidez de choisir. Attention, il doit bien entendu être unique.
Gérer les événements

Comme beaucoup d'IHM, sous Android, toutes les actions de l'utilisateur sont perçues comme un événement, que ce soit le clic sur un bouton d'une interface, le maintien du clic, l'effleurement d'un élément de l'interface, etc. Ces événements peuvent être interceptés par les éléments de votre interface pour exécuter des actions en conséquence.

Le mécanisme d'interception repose sur la notion d'écouteurs (listeners en anglais). Il permet d'associer un événement à une méthode à appeler en cas d'apparition de cet événement. Si un écouteur est défini pour un élément graphique avec un événement précis, la plate-forme Android appellera la méthode associée dès que l'événement se produira sur cet élément.

Notez que les types d'écouteurs sont prédéfinis. Ils sont représentés par des interfaces. Ils correspondent aux types d'événement à prendre en compte. Du coup, chaque type d'écouteur, possède une ou plusieurs méthodes qu'il est nécessaire de redéfinir pour préciser l'action à faire lors de la génération de l'événement.

Comme nous l'avons déjà vu lors du chapitre précédent, le type d'écouteur s'appelle OnClickListener et la méthode à prendre en compte et qu'il est nécessaire de redéfinir se nomme onClick().

Grâce à l'attribut android:onClick="changerHeure" déclaré dans le document XML, tout se fait en coulisse. Du coup, dans le code Java, vous n'avez plus à créer une classe qui implémente l'interface OnClickListener. Il suffit uniquement de spécifier les actions à faire dans la méthode que vous avez désignée dans la déclaration de cet attribut, ici changerHeure(View v).

Outils d'autogénération

L'outil aapt du SDK d'Android utilise ces layouts (fichiers XML stockés dans le répertoire res/layout) ; il est automatiquement appelé par la chaîne de production du projet.

C'est aapt qui produit le fichier source R.java du projet, qui vous permet d'accéder directement aux layouts et à leurs composants widgets depuis votre code Java, comme nous le verrons bientôt.

fr.manu.heure.R.java
/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package fr.manu.heure;

public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class id {
        public static final int bouton=0x7f050000;   // constante entière qui identifie le widget correspondant au bouton déclaré dans main.xml
    }
    public static final class layout {
        public static final int main=0x7f030000;     // constante entière qui identifie le fichier principal des disposition des composants
    }
    public static final class string {
        public static final int app_name=0x7f040000;
    }
}
Associer votre interface à une activité et définir la logique utilisateur

Une fois que vous avez configuré les widgets et les conteneurs dans un fichier de positionnement XML nommé main.xml et que vous l'avez bien placé dans le répertoire res/layout, vous n'avez plus qu'à créer votre code source Java qui va prendre en compte vos différents réglages et proposer la logique utilisateur correspond aux actions souhaitez en rapport avec votre application.

Dans une application, une interface est affichée par l'intermédiaire d'une activité. Vous devez donc avant toute chose créer une activité en ajoutant une nouvelle classe à votre projet dérivant de la classe Activity, ici Heure. C'est la même activité que le projet précédent sur laquelle nous allons apporter quelques petites modifications.

fr.manu.heure.Heure.java
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.Button;
import java.util.Date;

public class Heure extends Activity {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      bouton = (Button)findViewById(R.id.bouton);
      donnerHeure();      
   }

   public void changerHeure(View arg0) {
      donnerHeure();
   }

   private void donnerHeure() {
      CharSequence texte = DateFormat.format("EEEE, dd MMMM, hh:mm", new Date());
      bouton.setText(texte);
   }
}    
  1. Le chargement du contenu de l'interface s'effectue à l'intanciation de l'activité. Pour cela, redéfinissez la méthode onCreate() de l'activité pour y spécifier la définition de l'interface à afficher au travers de la méthode prévue à cet effet, setContentView(). Cette méthode prend en paramètre un identifiant qui spécifie quelle ressource doit être chargée et affichée comme contenu graphique.
  2. La vue construite à partir de notre fichier XML est désormais accessible à partir de la classe R. Nous avons définis tous nos positionnements dans le fichier main.xml qui se situe dans le répertoire res/layout. Dans notre classe R, qui tient compte de toutes nos constructions, nous voyons bien une constante entière qui se nomme main et qui se situe dans la classe layout. Finalement, notre ressource, dans le code Java s'identifie par R.layout.main qu'il suffit de spécifier dans cette méthode setContentView().
  3. Dans le chapitre précédent, nous avons créer le bouton à partir du code Java à l'aide de la commande bouton = new Button(this). Ici, notre approche est différente puisque le bouton est automatiquement créer en coulisse grâce au fichier de configuration et de dispositions main.xml.
  4. Il est quand même nécessaire, dans notre cas, de pouvoir communiquer avec cet objet, et c'est d'ailleurs la raison pour laquelle nous avons mis en oeuvre un identifiant. Pour accéder à ce bouton, il suffit d'utiliser la méthode findViewById() en lui passant en paramètre l'identifiant numérique du widget concerné.
  5. Encore une fois, cet identifiant numérique est automatiquement généré par Android dans la classe R, en relation avec la déclaration de l'attribut android:id proposée dans notre document de description XML. Logiquement, il est toujours de la forme R.id.quelquechose (où quelquechose est le widget que vous recherchez). Dans notre déclaration XML, nous avons décidé d'identifier notre bouton comme lors du chapitre précédent, c'est-à-dire bouton. Du coup, l'identifiant numérique est R.id.bouton.
  6. Grâce à la déclaration XML, la gestion événementielle est maintenant beaucoup plus simple dans le code Java. Deux écritures ont été définitivement enlevées. D'une part, l'implémentation de l'interface OnClicListener et d'autre part la définition de la source de l'événement, c'est-à-dire bouton.setOnClickListener(this).
  7. Enfin, nous avons modifier l'intitulé de la méthode onClic() par celle prévue lors de la déclaration du descriptif XML, savoir donnerHeure().
Avantage à utiliser des layouts XML

Toutes les propriétés des widgets (objets Java) sont bien entendu accessibles dans le code Java. Ce qui est extrêmement intéressant, c'est qu'elles peuvent également être fixées par des attributs dans le XML du layout.

  1. En plus de permettre des changements plus aisés dans les choix des différents réglages sur l'interface, le découplage de la conception visuelle et du code de la logique de l'application elle-même permet de rendre ce dernier plus concis.
  2. En outre, séparer ces définitions XML du code Java réduit les risques qu'une modification du code source perturbe l'application.

Afin que vous soyez convaincu par cette approche, je vous propose de modifier l'apparence du bouton afin que sa largeur ne soit plus la plus grande possible, mais qu'elle s'autodimensionne suivant son contenu. Par ailleurs, j'aimerais qu'il soit toujours placé automatiquement au centre de l'écran.

Vous remarquez que le comportement global ne change absoluement pas, c'est juste l'aspect visuel du bouton que nous modifions. Il suffit donc de s'intéresser au layout XML sans se préoccuper du tout du code Java. Ainsi, voici le nouveau main.xml à prendre en compte pour obtenir le résultat voulu.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/bouton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:onClick="changerHeure"
/>
  1. Pour appliquer les modifications souhaitée, je reviens sur la définition de l'attribut android:layout_width sur lequel je prévois maintenant la constante wrap_content.
  2. Je rajoute un nouvel attribut android:layout_gravity qui permet d'agir sur la stratégie de positionnement à la fois en horizontal et en vertical. Je demande ainsi que le composant soit centrer sur l'écran grâce au paramètre center.
  3. Comme autre exemple, si vous désirez que le bouton soit centré en horizontal, mais que cette fois-ci il soit placé en haut de l'écran, il suffit de proposer le paramètre center_horizontal sur cet attribut android:layout_gravity.

Nous commençons à comprendre l'architecture d'une application Android et comment développer une interface utilisateur. Dans les chapitres qui suivent, nous allons continuer sur cette interface utilisateur en prenant plus connaissance sur les types de widgets qui existent et comment disposer tous ces composanst les uns par rapport aux autres en revenant sur la notion de gabarit.

 

Choix du chapitre Les ressources

Maintenant que nous voyons l'intérêt des documents XML, avant de connaître plus précisément les widgets qui sont à notre disposition, ainsi que les différents types de conteneur (gabarit) qui gèrent le positionnement automatique des vues, je vous propose de nous attarder sur les types de ressources qui existent dans une application Android.

  1. Les ressources sont des informations statiques, stockées en dehors du code Java. Nous venons de rencontrer un type de ressource, les fichiers de description (layouts), mais il en existe bien d'autres dont vos applications peuvent tirer profit : les images et les chaînes de caractères, par exemple.
  2. Les ressources sont donc des fichiers externes, ne contenant pas d'instruction, qui sont utilisées par le code et liés à votre application au moment de sa construction. Android offre un support d'un grand nombre de ressources comme les fichiers images JPEG et PNG, les fichiers XML, etc.
  3. L'externalisation des ressources en permet une meilleure gestion ainsi qu'une maintenance plus aisée. Android étend ce concept à l'externalisation de la mise en page graphique (layouts) en passant par celle des chaînes de caractères, des images et bien d'autres...

Présentation

Dans un projet Android standard, le code et les ressources peuvent être structurellement dissociés. Toutes les ressources peuvent être écrites en code Java, toutefois le code serait très complexe avec de nombreuses conditions pour la gestion des différents terminaux. Les ressources structurées simplifient cette gestion, chaque fichier, écrit en XML, peut définir la configuration du terminal. Ainsi, c'est le système Android qui choisi le fichier ressource adéquat.

Cette gestion des configurations se fait ainsi de manière totalement automatique, sans prise en compte particulière de ces caractéristiques au niveau du code de l'application. Le lien entre le code Java et les ressources XML auxquelles il fait appel, reste assuré par le fichier automatiquement généré : R.java.

Les différents types de ressources

Les ressources de l'application Android sont stockées dans des fichiers situés sous le répertoire res de votre projet. Ce répertoire sert de racine et contient lui-même une arborescence de dossiers correspondant à différents types de ressources.

  1. Les images : res/drawable : ce dossier contient l'ensemble des ressources graphiques mise en oeuvre dans le cadre de votre projet, des images (PNG, JPEG, etc.), des vidéos, des icônes, etc. A la construction, ces images peuvent être optimisées automatiquement. Si vous projetez de lire une image bit à bit pour réaliser des opérations dessus, placez-la plutôt dans les ressources brutes.
  2. Mise en page : res/layout : ce dossier contient les fichiers décrivant la composition des interfaces (les vues) de l'application, décrites en format XML. Ces fichiers XML sont convertis en mise en page d'écrans (ou de parties d'écrans), que l'on appelle aussi gabarits.
  3. Les menus : res/menu : ce dossier contient la description XML des menus de l'application.
  4. Les ressources brutes : res/raw : ce dossier contient les ressources autres que celles décrites ci-dessus qui seront empaquetées sans aucun traitement spécifique.
  5. Les valeurs simples : res/values : ce dossier contient des messages, des dimensions, les couleurs, etc. prédéfinis qui sont très facile de modifier par la suite. Cela permet de découpler le code principal des différents réglages et interventions utltérieures possibles. Grâce à ce type de ressource, les chaînes, les couleurs, les tableaux et les dimensions permettent d'associer des noms symboliques à ces types de constantes et de les séparer du reste du code (pour l'internationnalisation et la localisation, notamment).
  6. Les animations : res/anim : de manière à rendre les interfaces utilisateurs moins statiques, ce type de ressources permet d'animer des composants de type vue : des textes, des images, des groupes de vues.
  7. Données personnalisées : res/xml : ce dossier contient les autres fichiers XML généraux que vous souhaitez soumettre. Ces fichiers statiques permettent de stocker vos propres données et structures.

A l'exception des ressources brutes (res/raw), tous les types de ressources sont analysés automatiquement, soit par le système de paquetages d'Android, soit par le système du terminal ou de l'émulateur. Si vous décrivez, par exemple, l'interface utilisateur d'une activité via une ressource de type layout (dans res/layout), vous n'avez pas besoin d'analyser vous-même le contenu du fichier XML, Android s'en chargera pour vous.

Avant de rentrer dans les détails techniques, je vous propose de prendre tout de suite un exemple d'utilisation en modifiant le projet précédent. Cette fois-ci, j'aimerais changer l'icône de l'application et faire en sorte que le texte du bouton soit de couleur orange et en gras. Par ailleurs, au démarrage de l'application, j'aimerais également que l'intitulé du bouton ne soit pas tout de suite la visualisation de la date mais qu'il soit un simple texte configurable :

Vu que le comportement diffère un peu, puisqu'au démarrage nous ne demandons plus d'afficher tout de suite la date et l'heure directement sur le bouton, je dois modifier le code Java en conséquence. Voici ce que nous obtenons :

fr.manu.heure.Heure.java
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.Button;
import java.util.Date;

public class Heure extends Activity {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      bouton = (Button)findViewById(R.id.bouton); 
   }

   public void changerHeure(View v) {
      CharSequence texte = DateFormat.format("EEEE, dd MMMM, hh:mm", new Date());
      bouton.setText(texte);
   }
} 

La conséquence directe de la modification du comportement de l'application est la suppression pure et simple de la méthode donnerHeure(). Le code devient du coup encore plus réduit.

Proposer une nouvelle icône

Par défaut, lorsque vous élaborez un nouveau projet, une icône standard vous est proposée. Il est possible, bien entendu, de choisir sa propre icône pour son application.

Pour cela, il suffit de récupérer ou de fabriquer votre icône et de la placer dans le répertoire res/drawable. Vous remarquez toutefois qu'il existe trois répertoires portant ce même nom avec un suffixe différent, respectivement hdpi, ldpi et mdpi, qui correspondent à la taille des icônes suivant la densité de l'écran choisi.

  1. drawable-ldpi : Utilisée pour stocker des images de faible capacité pour des écrans à densité de pixels comprise entre 100 et 140 dpi. Dans le cas des icônes, choisissez une résolution, soit de 32x32, soit comme c'est le cas ici 36x36.
  2. drawable-ldpi : Utilisée pour stocker des images de capacité moyenne pour des écrans à densité de pixels moyenne entre 140 et 180 dpi. Dans le cas des icônes, choisissez une résolution en gros de 48x48.
  3. drawable-ldpi : Utilisée pour stocker des images de forte capacité pour des écrans à densité élevée comprise entre 190 et 250 dpi. Dans le cas des icônes, choisissez une résolution en gros de 72x72 (double par rapport à la faible densité).
  4. drawable ou drawable-nodpi : Utilisée pour des images ne devant pas être mises à l'échelle, quelle que soit la densité de l'écran.

Afin de correspondre à la totalité des situations, suivant le type de mobile choisi, il est souhaitable de prévoir une icône par densité d'écran possible.

Le nom donné au fichier représentant l'icône s'appelle icon.png. Lorsque vous désirez changer l'icône de l'application, la meilleure solution consiste à supprimer celles qui existent et de proposer les nouvelles en prenant le même nom. Il est toutefois possible de choisir un autre nom de fichier et de garder éventuellement l'icône proposée par défaut. Dans ce cas, il faut avertir le manifeste de l'application afin qu'il prenne en compte ce nouveau nom de fichier.

Dans la figure ci-dessus, vous remarquez que nous avons modifié l'attribut android:icon de l'élément <application> en spécifiant la valeur @drawable/clock en lieu et place de @drawable/icon.

Intitulé du bouton en gras et de couleur orange

Nous désirons intervenir maintenant sur l'aspect visuel du bouton afin de proposer un texte avec différents réglages supplémentaires, comme la mise en gras et la demande d'une couleur particulière sur ce texte.

La première démarche qui vient à l'esprit est de spécifier ces réglages supplémentaires directement dans la description de la vue, c'est-à-dire dans le descriptif XML main.xml, comme ce qui suit :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/bouton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:onClick="changerHeure"
    android:text="Cliquez sur le bouton"
    android:textColor="#FF9900"
    android:textStyle="bold"
/>
  1. Nous avons rajouté un certain nombre d'attribut à la définition que nous avions déjà établie lors du projet précédent :
  2. android:text : permet de spécifier l'intitulé du bouton, ici "Cliquez sur le bouton".
  3. android:textColor : permet de choisir la couleur du texte du bouton, ici sous forme RGB "#FF9900".
  4. android:textStyle : modifie le style du texte, ici en gras "bold".

Nous pouvons encore aller plus loin dans la séparation des différents réglages. Il existe un fichier de description prédéfini strings.xml dans lequel, comme son nom l'indique, vous pouvez spécifiez vos propres chaînes de caractères statiques qui peuvent servir pour les widgets et même pour le codage Java.

  1. Toutes les chaînes prédéfinies doivent être spécifiées à l'intérieur d'un élément <string>. Il suffit de désigner un identifiant au moyen de l'attribut name afin qu'elle puisse être prise en compte dans un autre document.
  2. Remarquez au passage que l'identifiant app_name à automatiquement été généré lors de l'élaboration du projet qui prend ainsi la valeur par défaut MainActivity. Il est bien sûr possible de changer ce nom, comme "Obtention de l\'heure" (Remarquez le caractère d'échappement dû à l'utilisation de l'apostrophe).
  3. Toutes les ressources que vous créez doivent être placées dans un document XML qui sert de descriptif dont l'élément racine doit impérativement être <resources>. Finalement, le nom du fichier lui-même importe peu, nous le découvrirons par la suite.
  4. Je profite de ce document de description pour définir une nouvelle couleur au moyen de la balise prédéfinie <color> qui elle aussi possède l'attribut name qui sert à identifier cette couleur spécifique.

    Le choix du nom de ces balises prédéfinies est bien sûr d'une très grande importance (contrairement au nom du fichier lui-même), puisque c'est grâce à cette identification qu'il sera possible de récupérer le contenu de la chaîne de caractères prédéfinie ainsi que la couleur prédéfinie dans un autre document.

  5. Ainsi, pour récupérer le contenu d'une chaîne dans un document de description comme main.xml, il suffit d'écrire la syntaxe suivante @string/ suivi de l'identifiant de la chaîne. Donc, pour récupérer le contenu "Cliquez sur le bouton" défini dans le descriptif strings.xml, vous devez spécifier la syntaxe suivante @string/texte_bouton.
  6. Dans le même ordre d'idée, pour prendre en compte la couleur définie dans le document strings.xml, il suffit d'utiliser la syntaxe @color/ suivi de l'identifiant, donc ici @color/couleur_bouton.

Finalement la règle de cette syntaxe est très simple. Je rappelle que @ signifie que la ressource pointée par l'identifiant n'est pas définie dans le fichier courant, mais dans un fichier externe. Ensuite, nous devons mettre le type de ressource correspondant aux éléments que nous avons définis, donc @string relatif à l'élément <string> et @color relatif à l'élément <color>. Il suffit ensuite de placer l'identifiant que vous avez choisi en mettant en intermédiaire le séparateur "/".

Pour finir, j'aimerais aller au bout de la démarche de séparation et de proposer un fichier spécifique pour les couleurs afin de ne pas mélanger les types de ressource. En effet, comme son nom l'indique, le document strings.xml est normalement prévu pour la description des chaînes de caractères prédéfinies dans votre application.

Nous allons donc créer un fichier de description couleurs.xml que je place dans le même répertoire res/values. Ce fichier me servira à définir toutes les couleurs de l'application.

A l'issu de toutes ces expériences, je vous propose maintenant de consulter le fichier autogénéré R.java. Remarquez bien la présence de nouveaux éléments.

fr.manu.heure.R.java
/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package fr.manu.heure;

public final class R {
    public static final class attr {
    }
    public static final class color {
        public static final int couleur_bouton=0x7f040000;
    }
    public static final class drawable {
        public static final int clock=0x7f020000;
        public static final int icon=0x7f020001;
    }
    public static final class id {
        public static final int bouton=0x7f060000;
    }
    public static final class layout {
        public static final int main=0x7f030000;
    }
    public static final class string {
        public static final int app_name=0x7f050000;
        public static final int texte_bouton=0x7f050001;
    }
}
  1. Une nouvelle classe color qui possède une constante entière statique couleur_bouton.
  2. Une nouvelle constante entière statique clock dans la classe drawable.
  3. Egalement, une nouvelle constante entière texte_bouton dans la classe string.
  4. Ce sont tous les éléments que nous avons rajout dans ce chapitre.

Mise à part pour les fichiers images, vous pouvez remarquer que le nom des fichiers n'apparaît absolument pas dans ce code R.java, pour les ressources que vous créez, comme string.xml et couleurs.xml. C'est vraiment les balises prédéfinies <string> et <color> qui sont réellements pris en compte.

Utilisation des ressources

Les ressources que vous créez ou qu'Android propose peuvent être utilisées directement dans votre code Java ou être référencées dans d'autres ressources. Comme, nous venons de le voir, les ressources sont accessibles et utilisées depuis le code grâce à la classe statique R. Cette classe est automatiquement générée en fonction des ressources présentes dans votre projet au moment de la compilation et de la construction de l'application.

Ressources appelées dans le code

La classe R contient donc des classes également en membres statiques pour chaque type de ressources défini dans le projet, comme layout, drawable, string, etc. Vous pouvez accéder à des ressources en passant par ces classes internes R.string pour une chaîne de caractères, mais aussi R.color pour les couleurs... Chaque classe interne expose donc au moins une variable dont le nom correspond à l'identifiant de la ressource.

Pour utiliser une ressource, il suffit donc de connaître son type de ressource et son identifiant. La syntaxe est alors la suivante :

R.type_de_ressource.nom_ressource

Attention, ces ressources définies par la classe R ne sont que des identifiant exprimés sour la forme de constante entière. Si vous désirez manipuler directement les véritables ressources, les objets eux-même, il faut les extraire de la table grâces aux méthodes de la classe Resources. Elles possède effectivement un certain nombre de méthodes circonstanciées, comme getString(), getColor(), etc.

Resources ressources = getResources();
String nom = ressources.getString(R.string.app_name);
Ressources référencées par d'autres ressources

Vous pouvez également utiliser vos ressources comme valeurs d'attributs dans d'autres ressources sous forme XML. Comme nous en avons fait l'expérience, cette possibilité est très utilisée dans les mises en page par exemple.

La notation pour faire référence à une autre ressource est la suivante :

attribut = "@[nom_paquetage:]type_de_ressource/nom_ressource

Utilisation de ressources système

Les applications natives Android externalisent elles aussi leurs ressources. Dès lors, il est possible d'utiliser leurs images, leurs chaînes de caractères, leurs couleurs, etc.

Le mode opératoire pour accéder à ces ressources est le même que pour vos ressources personnelles. Vous devez par contre spécifier l'espace de nommage android en préfixe :

android.R.string.texte_bouton    // dans le code source
@android:string/texte_bouton    // dans un autre document de description XML

Maintenant que nous connaissons bien les principes et en se servant également de nos différentes expériences, nous allons maintenant rentrer un peu plus dans le détail technique suivant les types de ressources à mettre en oeuvre. Je ne vais par contre pas perdre de temps sur les mise ne pages (res/layout) puisqu'un gros chapitre lui sera entièrement dédié.

Les images

Android reconnaît les images aux formats PNG, JPEG et GIF, bien que GIF soit officiellement déconseillé ; il est généralement préférable d'utiliser le format PNG qui est très fortement encouragé par la documentation Android. Les images peuvent être utilisées partout où nous attendons un objet de type Drawable, comme pour une image et le fond d'une ImageView ou bien pour un bouton de type ImageButton.

L'utilisation des images consiste simplement à placer les fichiers images dans les répertoires res/drawable puis à y faire référence comme des ressources, à l'image de ce que nous avons fait pour les icônes.

  1. Dans les fichiers de description XML, vous pouvez les désigner par @drawable/nom_fichier, où nom_fichier est le nom de base du fichier (le nom de la ressource correspondant au fichier res/drawable/horloge.png est donc @drawable/horloge).
  2. Dans le code Java, il suffit de préfixer le nom de base du fichier par R.drawable lorsque vous avez besoin de l'identifiant ressource (R.drawable.horloge par exemple).
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/bouton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:onClick="changerHeure"
    android:src="@drawable/horloge"
/>

A partir d'Android 1.6, le kit de développement propose des possibilités étendues pour la gestion des résolutions d'écran hétérogènes dues à des appareils Android équipés de matériels aux tailles et caractéristiques différentes. Pour ce faire, la plate-forme classe les tailles d'écran et leurs résolutions en trois catégories.

Concernant les tailles d'écran, le fichier de configuration dispose d'une balise <support-screens> qui grâce aux attributs android:smallScreens, android:normalScreens et android:largeScreens permet de spécifier quelle(s) taille(s) d'écran votre application supporte.

Concernant les résolutions, chacune de ces catégories (low, medium, high) dispose d'un répertoire où enregistrer vos ressources, respectivement comme nous l'avons déjà dit : drawable-ldpi, drawable-mdpi et drawable-hdpi.

Les animations

La déclaration d'animations est plus compliquée, mais le résulat n'est pas décevant avec des possibilités de rotation, de fondu, de transalation et de changement de taille.

Les animations sont placées dans un ou plusieurs fichiers dans le répertoire res/anim. Voici les quatres types d'animations proposées :

  1. <alpha> : permet de faire un fondu,
  2. <scale> : définit un changement de facteur d'échelle,
  3. <translate> : permet de déplacer l'élément,
  4. <rotate> : propose une rotation avec un point pivot et un angle en degré.

Android propose également des attributs sur chacun de ces éléments permettant de réaliser de nombreux réglages comme la durée, la répétition, etc. Nous développerons largement ce type de ressource dans un chapitre dédié. A l'attendant, voici un exemple d'animation de type fondu avec la transparence qui passe de 0 à 1 :

res/anim/animations.xml
<?xml version="1.0" encoding="UTF-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
       android:interpolator="@android:anim/linear_interpolator"
       android:fromAlpha="0.0"
       android:toAlpha="1.0"
       android:duration="2000"
/>
Les couleurs

De manière à faciliter les évolutions graphiques, il est préconisé de définir les différentes couleurs utilisées dans le cadre d'un projet au travers de fichiers ressources dédié. Son nom n'a pas d'importance, habituellement il est nommé colors.xml et se trouve dans le répertoire res/values.

Les couleurs d'Android s'expriment en valeur RGB hexadécimales et peuvent éventuellement indiquer un canal alpha. Vous avez le choix entre des valeurs hexadécimales d'un seul caractère ou de deux caractères, ce qui donne donc quatre formats possibles :

  1. #RGB : Rouge-Vert-Bleu / Valeur hexadécimale sur 1 caractère [0,15].
  2. #ARGB : Alpha(transparence)-Rouge-Vert-Bleu / Valeur hexadécimale sur 1 caractère [0,15].
  3. #RRGGBB : Rouge-Vert-Bleu / Valeur hexadécimale sur 2 caractères [0,255].
  4. #AARRGGBB : Alpha(transparence)-Rouge-Vert-Bleu / Valeur hexadécimale sur 2 caractères [0,255].

Il suffit d'ajouter des éléments <color> au fichier de ressource afin de les gérer comme des ressources. Ces éléments doivent également posséder un attribut name afin de spécifier un nom unique à la couleur et contenir la valeur RGB.

res/values/couleurs.xml
<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <color  name="jaune_orange">#FFD555</color>
    <color  name="vert_foret">#005500</color>
    <color  name="ambre_fonce">#8A3324</color>
</resources>

Je rappelle que dans un fichier de description, ces couleurs peuvent ensuite être désignées par @color/nom_couleur, où nom_couleur est le nom unique de la couleur (ambre_fonce, par exemple). En java prefixez ce nom unique par R.color :

Resources.getColor(R.color.vert_foret);

Les dimensions

Nous l'avons vu, Android utilise les dimensions en de nombreux endroits pour décrire des distances, comme la valeur de remplissage d'un widget. Bien que nous utilisions souvent le pixel comme unité de mesure (10px pour 10 pixels, par exemple), vous pouvez en choisir d'autres :

  1. in et mm indiquent, respectivement, des pouces et des millimètres, d'après la taille physique de l'écran.
  2. pt représente des points, c'est-à-dire 1/72e de pouce en terme typographiques, là aussi d'après la taille physique de l'écran.
  3. dp (device-independant pixel) et sp (scale-independant pixel) indiquent des pixels indépendants du terminal - un pixel est égal à un dp pour un écran de 160 dpi, le facteur d'échelle reposant sur la densité de l'écran (les pixels indépendants de l'échelle tiennent également compte de la taille de la police choisie par l'utilisateur).

Pour encoder une dimension comme ressource, ajoutez un élément <dimen> avec un attribut name nommant de façon unique cette ressource. Le contenu de cet élément est un texte représentant la valeur de la dimension :

res/values/dimensions.xml
<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <dimen  name="fin">10px</dimen>
    <dimen  name="epais">250mm</dimen>
</resources>

Dans le fichier de description, les dimensions peuvent être référencées par @dimen/nom_dimension, où nom_dimension est le nom unique de la ressource (fin ou epais, dans l'exemple précédent). Dans le code Java, il suffit d'utiliser le nom unique préfixé par R.dimen :

Resources.getDimen(R.dimen.fin);

Les tableaux

Les ressources tableaux sont conçues pour contenir des listes de chaînes simples, comme une liste de civilité par exemple (Mr, Mme, Mlle, Dr, etc.).

Dans le fichier ressource, vous avez besoin d'un élement <string-array> ayant un attribut name afin de spécifier le nom unique au tableau. Cet élément doit avoir autant de fils <item> qu'il y a d'éléments dans le tableau. Le contenu de chacun de ces fils est la valeur de l'entrée correspondante :

res/values/tableaux.xml
<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <string-array  name="civilite">
        <item>Mr</item>       
        <item>Mme</item>
        <item>Mlle</item>
        <item>Dr</item>
    </string-array>
</resources>

Dans le code Java, il suffit d'utiliser le nom unique préfixé par R.array.nom_tableau pour obtenir un tableau de String contenant tous les éléments du tableau nom_tableau :

Resources.getStringArray(R.array.civilite);

Les chaînes de caractères

Terminons ce chapitre par la mise en place de ressources chaînes de caractères. Vous placez l'ensemble de vos chaînes dans un fichier de description XML situé dans le répertoire res/values (le plus souvent res/values/strings.xml).

Comme pour les autres ressources, la racine du document est l'élément <resources>, qui a autant de fils <string> qu'il y a de chaînes à encoder comme ressource. L'élément <string> possède également un attribut name qui sert à identifier le nom unique de la chaîne. Le contenu de cet élément est le texte de la chaîne.

  1. Le seul point épineux concerne la présence de guillemets ou d'apostrophe dans le texte de la chaîne car vous devrez alors les protéger en les préfixant d'un antislash (Obtention de l\'heure). Dans le cas de l'apostrophe, vous pouvez également placer tout le texte entre guillemets ("Obtention de l'heure").
  2. Comme pour les autres ressources, vous pouvez ensuite faire référence à une ressource chaîne depuis un fichier de description sous la forme @string/nom_chaîne ou y accéder depuis votre code Java à l'aide de la méthode getString(), en lui passant l'identifiant de la ressource chaîne, c'est-à-dire son nom unique préfixé par R.string (R.string.nom_chaîne).
  3. Formats de chaînes : Comme les autres implémentations du langage Java, la machine virtuelle Dalvik d'Android reconnaît les formats de chaînes. Ces formats sont des chaînes contenant des marqueurs d'emplacements et seront remplacés lors de l'exécution par des données variables (Mon nom est %1$s, par exemple). Vous pouvez utiliser pour ces formats des chaînes normales stockées sous forme de ressources.

Pour ce dernier point, il s'agit finalement de textes paramétrés. La syntaxe utilisée est la même que lorsque nous utilisons la méthode printf() que nous avons rencontré dans le langage Java, mise à part que le chiffre proposé correspond au numéro de la variable. Si vous avez deux paramères dans votre texte, le premier est identifié %1 et le deuxième %2. Voici d'ailleurs les cours relatifs à cette méthode printf().

A titre d'exemple, je vous propose de modifier le projet précédent. Effectivement, l'intitulé du bouton à l'issu d'un clic affiche une date formatée. Jusqu'à présent, ce formatage était exprimé à partir du code Java. Puisqu'il s'agit d'avoir une représentation particulière, c'est normalement au niveau de la vue qu'il faut s'en préoccuper et non plus dans le code lui-même. Je vous propose donc de mettre en oeuvre une ressource chaîne de caractères paramétrée qui tient compte de l'affichage de la date tel que vous le voyez ci-dessous :

res/values/strings
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Heure</string>
    <string name="texte_bouton">Cliquez sur le bouton</string>
    <string name="temps">%1$tA %1$te %1$tB, %1$tT</string>
</resources>
  1. Dans le fichier strings.xml, je rajoute une nouvelle ressource chaîne, nommée temps, dans laquelle je formate une information temporelle.
  2. Vous remarquez que cette ressource possède systématiquement le paramètre %1, ce qui veut dire qu'un seul paramètre est pris en compte.
  3. Toujours au niveau de ce paramètre, vous trouvez systématiquement les symboles $t qui signifie qu'il s'agit bien d'un paramètre de type temporel. A l'issu de cette évocation, le caractère qui suit sert à identifier quel est le paramètre de temps que vous désirez visualiser. Ainsi :
  4. $tA --> jours de la semaine, écriture complète comme jeudi.
  5. $te --> jours du mois sur deux chiffres, comme 28.
  6. $tB --> nom du mois en toute lettre, écriture complète comme juillet.
  7. $tT --> heure du jours avec l'heure, les minutes et les secondes, comme 13:59:53.
  8. Pour en savoir plus sur ces paramètres spécifiques.
fr.manu.heure.Heure
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.util.Date;

public class Heure extends Activity {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      bouton = (Button)findViewById(R.id.bouton);  
   }

   public void changerHeure(View v) {
      String date = getString(R.string.temps, new Date());
      bouton.setText(date);
   }
}
  1. Au niveau du code, nous avons supprimé l'écriture propre au formatage de la date. Par contre, maintenant nous utilisons en lieu et place la méthode getString() avec deux paramètres (il peut y en avoir plus de deux si le besoin s'en fait sentir).
  2. Le premier précise la ressource chaîne qui vous intéresse sous forme de constante entière déterminée par la classe R.
  3. Le deuxième paramètre correspond à la variable à prendre en compte pour l'élaboration de la chaîne paramétrée. Notre chaîne paramétrée est donc construire avec la date et l'heure actuelle.
  4. Cette chaîne créée peut donc maintenant servir comme nouvel intitulé de notre bouton.
  5. La méthode getString() peut avoir un nombre quelconque de paramètre suivant le nombre d'argument à prendre en compte dans votre chaîne paramétrée.

Je le rappelle, cette façon de procéder permet de séparer l'aspect visuel du traitement en coulisse réalisé en code Java. Ainsi, si vous désirez ne plus afficher le jour de la semaine dans l'intitulé de votre bouton, il suffit d'enlever ce paramètre de formatage dans le document strings.xml sans changer le code Java déjà compilé.

 

Choix du chapitre Widgets de base

Chaque environnement possède ses propres composants graphiques : champs de saisie, labels, boutons, etc. Celui d'Android possède également les siens, qui offrent bien entendu les mêmes fonctionnalités, qui sont appelés, nous l'avons découvert, des widgets. Nous allons passer en revu un certain nombre pour s'habituer à connaître leurs principales propriétés.

Les vues

Avant de s'intéresser à des widgets plus ou moins spécialisés, je vous propose de prendre en compte tout ce qu'ils ont en commun. Nous allons donc nous attarder sur la classe de base de tous les widgets, c'est-à-dire la classe View. Je vous propose de décrire ainsi toutes les propriétés communes qui sont accessibles soit dans le code Java, soit au travers d'un fichier de description XML que nous élaborons dans le répertoire res/layout.

  1. android:alpha : setAlpha(float) : spécifie le niveau de transparence de 0 (totalement transparent) à 1 (totalement opaque).
  2. android:background : setBackgroundRessource(int) : utilise une ressource comme fond.
  3. android:clickable : setClickable(boolean) : fait en sorte que l'élément soit réactif à un clic de souris.
  4. android:fadingEdge : setVerticalFadingEdgeEnabled(boolean) : les bords, notamment le haut et le bas deviennent progressivements transparents.
  5. android:focusable : setFocusable(boolean) : le composant peut prendre le focus.
  6. android:id : setId(int) : spécifie un identifiant au widget.
  7. android:minHeight : hauteur minimale que le composant doit obligatoirement avoir.
  8. android:minWidth : largeur minimale que le composant doit obligatoirement avoir.
  9. android:onClic : spécification de la méthode à appeler automatiquement lors d'un clic de la souris sur le composant.
  10. android:padding (paddingBottom, paddingLeft, paddingRight, paddingTop) : setPadding(int, int, int, int) : spécification d'une marge interne.
  11. android:rotation (rotationX, rotationY) : setRotation(float) : rotation de la vue en degré.
  12. android:scaleX, android:scaleY : setScale(float) : changement d'échelle suivant l'axe des X et des Y.
  13. android:transformPivotX, android:transformPivotY : setPivotX(float), setPivotY(float) : rotation et changement d'échelle autour d'un axe de rotation.
  14. android:translationX, android:translationY : setTranslationX(float), setTranslationY(float) : déplacement de la vue suivant l'axe des X et suivant l'axe des Y.
  15. android:visibility : setVisibility(int) : contrôle la visibilité initiale de la vue.

Les labels

Le label est le widget le plus simple et il est représenté par la classe TextView. Il s'agit de chaînes de caractères non modifiables par l'utilisateur. Ils servent souvent à identifier les widgets qui leur sont adjacents ("Nom : ", par exemple placé à côté d'un champ de saisie).

En Java, un label est un objet de la classe TextView. Cependant, ils seront le plus souvent générés à partir des fichiers de descriptions dans le répertoire res/layout en ajoutant un élément XML <TextView> doté au moins de la propriété android:text pour définir la chaîne de caractères qui lui est associée. Un élément <TextView> possède de nombreux autres attributs qui sont bien entendu accessibles au travers d'une méthode associée dans le code Java :

  1. android:autoLink : setAutoLinkMask(int) : contrôle éventuellement si le texte correspond à un lien quelconque, comme une adresse email ou une URL qui, le cas échéant, est converti en élément cliquable.
  2. android:autoText : setKeyListener(KeyListener) : contrôle si le champ doit fournir une correction automatique de l'orthographe.
  3. android:capitalize : setKeyListener(KeyListener) : demande que le champ mette automatiquement en majuscule la première lettre de son contenu.
  4. android:cursorVisible : setCursorVisible(boolean) : permet de rendre où pas le curseur visible. Par défaut, le curseur est visible.
  5. android:digits : setKeyListener(KeyListener) : permet, dans le cas où l'élément est éditable, de réaliser une saisie qui prend en compte uniquement certains chiffres.
  6. android:drawableBottom : setCompoundDrawablesWithIntrinsicBounds(int, int, int, int) : dessin à tracer en dessous du texte.
  7. android:drawableLeft : setCompoundDrawablesWithIntrinsicBounds(int, int, int, int) : dessin à tracer à gauche du texte.
  8. android:drawablePadding : setCompoundDrawablePadding(int) : spécification de la marge entre le dessin et le texte.
  9. android:drawableRight : setCompoundDrawablesWithIntrinsicBounds(int, int, int, int) : dessin à tracer à droite du texte.
  10. android:drawableTop : setCompoundDrawablesWithIntrinsicBounds(int, int, int, int) : dessin à tracer en haut du texte.
  11. android:editable : permet de définir cet élément comme champ de saisie.
  12. android:gravity : setGravity(int) : permet de spécifier la stratégie de placement du texte lorsque ce dernier est plus petit que le conteneur minimum prévu pour le composant (top, bottom, left, right, center_vertical, fill_vertical, center_horizontal, fill_horizontal, center, fill, clip_vertical et clip_horizontal).
  13. android:height : setHeight(int) : permet de spécifier exactement la hauteur du composant au pixel près.
  14. android:hint : setHint(int) : propose un texte par défaut lorsque le champ est vierge.
  15. android:inputType : setRawInputType(int) : permet de spécifier le type de clavier à prendre en compte, alphanumérique, uniquement que des chiffres, etc.
  16. android:maxHeight : setMaxHeight(int) : définit la hauteur maximale du composant.
  17. android:maxLength : setMaxLength(int) : définit la longueur maximale du texte en terme de nombre de caractères.
  18. android:maxWidth : setMaxWidth(int) : définit la largeur maximale du composant.
  19. android:minHeight : setMinHeight(int) : définit la hauteur minimale du composant.
  20. android:minLines : setMinLines(int) : définit le nombre de lignes minimales.
  21. android:minWidth : setMinWidth(int) : définit la largeur minimale du composant.
  22. android:numeric : setKeyListener(KeyListener) : spécifie qu'il s'agit d'un champ de saisie pour des valeurs numériques (avec virgule).
  23. android:password : setTransformationMethod(TransformationMethod) : s'il s'agit d'un champ de saisie, chaque caractère tapé est remplacé par un point afin d'éviter de voir le mot de passe introduit.
  24. android:phoneNumber : setKeyListener(KeyListener) : spécifie qu'il s'agit d'une saisie d'un numéro de téléphone.
  25. android:scrollHorizontally : setScrollHorizontaly(boolean) : met en place un acenseur horizontal afin que le texte reste sur une seule ligne.
  26. android:shadowColor : setShadowLayer(float, float, float, int) : place une ombre derrière le texte.
  27. android:shadowDx : setShadowLayer(float, float, float, int) : décalage de l'ombre suivant l'axe des X.
  28. android:shadowDy : setShadowLayer(float, float, float, int) : décalage de l'ombre suivant l'axe des Y.
  29. android:shadowRadius : setShadowLayer(float, float, float, int) : rayon de l'ombre autour du texte.
  30. android:singleLine : setTransformationMethod(TransformationMethod) : pour indiquer si la saisie ne s'effectue que sur une seule ou plusieurs lignes, autrement dit, est-ce que l'appui sur la touche "Entrée" vous place-t-il sur le widget suivant ou ajoute-t-il une nouvelle ligne ?
  31. android:text : setText(CharSequence, TextView.BufferType) : définition du texte associé à ce widget.
  32. android:textColor : setTextColor(int) : permet de définir la couleur du texte, au format RGB hexadécimal (#FF0000 pour un texte en rouge, par exemple).
  33. android:textSize : setTextSize(int, float) : taille du texte.
  34. android:textStyle : setTypeface(Typeface) : définition du style de la police, si le texte doit être en gras, en itallique ou les deux (bold, italic).
  35. android:typeface : setTypeface(Typeface) : définition du type de police (normal, sans, serif, monospace).
  36. android:width : setWidth(int) : définition de la largeur du widget en pixels.
Sur chacun de ces widgets, je vous proposerez systématiquement un petit exemple afin de bien comprendre l'intérêt de ces propriétés particulières. Tous les réglages seront effectués au travers du fichier de description res/layout/main.xml. Du coup, je mettrais en oeuvre systématiquement le même projet dont le code Java de l'activité est réduit à sa plus simple expression.
fr.manu.test.TestWidgets.java
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;

public class TestWigets extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Dès que l'activité est créée elle génère tout ce qui est décrit dans le fichier res/layout/main.xml dont voici le contenu.

  1. Vous remarquez que l'attribut android:textStyle est une combinaison de la constante bold et italic dont la fusion est assurée par le caractère ou logique "|".
  2. Dans le même ordre d'idée, au niveau de l'attribut android:gravity, nous aurions pu également écrire "center|top" au lieu de "center_horizontal".
  3. Pour générer une ombre portée, vous êtes obligé de décrire tous les attributs qui sont proposés ici.

Les boutons

Nous avons déjà utilisé le widget Button dans les deux chapitres précédents. Button étant une sous-classe de TextView, tout ce qui a été dit dans la section précédente concernant le formatage du texte s'applique également au texte d'un bouton.

Cependant, Android 1.6 ajoute une nouvelle fonctionnalité permettant de déclarer un écouteur "de clic" (Listener) pour un bouton. Outre l'approche classique consistant à définir un objet (comme l'activité) comme l'implémentation de l'interface View.OnClicListener, nous pouvons désormais utiliser une approche plus simple :

  1. Définir une méthode publique de l'activité qui prend un unique paramètre View et qui renvoie void.
  2. Dans l'élément Button du fichier de positionnement XML, inclure l'attribut android:onClic en précisant le nom de cette méthode afin qu'elle qui soit automatiquement appelée lors d'une action sur ce bouton.
Je reprends tout simplement le projet des chapitres précédents en plaçant cette fois-ci une image de fond :

fr.manu.heure.Heure.java
package fr.manu.heure;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.util.Date;

public class Heure extends Activity {
   private Button bouton;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      bouton = (Button)findViewById(R.id.bouton);  
   }

   public void changerHeure(View v) {
      String date = getString(R.string.temps, new Date());
      bouton.setText(date);
   }
}

Les images

Android dispose de deux widgets permettant d'intégrer des images dans les activités : ImageView et ImageButton. Comme leur nom l'indique, il s'agit, respectivement, des équivalents images de TextView et Button.

Chacun d'eux possède un attribut android:src permettant de préciser l'image utilisée. Cet attribut désigne généralement une ressource graphique, un drawable. Vous pouvez également configurer le contenu de l'image en utilisant une URI d'un fournisseur de contenu, via un appel à la méthode setImageURI().

ImageButton, une sous-classe d'ImageView, lui ajoute les comportements d'un Button standard pour répondre aux clics et autres actions. Voici ci-dessous leurs principales propriétés :

  1. android:adjustViewBounds : setAdjustViewBounds(boolean) : En cas de validation permet de respecter le ratio entre la largeur et la hauteur de l'image.
  2. android:baseline : setBaseline(int) : réglage du décalage par rapport à la ligne de base de l'image.
  3. android:baselineAlignBottom : setBaselineAlignBottom(boolean) : permet d'aligner le bas de l'image sur la ligne de base du composant.
  4. android:cropToPadding : si c'est validé, permet de recadrer l'image en tenant compte des marges.
  5. android:maxHeight : setMaxHeight(int) : définit la hauteur maximale du composant.
  6. android:maxWidth : setMaxWidth(int) : définit la largeur maximale du composant.
  7. android:scaleType : setScaleType(ImageView.ScaleType) : définit comment l'image se reéchantillonne pour correspondre à la dimension du composant lui-même.
  8. android:src : setImageResource(int) : permet de situer l'endroit où se trouve le fichier image.
  9. android:tint : setColorFilter(int, PorterDuff.Mode) : détermine la couleur qui permet de teinter l'image.
Je vous propose de prendre l'image correspondant à l'horloge. Les réglages proposés permettent d'avoir l'image la plus grande possible en respectant bien entendu le ratio, avec une couleur de fond verte et une teinte sur l'horloge elle-même, également en vert (avec une transparence).

Champs de saisie

Outre les boutons et les labels, les champs de saisie forment le troisième pilier de la plupart des outils de développement graphiques. Avec Android, ils sont représentés par le widget EditText, qui est une sous-classe de TextView.

Toutes les propriétés relatives à la saisie des valeurs font déjà partie des propriétés de TextView. Elles deviennent automatiquement opérationnelles lorsque nous créons une instance de la classe EditText.

Android 1.5 a introduit le framework des méthodes de saisie IMF (Input Method Framework), que nous désignons sous le terme de claviers logiciels bien qu'il ne soit pas rigoureusement exact car IMF peut être utilisé pour la reconnaissance d'écriture ou tout autre moyen de saisir du texte via l'écran d'un mobile.

IMF sait gérer toutes les situations : s'il n'existe pas de clavier physique sur votre mobile, un IME (Input Method Editor) apparaîtra lorsque l'utilisateur tape sur un EditText.

  1. Android 1.1 et les versions précédentes offraient de nombreux attributs pour les widgets EditText (en réalité, définis sur un TextView) afin de contrôler leur style de saisie, comme android:password, afin de spécifier que ce champ de saisie sert à saisir un mot de passe (et ainsi que le texte saisie doit donc être caché des yeux indiscrets).
  2. A partir d'Android 1.5 et IMF, la plupart des modes de saisie ont été regroupés dans un seul attribut android:inputType dont la valeur est composée d'une classe et de modificateurs, séparés par le caractère "pipe" (|). La classe décrit généralement ce que l'utilisateur est autorisé à saisir et détermine l'ensemble de base des touches disponibles sur le clavier logiciel. Les classes possibles sont les suivantes : text (par défaut), number, phone, date, time, etc.
  3. La plupart d'entre elles offrent un ou plusieurs modificateurs pour affiner ce que pourra saisir l'utilisateur. Par exemple, text|textCapCharacters stipule que la saisie attendue est de type texte et que ce dernier sera entièrement transformé en lettres majuscules.
classes et modificateurs associés à l'attribut android:inputType
  1. text : saisie d'un simple texte sans analyse particulière.
  2. number : saisie d'une valeur numérique.
  3. phone : saisie d'un numéro de téléphone.
  4. datetime : saisie d'une date complète avec l'heure.
  5. date : saisie de la date seule, sans l'heure.
  6. time : saisie de l'heure uniquement, sans la date.
Je vous propose de prendre quelques petits exemples avec une seule zone de saisie qui nous permettrons de bien comprendre les types de saisie possibles.
Saisie d'un numéro de téléphone

En choisissant la valeur android:inputType appropriée, vous pouvez offrir aux utilisateurs le clavier qui convient le mieux à ce qu'ils doivent saisir, comme ici le clavier virtuel correspond à la saisie d'un numéro de téléphone.

Saisir un nom tout en majuscule
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:inputType="textPersonName|textCapCharacters"
    android:hint="Votre nom"
    android:background="@color/fond"
    android:textColor="@color/texte"
    android:textSize="22dp"
    android:textStyle="bold"
/>











Saisir un nom avec seulement la première lettre en majuscule
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:inputType="textPersonName|textCapWords"
    android:hint="Votre nom"
    android:background="@color/fond"
    android:textColor="@color/texte"
    android:textSize="22dp"
    android:textStyle="bold"
/>











  
Saisir un e-mail
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:inputType="textEmailAddress"
    android:hint="Votre email"
    android:background="@color/fond"
    android:textColor="@color/texte"
    android:textSize="22dp"
    android:textStyle="bold"
/>











  
Saisir une date
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:inputType="date"
    android:hint="Date de naissance"
    android:background="@color/fond"
    android:textColor="@color/texte"
    android:textSize="22dp"
    android:textStyle="bold"
/>











  

Les champs numériques et date limitent les touches au clavier numérique et à un ensemble de symboles autorisés pour un champ donné.
.

Dire à Android où aller

Vous remarquez que la dernière touche, en bas à droite, affiche systématiquement "OK". Vous avez la possibilité de contrôler ce qui s'affiche sur cette touche, appelée bouton accessoire.

Par défaut, le bouton accessoire d'un EditText avec un attribut android:inputType sera "Suivant" pour vous permettre de passer au champ EditText suivant sur votre activité en possède un autre, ou "OK" si vous vous trouvez sur le dernier EditText de l'écran. Par défaut, "Suivant" déplace le focus sur le widget EditText suivant et "OK" ferme l'IME. Vous pouvez toutefois indiquer manuellement ce qui s'affichera à l'aide de l'attribut android:imeOptions.

Valeurs possibles pour cet attribut android:imeOptions
  1. normal : C'est le comportement par défaut qui est préconisé sur le bouton accessoire qui s'adaptera en fontion du type d'entrée saisie.
  2. actionUnspecified : Aucune action n'est proposée, donc aucun bouton associé. L'intitulé "OK" est proposé au bouton accessoire.
  3. actionNone : Aucune action n'est proposée, donc aucun bouton associé. L'intitulé du bouton accessoire représente le symbole de validation.
  4. actionGo : Correspond à une navigation en relation avec une URL. L'intitulé "OK" est proposé au bouton accessoire.
  5. actionSearch : Correspond à une action de recherche en rapport avec la valeur saisie. Cette fois-ci, le bouton accessoire représente le symbole d'une loupe.
  6. actionSend : Correspond à la gestion de l'envoi d'un email. Le bouton accessoire aura alors la valeur "Envoyer".
  7. actionNext : Demande à passer au champ de saisie suivant. Le bouton accessoire aura justement la valeur "Suivant".
  8. actionDone : Fin des saisies. L'intitulé "OK" est alors proposé au bouton accessoire.
  9. actionPrevious : Demande à passer au champ de saisie précédent. Le bouton accessoire aura justement la valeur "Précédent".
(exemple) Modification de la saisie de l'e-mail afin qu'il puisse être envoyé directement
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:inputType="textEmailAddress"
    android:imeOptions="actionSend"
    android:hint="Votre e-mail"
    android:background="@color/fond"
    android:textColor="@color/texte"
    android:textSize="22dp"
    android:textStyle="bold"
/>

Cases à cocher

La case à cocher classique peut être dans deux états : cochée ou décochée. Un clic sur la case inverse son état pour indiquer un choix.

Le widget CheckBox d'Android permet d'obtenir ce comportement. Comme il dérive indirectement de la classe TextView, les propriétés de celles-ci (comme android:textColor, par exemple) permettent également de formater ce widget.

Cette classe CheckBox, comme la classe RadioButton que nous regarderons dans la prochaine session, hérite de la classe CompoundButton. Cette dernière dispose de méthodes très utiles dans le code Java pour connaître l'état d'une case ou d'un bouton :

  1. isChecked() : pour savoir si la case est cochée.
  2. setChecked() : pour forcer la case dans l'état cochée ou décochée.
  3. toggle() : pour inverser l'état de la case, comme si l'utilisateur avait cliqué dessus.
  4. setOnCheckedChangeListener() : Permet d'enregistrer un objet écouteur de type OnCheckedChangeListener qui prévient du changement d'état de la case à cocher.
Je vous propose de prendre un petit exemple qui récupère l'état de la case à cocher. Si cette dernière est cochée, le texte passe automatiquement en gras et en italique. Pour cela, vous êtes obligé de prendre en compte l'événement issu du changement d'état de cette case à cocher dans le code Java en implémentant l'interface OnCheckedChangeListener.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gras"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textSize="28dp"
    android:textColor="@color/texte"
    android:text="Gras et italique"
/>
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.widget.*;
import android.widget.CompoundButton.OnCheckedChangeListener;

public class TestWidgets extends Activity implements OnCheckedChangeListener {
    private CheckBox gras;
            
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        gras = (CheckBox)  findViewById(R.id.gras);
        gras.setOnCheckedChangeListener(this);
    }

    public void onCheckedChanged(CompoundButton vue, boolean etat) {
        if (etat) gras.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC);
        else gras.setTypeface(Typeface.SANS_SERIF, Typeface.NORMAL);
    }
}
  1. Vous remarquez que c'est l'activité elle-même qui sert d'écouteur pour les changements d'état de la case à cochée car elle implémente l'interface CompoundButton.OnCheckedChangeListener.
  2. Les changements sont effectivement pris en compte dès la création de l'activité gràce à l'écriture gras.setOnCheckedChangeListener(this).
  3. La méthode de rappel de l'écouteur est onCheckedChanged(CompoundButton vue, boolean etat) : elle reçoit la case qui a changé d'état et son nouvel état. Ici, nous nous contentons de modifier le style du texte de la case au moyen de sa méthode setTypeface().
  4. Cliquer sur la case modifie donc instantanément son style de texte, comme le montre les figures ci-dessus.

Au lieu de créer un attribut de type CheckBox, nous pouvons agir directement sur la case à cocher au travers du paramètre vue proposé par la méthode onCheckedChanged(). Voici la modification du code Java ci-dessous :

fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.widget.*;
import android.widget.CompoundButton.OnCheckedChangeListener;

public class TestWidgets extends Activity implements OnCheckedChangeListener {           
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        CheckBox gras = (CheckBox)  findViewById(R.id.gras);
        gras.setOnCheckedChangeListener(this);
    }

    public void onCheckedChanged(CompoundButton vue, boolean etat) {
        if (etat) vue.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC);
        else vue.setTypeface(Typeface.SANS_SERIF, Typeface.NORMAL);
    }
}

Boutons radio

Comme les cases à cocher, les boutons radio ont deux états, mais ils peuvent être regroupés de sorte qu'un seul bouton radio puisse être coché par groupe à un instant donné.

Comme CheckBox, RadioButton (qui représente un bouton radio) hérite de la classe CompoundButton, qui dérive elle-même de TextView. Ainsi, comme beaucoup, toutes les propriétés standard de TextView pour la police, le style, la couleur, etc. s'appliquent donc également aux boutons radio.

Vu que RadioButton hérite de CompoundButton, comme nous l'avons vu dans la session précédente, vous pouvez appeler la méthode isChecked() sur un bouton radio pour savoir s'il est coché, la méthode toggle() pour le sélectionner, etc. exactement comme avec une case à cocher.

La plupart du temps, les widgets RadioButton sont placés dans un conteneur RadioGroup qui permet de lier les états des boutons qu'il regroupe afin qu'un seul puisse être sélectionné à un instant donné. En affectant un identifiant android:id au RadioGroup dans le fichier de description XML, ce groupe devient accessible au code Java, qui peut alors lui appliquer les méthodes suivantes :

  1. check() : pour tester un bouton radio à partir de son identifiant (avec groupe.check(R.id.radio1)).
  2. clearCheck() : pour décocher tous les boutons du groupe.
  3. getCheckedRadioButtonId() : pour obtenir l'identifiant du bouton radio actuellement coché (cette méthode renvoie -1 si aucun bouton n'est coché).
  4. setOnCheckedChangeListener() : Permet d'enregistrer un objet écouteur de type RadioGroup.OnCheckedChangeListener qui prévient du changement d'état dans le groupe de boutons radio. Attention, cette méthode est vraiment associée à RadioGroup, Ce n'est donc pas la même que celle issue de CompoundButton. D'ailleurs, remarquez que l'écouteur est également différent puisqu'il est lui-même associé au groupe de boutons.
Nous allons prendre un autre exemple qui permet de bien connaître ces boutons radio. Cette fois-ci, vous avez la possibilité de choisir plus précisément votre style de texte.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/groupe"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:orientation="vertical"
    android:background="@color/fond">

    <RadioButton android:id="@+id/gras"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Gras"/>   
    <RadioButton android:id="@+id/italique"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Italique"/>   
    <RadioButton android:id="@+id/lesdeux"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Gras et italique"/>   
    <TextView  android:id="@+id/resultat"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:textSize="26dp"
        android:textColor="@color/texte"
        android:layout_gravity="center"
        android:text="Résultat du choix" />
</RadioGroup>
  1. Vous remarquez que les boutons radio sont bien imbriqué dans un élément <RadioGroup>.
  2. Un groupe de bouton étant un conteneur de type linéaire, vu qu'il hérite de la classe LinearLayout (les conteneurs seront traités dans le prochain chapitre), il est possible de placer d'autres élément comme ici un TextView afin de visualiser le résultat du choix effectué.
  3. Chaque élément possède son propre identifiant, il sera donc possible d'agir sur chacun d'entre eux au niveau du code Java.
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.widget.*;
import android.widget.RadioGroup.OnCheckedChangeListener;

public class TestWidgets extends Activity implements OnCheckedChangeListener {
    private TextView resultat;
            
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        resultat = (TextView)  findViewById(R.id.resultat);
        RadioGroup groupe = (RadioGroup)  findViewById(R.id.groupe);
        groupe.setOnCheckedChangeListener(this);
    }

    public void onCheckedChanged(RadioGroup groupe, boolean identifiant) {
        if (identifiant == R.id.gras) gras.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
        if (identifiant == R.id.italique) gras.setTypeface(Typeface.SANS_SERIF, Typeface.ITALIC);
        if (identifiant == R.id.lesdeux) gras.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC);
    }
}
  1. Vous remarquez que c'est l'activité elle-même qui sert d'écouteur pour les changements de l'ensemble du groupe de boutons car elle implémente l'interface RadioGroup.OnCheckedChangeListener.
  2. La méthode de rappel de l'écouteur est onCheckedChanged(RadioGroup groupe, boolean identifiant) : elle est différente de la méthode de rappel issue de la classe CompoundButton. Cette-ci reçoit en paramètres cette fois-ci le groupe de bouton ainsi que l'identifiant du bouton radio actif.

Vous remarquez que, au départ, aucun bouton du groupe n'est coché. Du coup, aucun style n'est proposé. Pour que l'application sélectionne l'un de ces boutons dès son lancement, il faut appeler soit la méthode setChecked() sur le RadioButton concerné, soit la méthode check() sur le RadioGroup à partir de la méthode onCreate() de l'activité. Pour que le bouton radio "gras" soit actif au départ, voici le changement à apporter :

fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.widget.*;
import android.widget.RadioGroup.OnCheckedChangeListener;

public class TestWidgets extends Activity implements OnCheckedChangeListener {
    private TextView resultat;
            
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        resultat = (TextView)  findViewById(R.id.resultat);
        RadioGroup groupe = (RadioGroup)  findViewById(R.id.groupe);
        groupe.setOnCheckedChangeListener(this);
        groupe.check(R.id.gras);
    }

    public void onCheckedChanged(RadioGroup groupe, boolean identifiant) {
        if (identifiant == R.id.gras) gras.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
        if (identifiant == R.id.italique) gras.setTypeface(Typeface.SANS_SERIF, Typeface.ITALIC);
        if (identifiant == R.id.lesdeux) gras.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC);
    }
}
Gestion des couleurs

Je vous propose un autre exemple qui permet de gérer les couleurs par programme, dans le code Java, toujours à l'aide des boutons radio.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/groupe"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:orientation="vertical">

    <RadioButton android:id="@+id/rouge"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="26dp"
        android:text="Rouge"/>   
    <RadioButton android:id="@+id/vert"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:textStyle="bold"
        android:textSize="26dp"
        android:text="Vert"/>   
    <RadioButton android:id="@+id/bleu"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:textStyle="bold"
        android:textSize="26dp"
        android:text="Bleu"/>   
</RadioGroup>
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.*;
import android.widget.RadioGroup.OnCheckedChangeListener;

public class TestWidgets extends Activity implements OnCheckedChangeListener {   
    private RadioButton bouton;
    
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        RadioGroup groupe = (RadioGroup)  findViewById(R.id.groupe);
        groupe.setOnCheckedChangeListener(this);
        bouton = (RadioButton) findViewById(R.id.rouge);
        groupe.check(R.id.rouge);      
    }
    
    public void onCheckedChanged(RadioGroup groupe, int identifiant) {   
       bouton.setTextColor(Color.WHITE);
       RadioButton boutonActif = (RadioButton) findViewById(identifiant);
       boutonActif.setTextColor(Color.argb(0x77, 0x00, 0x00, 0x00));
       bouton = boutonActif;
        if (identifiant == R.id.rouge) groupe.setBackgroundColor(Color.RED);
        if (identifiant == R.id.vert) groupe.setBackgroundColor(Color.rgb(0, 0x88, 0));
        if (identifiant == R.id.bleu) groupe.setBackgroundColor(Color.BLUE);       
    }
}
  1. Dans le code, les méthodes qui permettent de changer respectivement la couleur de fond et la couleur de texte sont setBackgroundColor() et setTextColor().
  2. Pour le choix de la couleur, vous pouvez prendre des couleurs prédéfinies dans les fichiers de descriptions au travers des constantes générées automatiquement dans la classe R, comme R.color.texte par exemple.
  3. Toutefois, cette technique ne fonctionne pas pour la méthode setBackgroundColor().
  4. Il existe aussi, comme dans le cas de la JVM standard, la classe Color qui possède des constantes prédéfinies, comme Color.RED, ainsi que des méthodes statiques où vous pouvez générer votre couleur suivant les tons rouge, vert, bleu désirés. Vous pouvez même ajouter une transparence. Les deux méthodes adaptés à ces deux situations sont respectivement rgb() et argb().

Affichage de l'heure

Pour afficher l'heure sans autoriser les utilisateurs à la modifier, utiliser les widgets DigitalClock ou AnalogClock. Il suffit de les placer dans votre descripteur de placement et de les laisser travailler indépendemment. Vous n'avez rien à préciser spécialement dans le code Java.

La classe AnalogClock hérite de la classe de base View alors que la classe DigitalClock hérite de la classe TextView. Pour cette dernière, nous pouvons donc faire plus de réglage associé au texte de l'horloge.

Voici deux exemples consécutifs qui nous permet d'afficher simplement l'heure avec des aiguilles ou sous forme digitale.

(Horloge avec aiguilles) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<AnalogClock xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
/>
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;

public class TestWidgets extends Activity {   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}
(Horloge digitale) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<DigitalClock xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textStyle="bold"
    android:textSize="22dp"
    android:textColor="@color/texte"
    android:background="@color/fond"
    android:gravity="center"
/>

Chronomètre

Si vous rechercher plutôt un chronomètre, le widget Chronometer est fait pour vous car il permet de mémoriser le temps écoulé à partir d'un point de départ : il suffit de lui dire quand démarrer à l'aide de la méthode start(), de s'arrêter avec la méthode stop() et, éventuellement, de redéfinir le format de la chaîne de texte affichée.

Le format d'affichage du chronomètre se réalise à partir de l'attribut android:format ou, dans le code Java, au travers de la méthode setFormat(). Vous pouvez proposer un intitulé totalement personnalisé. Il suffit juste de spécifier "%s" à l'endroit où vous désirez montrer le temps écoulé. Au départ, l'affichage du temps se fait uniquement avec les minutes et les secondes "MM:SS" . Si le temps écoulé dépasse l'heure, l'affichage du temps proposera l'heure en plus "H:MM:SS".

Voici un exemple simple de temps écoulé avec une base de temps de une seconde.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Chronometer xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/temps"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textStyle="bold"
    android:textSize="32dp"
    android:textColor="@color/texte"
    android:background="@color/fond"   
    android:gravity="center"
    android:format="Temps écoulé (%s)"
/>










fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Chronometer;

public class TestWidgets extends Activity {   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Chronometer temps = (Chronometer) findViewById(R.id.temps);
        temps.start();
    }
}

A noter que si vous désirez capturer un événement à chaque top d'horloge, il existe pour cela l'écouteur Chronometer.OnChronometerTickListener qui est une interface qui implémente la méthode onChronometerTick(Chronometer temps).

Saisir une date et une heure très simplement

Avec des terminaux ayant des capacités de saisie limitées comme les télépnones, il est très utile de disposer de widgets et de boîtes de dialogue capable d'anticiper ce que l'utilisateur veut taper. Cela minimise le nombre de frappes au clavier et de touches à l'écran et réduit les risques d'erreur (la saisie d'une lettre à la place d'un chiffre, par exemple).

Comme nous l'avons mentionné dans ce chapitre, EditText possède des variantes permettant de saisir des nombres ou des textes. Android dispose également de widgets (DatePicker, TimePicker) et des dialogues (DatePickerDialog, TimePiquerDialog) facilitant la saisie des dates et des heures respectivement.

  1. DatePicker et DatePickerDialog permettent de fixer une date de départ, sous la forme d'une année, d'un mois et d'un jour. Les mois vont de 0 (janvier) à 11 (décembre). Vous pouvez également préciser un écouteur (OnDateChangedListener ou OnDateSetListener) qui seront informé lorsqu'une nouvelle date a été choisie. Il vous appartient de stocker cette date quelque part, notamment si vous utilisez la boîte de dialogue, car vous n'aurez pas d'autre moyen d'obtenir ensuite une date choisie.
  2. De même, TimePicker et TimePickerDialog permettent de fixer l'heure initiale, que l'utilisateur peut ensuite ajuster sous la forme d'une heure (de 0 à 23) et de minutes (de 0 à 59). Vous pouvez indiquer si le format de la date choisi utilise le mode sur 12 heures avec un indicateur AM/PM ou le mode 24 heures (ce qui, aux Etats-Unis, est appelé "temps militaire" et qui est le mode utilisé partout ailleurs dans le monde). Vous pouvez également fournir un écouteur (OnTimeChangedListener ou OnTimeSetListener) pour être prévenu du choix d'une nouvelle heure, qui sera fournie sous la forme d'une heure et de minutes.
Propriétés spécifiques de android.widget.DatePicker
  1. android:calendarViewShown : Visualise ou pas le calendrier.
  2. android:endYear : fixe l'année incluse à ne pas dépasser, par exemple "2011".
  3. android:maxDate : fixe la date maximale affichable par le calendrier au format "mm/dd/yyyy".
  4. android:minDate : fixe la date minimale affichable par le calendrier au format "mm/dd/yyyy".
  5. android:spinnersShown : Visualise ou pas les boutons plus et moins.
  6. android:startYear : fixe l'année incluse de départ, par exemple "1959".

Vous pouvez ensuite récupérer les valeurs de la date au moyen des méthodes getCalendarView().getDate(), getDayOfMonth(), getMonth() et getYear() issues de la classe DatePicker.

Vous pouvez également récupérer les valeurs de l'heure au moyen des méthodes getCurrentHour() et getCurrentMinute() issues de la classe TimePicker. Par ailleurs, vous pouvez fixer l'heure au myen des setters respectifs : setCurrentHour() et setCurrentMinute().

Voici deux exemples consécutifs qui nous permet d'afficher simplement la date et l'heure avec les widgets spécialisés.

(DatePicker) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<DatePicker xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/naissance"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:endYear="2011"
    android:startYear="1941"
/>  
(TimePicker) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TimePicker xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/heure"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
/>
(TimePicker) fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TimePicker;

public class TestWidgets extends Activity {   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TimePicker heure = (TimePicker) findViewById(R.id.heure);
        heure.setIs24HourView(true);
    }
}
Boîte de dialogue de saisie de date DatePickerDialog

Nous allons reprendre les mêmes saisies mais cette fois-ci au travers d'une boîte de dialogue DatePickerDialog.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/date"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Date de naissance"
    android:onClick="choisirDate"
    android:textSize="22dp"
    android:textColor="@color/fond"
/>  
(DatePickerDialog) fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.*;
import android.widget.*;
import java.util.Calendar;

public class TestWidgets extends Activity {   
   private Calendar calendrier = Calendar.getInstance();
   private Button bouton;
   
   private DatePickerDialog.OnDateSetListener evt = new DatePickerDialog.OnDateSetListener() {
      public void onDateSet(DatePicker dialog, int annee, int mois, int jour) {
         calendrier.set(annee, mois, jour);
         bouton.setText(DateFormat.format("EEEE dd MMMM yyyy", calendrier));
      }
   };
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       bouton = (Button) findViewById(R.id.date);
   }
    
   public void choisirDate(View vue) {
       new DatePickerDialog(this, evt, 
               calendrier.get(Calendar.YEAR), 
               calendrier.get(Calendar.MONTH),
               calendrier.get(Calendar.DAY_OF_MONTH)).show();
   }
}
  1. Cette activité possède un certain nombre d'attributs.
  2. D'une part un objet calendrier représentant la mémorisation de la date saisie, avec en initialisation la date du jour.
  3. Ensuite une instance bouton représentant le bouton prédéfinie dans la vue proposé par le fichier de description XML. Ce bouton est initialisé durant la phase de création de l'activité, et lorsque nous agissons sur ce bouton, la méthode choisirDate() est automatiquement sollicité.
  4. Toujours au niveau des attributs, nous définissons un écouteur de type DatePickerDialog.OnDateSetListener. Cet écouteur sera utile lors de la création de la boîte de dialogue de type DatePickerDialog. Cet écouteur propose l'affichage de la date saisie directement sur le texte du bouton.
  5. Dans la méthode choisirDate(), nous créons temporairement la boîte de dialogue DatePickerDialog que nous affichons intantanément. Nous lui passons en paramètre, l'écouteur et les différentes valeurs propre à la date enregistrée.
Boîte de dialogue de saisie de l'heure TimePickerDialog

Nous allons reprendre les mêmes saisies mais cette fois-ci au travers d'une boîte de dialogue TimePickerDialog.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/heure"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Saisie de l'heure"
    android:onClick="choisirHeure"
    android:textSize="22dp"
    android:textColor="@color/fond"
/>  
(DatePickerDialog) fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.*;
import android.widget.*;
import java.util.Calendar;

public class TestWidgets extends Activity {   
   private Calendar calendrier = Calendar.getInstance();
   private Button bouton;
   
   private TimePickerDialog.OnTimeSetListener evt = new TimePickerDialog.OnTimeSetListener() {
      public void onTimeSet(TimePicker dialog, int heure, int minute) {
         calendrier.set(heure, minute);
         bouton.setText(DateFormat.format("hh 'h' mm 'mn'", calendrier));
      }
   };
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       bouton = (Button) findViewById(R.id.heure);
   }
    
   public void choisirHeure(View vue) {
       new TimePickerDialog(this, evt, 
               calendrier.get(Calendar.HOUR_OF_DAY), 
               calendrier.get(Calendar.MINUTE),
               true).show();
   }
}
  1. Nous retrouvons la même ossature que le projet précédente sur la saisie de date avec toutefois la prise en compte des heures et des minutes en lieu et place de l'année, du mois et du jour du mois.
  2. Pour cela, c'est la boîte de dailogue TimePickerDialog qui est utilisée. Par ailleurs, l'écouteur s'appelle maintenant OnSetTimeSetListener avec la méthode de rappel onTimeSet().
  3. Dans la méthode choisirHeure(), nous créons temporairement la boîte de dialogue DatePickerDialog que nous affichons intantanément. Nous lui passons en paramètre, l'écouteur et les différentes valeurs propres à l'heure enregistrée.
  4. Vous remarquez la présence de la valeur true afin de spécifier que nous désirons un format sur 24 heures.

Mesurer la progression

Si une opération doit durer un certain temps, les utilisateurs doivent pouvoir :

  1. Utiliser un thread en arrière plan,
  2. Être tenus au courant de la progression de l'opération, sous peine de penser que l'activité a un problème.

L'approche classique pour tenir les utilisateurs informés d'une progression consiste à utiliser une barre de progression ou un disque tournant (animation classique des principaux navigateurs web). Android dispose pour cela du widget ProgressBar.

  1. Une ProgressBar mémorise la progression, définie par un entier allant de 0 (aucune progression) à une valeur maximale définie par setMax() qui indique que l'opération s'est terminée.
  2. Par défaut, une barre de progression part de valeur 0, mais vous pouvez choisir une autre valeur de départ via un appel à sa méthode setProgress().
  3. Si vous préférez que la barre de progression soit indéterminée, passer la valeur true à sa méthode setIndeterminate(). Sauf avis contraire, l'indétermination est proposée par défaut et correspond à un affichage d'un disque tournant sans spécification de valeur particulière évoquant une attente de fin de traitement, à l'image des disques tournants dans les navigateurs web qui visualisent l'attente du téléchargement complet de la page web.
  4. Dans le code Java, vous pouvez fixer le montant de progression effectué via la méthode setProgress() ou incrémenter la progression courante d'une valeur déterminée via la méthode incrementProgressBy().
  5. La méthode getProgress(), quant à elle, permet de connaître la progression actuelle déjà effectuée.
Styles de présentation des barres de progression

Par défaut, nous venons de le dire, c'est le mode indéterminé qui est proposé par défaut. Il existe différents styles de présentation à l'image des barres de progression classique horizontales. Pour cela, vous devez utiliser l'attribut style. Attention, cet attribut n'utilise pas l'espace de nom android.

La syntaxe du style est bien entendu très rigoureuse. Voici un exemple pour afficher une barre de progression classique :
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/progression"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    style="@android:style/Widget.ProgressBar.Horizontal"
    android:max="10000"
/>

Voici ci-dessous la liste des styles possibles :

  1. Widget.ProgressBar.Horizontal : Barre de progression horizontale classique.
  2. Widget.ProgressBar.Small : Disque tournant de plus petite dimension que celui proposé par le style par défaut.
  3. Widget.ProgressBar.Large : Disque tournant de plus grande dimension que celui proposé par le style par défaut.
  4. Widget.ProgressBar.Inverse : Disque tournant dans le sens inverse, c'est-à-dire dans le sens anti-horaire.
  5. Widget.ProgressBar.Small.Inverse : Disque tournant dans le sens inverse, mais de petite dimension.
  6. Widget.ProgressBar.Large.Inverse : Disque tournant dans le sens inverse, mais de grande dimension.
Propriétés spécifiques de android.widget.ProgessBar
  1. android:animationResolution : Timeout entre les différentes visualisations de l'animation. Doit être un entier représentant le nombre de millisecondes, comme "100".
  2. android:indeterminate : permet de fixer le mode indéterminé, true ou false.
  3. android:indeterminateBehavior : précise comment le monde indéterminé doit se comporter lorsque la progression est à sa valeur maximale, valeur repeat qui impose la progression à repartir de 0, ou cycle qui dans ce cas là inverse la progression de la valeur maximale vers 0.
  4. android:indeterminateDrawable : visualisation utilisée dans le mode indéterminé.
  5. android:indeterminateDuration : Durée de l'animation dans le mode indéterminé.
  6. android:indeterminateOnly : Restreint uniquement sur le mode indéterminé au détriment du mode observationnel.
  7. android:interpolator :
  8. android:max : définit la valeur maximale que la barre de progression peut atteindre.
  9. android:maxHeight : hauteur maximale du widget.
  10. android:maxWidth : largeur maximale du widget.
  11. android:minHeight : hauteur minimale du widget.
  12. android:minWidth : lageur minimale du widget.
  13. android:progress : fixe la valeur de progression initiale entre 0 et la valeur maximale.
  14. android:progressDrawable : précise le contexte d'affichage graphique dans le mode progression.
  15. android:secondaryProgress : fixe une progression secondaire entre 0 et la valeur maximale.
Voici quelques exemples consécutifs qui nous permettent d'afficher une barre de progression qui visualise le temps écoulé (ou le temps restant).

Je vous propose de regardez dans un premier temps le code Java qui sera le même quelque soit les différents réglages que nous effectuerons dans la vue au moyen des fichiers de descriptions XML.

fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.graphics.Color;
import android.os.*;
import android.widget.*;

public class TestWidgets extends Activity {   
   private ProgressBar progression;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       progression = (ProgressBar) findViewById(R.id.progression);
       new CountDownTimer(progression.getMax(), 50) {
         @Override
         public void onTick(long tempsRestant) {
            progression.setProgress(progression.getMax() - (int)tempsRestant);
         }

         @Override
         public void onFinish() {
            progression.setProgress(0);
         }         
       }.start();
   }
}
  1. La première remarque concerne l'utilisation de la classe abstraite android.os.CountDownTimer qui, comme son nom l'indique, permet de réaliser un timer qui part d'une valeur de temps prédéfinie pour aboutir à 0 en choisissant une base de temps de décompte. Il s'agit donc d'un compte à rebour.
  2. Comme CountDownTimer est une classe abstraite, vous devez créer une nouvelle classe, ou comme ici créer un objet anonyme. Vous êtes alors dans l'obligation de redéfinir les méthodes abstraites onTick() qui sera sollicité à chaque top d'horloge défini par la base de temps, ainsi que la méthode onFinish() qui sera activée lorsque le temps total sera écoulé.
  3. La définition du temps à décompter ainsi que la base de temps se fait au moyen du constructeur de cette classe CountDownTimer. Effectivement, le premier paramètre précise le temps que va durer le timer en milliseconde, le deuxième paramètre précise la base de temps également en milliseconde.
  4. Le compte à rebour se fait avec l'attribut max de la barre de progression fixé dans le fichier de description au moyen de la méthode getMax() de ProgressBar.
  5. Â chaque top d'horloge, nous changeons la valeur de progression au moyen de la méthode setProgress() de la classe ProgressBar.
(Mode indéterminé) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/progression"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:max="10000"
/>
(Barre de progression classique) res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/progression"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    style="@android:style/Widget.ProgressBar.Horizontal"
    android:max="10000"
/>
Saisie d'une valeur parmi un intervalle SeekBar

La classe SeekBar hérite de ProgressBar mais, alors que ce dernier est un widget de sortie dont le but consiste à indiquer l'état d'une progression à l'utilisateur, SeekBar est un widget d'entrée qui permet de saisir une valeur prise parmi un intervalle donné.

L'utilisateur peut faire glisser le curseur ou cliquer sur l'un des côté pour le repositionner. Ce curseur désigne en réalité une valeur dans un intervalle donné - 0 à 100 par défaut, mais vous pouvez modifier la borne supérieure par un appel à setMax(). La méthode getProgress() renvoie la position courante et l'enregistrement. Vous pouvez également placer un écouteur SeekBar.OnSeekBarChangeListener qui prévient toute modification de la position du curseur.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:orientation="vertical"
 >
   <SeekBar android:id="@+id/saisie"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:max="50"
   />
   <TextView android:id="@+id/resultat" 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="0 %"
       android:textSize="22dp"
   />
</LinearLayout>
       



  
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.*;
import android.widget.*;

public class TestWidgets extends Activity implements SeekBar.OnSeekBarChangeListener {   
   private TextView resultat;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       SeekBar saisie = (SeekBar) findViewById(R.id.saisie);
       resultat = (TextView) findViewById(R.id.resultat); 
       saisie.setOnSeekBarChangeListener(this);
   }

   public void onProgressChanged(SeekBar saisie, int valeur, boolean utilisateur) {
      resultat.setText(valeur+" %");
   }

   public void onStartTrackingTouch(SeekBar saisie) {   }

   public void onStopTrackingTouch(SeekBar saisie) {   }
}  
  1. Nous définissons un écouteur de type Seek.OnSeekBarChangeListener. Cet écouteur prend en compte le changement de position du curseur de la barre de saisie. Cet écouteur dispose de trois méthodes qu'il est nécessaire alors de redéfinir.
  2. La méthode onProgressChanged() qui est activée lors d'un changement de position du curseur de saisie. Elle prend trois paramètres. Le premier qui, comme d'habitude, correspond au composant source du changement. Le deuxième correspond à la valeur actuelle de la position du curseur en interpolation entre la valeur 0 et la valeur maximum. Le dernier précise si le changement de position est réalisé par l'utilisateur ou par programmation.
  3. La méthode onStartTrackingTouch() qui est activé lorsque l'utilisateur commence à pointer son doigt sur le composant. Cette méthode peut éventuellement être utile lorsque nous désirons bloquer le déplacement du curseur dans certaines conditions.
  4. La méthode onStopTrackingTouch() qui est activé lorsque l'utilisateur enlève son doigt du composant.
Proposer une évaluation : RatingBar

Il existe une autre classe qui hérite de ProgressBar, il s'agit de RatingBar. Cette classe est utile lorsque vous devez évaluer une information. Cette évaluation se traduit sous forme graphique avec un certain nombre d'étoiles.

Il est possible de choisir le nombre d'étoile servant à l'évaluation au moyen de la méthode setNumStars(). La valeur saisie se récupère au moyen de la méthode getRating(). Vous pouvez également placer un écouteur RatingBar.OnRatingBarChangeListener qui prévient toute modification de la valeur de l'évaluation. Cet écouteur ne propose qu'une seule méthode onRatingChanged() qui se lance lorsque l'utilisateur propose une nouvelle évaluation.

Propriétés spécifiques de android.widget.RatingBar
  1. android:isIndicator : lorsque cet attribut est validé, le système d'évaluation est alors en lecture seule, il sert juste de renseignement d'une évaluation proposée à priori.
  2. android:numStars : fixe le nombre d'étoile servant à l'évaluation.
  3. android:rating : permet de proposer une valeur d'évaluation dès le départ.
  4. android:stepSize : permet de fixer le degré de précision. Par défaut, l'évaluation se fait en demi valeur (0.5). Si vous souhaitez, par exemple, proposer que des valeurs entières, il faudra spécifier la valeur "1.0". Si vous souhaitez évaluer au dixième près, il faudra alors spécifier la valeur "0.1".
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:orientation="vertical"
 >
   <RatingBar android:id="@+id/saisie"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:numStars="4"
       android:rating="2.5"
   />
   <TextView android:id="@+id/resultat" 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:textSize="22dp"
   />
</LinearLayout>




fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.*;
import android.widget.*;

public class TestWidgets extends Activity implements RatingBar.OnRatingBarChangeListener {   
   private TextView resultat;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       RatingBar saisie = (RatingBar) findViewById(R.id.saisie);
       resultat = (TextView) findViewById(R.id.resultat); 
       saisie.setOnRatingBarChangeListener(this);
       resultat.setText(""+saisie.getRating());
   }

   public void onRatingChanged(RatingBar saisie, float valeur, boolean utilisateur) {
      resultat.setText(""+valeur);
   }
}  

 

Choix du chapitre Conteneurs

Les conteneurs permettent de disposer un ensemble de widgets (et, éventuellement, des conteneurs fils) pour obtenir la présentation de votre choix. Les conteneurs sont aussi souvent dénommés des gabarits.

Si, par exemple, vous préférez placez les labels à gauche et les champs de saisie à droite, vous aurez besoin d'un conteneur. Si vous désirez que les boutons OK et Annuler soient l'un à côté de l'autre, en bas à droite du formulaire, vous aurez également besoin d'un conteneur.

Il est à noter que d'un point de vue purement XML, si vous manipulez plusieurs widgets, vous devrez utiliser un conteneur afin de disposer d'un élément racine dans lequel les placer.

Positionner les vues avec les gabarits

Une interface graphique n'est pas uniquement composée de vues "feuilles". Il existe, en effet, des vues particulières permettant de contenir d'autres vues et de les positionner : les gabarits. Nous les appelons également par leur nom anglais : layouts. Avec Java SE, nous connaissons bien ce genre de technique qui consiste à positionner et dimensionner les composants automatiquement à l'aide de gestionnaires de dispositions.

Ces vues particulières, ces gabarits, héritent toutes de la classe racine ViewGroup qui hérite elle-même de la classe View. Un gabarit est donc un conteneur qui aide à positionner les objets, qu'il s'agisse de vues ou d'autres gabarits au sein de votre interface. Vous pouvez imbriquer des gabarits les uns dans les autres, ce qui vous permettra de créer des mises en forme évoluées. Nous parlerons ainsi de gabarit parent et de gabarit enfant (ou plus généralement d'éléments enfants voire simplement d'enfants), le gabarit enfant étant inclus dans le gabarit parent.

Comme nous l'avons dit plus haut, vous pouvez décrire vos interfaces utilisateur soit par une déclaration XML, soit directement dans le code d'une activité en utilisant les classes adéquates. Dans les deux cas, vous pouvez utiliser différents types de gabarits. En fonction du type choisi, les vues et les gabarits seront disposés différemment :

  1. LinearLayout : Ce gabarit aligne l'ensemble de ses enfants dans une direction unique, horizontale ou verticale. Les éléments se succèdent ensuite de gauche à droite ou de haut en bas. Ce gabarit n'affiche donc qu'un seul élément par ligne ou colonne (selon l'orientation choisie : horizontale ou verticale).
  2. FrameLayout : C'est le plus basique des gabarits. Ce gabarit empile les éléments fils les uns sur les autres tel une accumulation de calques. Par défaut, toutes les vues filles s'alignent sur le coin supérieur gauche, mais il est possible de modifier le positionnement avec le paramètre android:layout_gravity.
  3. RelativeLayout : Ses enfants sont positionnés les uns par rapport aux autres, le premier enfant servant de référence aux autres. Ce gabarit a l'avantage de permettre un positionnement précis et intelligent tout en minimisant le nombre de vues utilisées.
  4. TableLayout : Permet de positionner vos vues en lignes et colonnes à l'instar d'un tableau.
  5. ScrollView : Permet de gérer le défilement vertical d'un ensemble trop conséquent d'informations.
  6. Les onglets : La taille de l'écran d'un téléphone étant limitée, vous serez souvent amené à utiliser des onglets pour afficher tout ce que votre application peut mettre à disposition de l'utilisateur.
  7. <include> : Permet de découper sa vue en plusieurs fichiers de description dans le cas où beaucoup d'éléments sont à insérer, et surtout dans le cas où une partie de la vue se retrouve souvent.

Penser de façon linéaire - LinearLayout

Le conteneur LinearLayout est un modèle reposant sur des boîtes : les widgets ou les conteneurs fils sont alignés en colonnes ou en lignes, les uns après les autres, exactement comme avec le gestionnaire de disposition FlowLayout de Swing.

La boîte est l'unité essentielle de disposition de widgets. Avec LinearLayout, vous pouvez vous passer des autres types de conteneur. Obtenir la disposition que vous souhaitez revient alors principalement à identifier les imbrications et les propriétés des différentes boîtes - leur alignement par rapport aux autres boîtes, par exemple. Ceci dit, nous verrons que les autres types de conteneur peuvent quand même s'avérer utiles dans bien des cas.

Pour configurer correctement un LinearLayout, vous pouvez agir sur cinq paramètres : l'orientation, le modèle de remplissage, le poids, la gravité et le remplissage.
.

Orientation

L'orientation précise si le LinearLayout représente une ligne ou une colonne. Il suffit d'ajouter la propriété android:orientation à l'élément LinearLayout du descripteur XML en fixant sa valeur à horizontal pour une ligne ou à vertical pour une colonne. Par défaut, c'est une présentation horizontale qui est proposée.

Cette orientation peut être modifiée en cours d'exécution en appelant la méthode setOrientation() du conteneur et en lui fournissant en paramètre la constante HORIZONTAL ou VERTICAL.

Modèle de remplissage

Supposons que nous ayons une ligne de widgets - une paire de boutons, par exemple. Ces widgets ont une taille "naturelle" reposant sur celle de leur texte. Ces tailles combinées ne correspondent sûrement pas à la largeur de l'écran du terminal d'Android - notamment parce que les tailles des écrans varient en fonction des modèles. Il faut donc savoir que faire de l'espace restant.

Pour résoudre ce problème, tous les widgets d'un LinearLayout doivent fournir une valeur pour les propriétés android:layout_width et android:layout_height. Ces valeurs peuvent s'exprimer de trois façons différentes :

  1. Une dimension précise, comme 125px, afin d'indiquer que le widget devra occuper exactement 125 pixels.
  2. wrap_content, afin de demander que le widget occupe sa place naturelle sauf s'il est trop gros, auquel cas Android coupera le texte entre les mots pour qu'il puisse tenir.
  3. fill_parent, afin de demander que le widget occupe tout l'espace disponible de son conteneur après placement des autres widgets.

Les valeurs les plus utilisées sont les deux dernières, car elles sont indépendantes de la taille de l'écran ; Android peut donc ajuster la disposition pour qu'elle tienne dans l'espace disponible.

Poids

Que se passe-t-il si deux widgets doivent se partager l'espace disponible ? Supposons, par exemple, que nous ayons deux champs de saisie multilignes en colonne et que nous voulions qu'ils occupent tout l'espace disponible de la colonne après le placement de tous les autres widgets.

Pour ce faire, en plus d'initialiser android:layout_width (pour les lignes) ou android:layout_height (pour les colonnes) avec fill_parent, il faut également donner à android:layout_weight, une valeur qui indique la proportion d'espace libre qui sera affiché au widget.

Si cette valeur est la même pour les deux widgets (1, par exemple), l'espace libre sera partagé équitablement entre eux. Si la valeur est 1 pour un widget et 2 pour l'autre, le second utilisera deux fois plus d'espace libre que le premier, etc. Le poids d'un widget est fixé à zéro par défaut.

ATTENTION : Pour que cela fonctionne correctement avec l'attribut android:layout_weight, vous devez impérativement, avec une disposition en ligne initialisez à zéro les valeurs android:layout_width de tous les widgets du layout, et avec une disposition en colonne initialisez à zéro les valeurs android:layout_height de tous les widgets du layout.

Un autre moyen d'utiliser le poids consiste à allouer des pourcentages. Pour utiliser cette technique avec une disposition en ligne, par exemple :

  1. Initialisez à zéro les valeurs android:layout_width de tous les widgets du layout.
  2. Initialisez avec les poucentages adéquats les valeurs android:layout_weight de tous les widgets du layout.
  3. Assurez-vous que la somme de ces pourcentages soit égale à 100.
Gravité

Par défaut, les widgets s'alignent à partir de la gauche et du haut. Si vous créez une ligne avec un LinearLayout horizontal, cette ligne commencera donc à se remplir à partir du bord gauche de l'écran.

Si ce n'est pas ce que vous souhaitez, vous devez indiquer une gravité à l'aide de la propriété android:layout_gravity d'un widget (ou en appelant sa méthode setGravity() en cours d'exécution) afin d'indiquer au widget et à son conteneur comment l'aligner par rapport à l'écran.

  1. Pour une colonne de widgets, les gravités les plus courantes sont left, center_horizontal et right pour respectivement, aligner les widgets à gauche, au centre ou à droite.
  2. Pour une ligne, le comportement par défaut consiste à placer les widgets de sorte que leur texte soit aligné sur la ligne de base (la ligne invisible sur laquelle les lettres semblent reposer), mais il est possible de préciser une gravité center_vertical pour centrer verticalement les widgets dans la ligne.
Remplissage

Les widgets sont, par défaut, serrés les uns contre les autres. Vous pouvez augmenter l'espace intercalaire à l'aide de la propriété android:padding (ou en appelant en cours d'exécution la méthode setPadding() du widget).

La valeur de remplissage précise l'espace situé entre le contour de la cellule du widget et son contenu réel.

La propriété android:padding permet de préciser le même remplissage pour les quatre côtés du widget ; son contenu étant alors centré dans la zone qui reste. Pour utiliser des valeurs différents en fonction des côtés, utilisez plutôt les propriétés android:paddingLeft, android:paddingRight, android:paddingTop et android:paddingBottom.

La valeur de ces propriétés est une dimension, comme 5px pour demander un remplissage de 5 pixels. Si vos appliquez un fond au widget (avec l'attribut android:background, par exemple), ce fond sera placé à la fois derrière le widget et sa zone de remplissage.

Si ce n'est pas ce que vous souhaitez, utilisez des marges plutôt que le remplissage car celles-ci ajoutent de l'espace sans augmenter la taille intrinsèque du widget. Pour configurer ces marges, utiliser les attributs android:margin, android:marginLeft, android:marginRight, android:marginTop et android:marginBottom.

A titre d'exemple, je vous propose de reprendre le projet sur la saisie d'une valeur parmi un intervalle au moyen du widget SeekBar dont voici le code Java correspondant.
fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.*;
import android.widget.*;

public class TestWidgets extends Activity implements SeekBar.OnSeekBarChangeListener {   
   private TextView resultat;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       SeekBar saisie = (SeekBar) findViewById(R.id.saisie);
       resultat = (TextView) findViewById(R.id.resultat); 
       saisie.setOnSeekBarChangeListener(this);
   }

   public void onProgressChanged(SeekBar saisie, int valeur, boolean utilisateur) {
      resultat.setText(valeur+" %");
   }

   public void onStartTrackingTouch(SeekBar saisie) {   }

   public void onStopTrackingTouch(SeekBar saisie) {   }
}  

En reprenant exactement le même exemple, nous voyons que le conteneur place ces widgets les uns au dessus des autres. C'est finalement le mode qui réclame le moins de réglage, il suffit de spécifier juste l'orientation à l'aide de l'attribut android:orientation="vertical".

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:orientation="vertical"
 >
   <SeekBar android:id="@+id/saisie"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:max="50"
   />
   <TextView android:id="@+id/resultat" 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="0 %"
       android:textSize="22dp"
   />
</LinearLayout>




Dans le même projet, je vous invite à placer les widgets cette fois-ci sur une même ligne. La première démarche consiste à enlever l'attribut android:orientation puisque c'est le mode horizontal qui est proposé par défaut. Si vous effectuez cette suppression sans autres réglages sur les widgets, vous remarquerez que seul le premier widget apparaît, le SeekBar.

La solution consiste à placer un poids sur chacun des widgets conserné en privilégiant la saisie au travers du SeekBar pour avoir ainsi plus de place et ainsi plus de souplesse. N'oubliez pas, dans ce cas là, de proposer une valeur nulle à l'attribut android:layout_width sur chacun des widgets du LinearLayout.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:padding="5px"
 >
   <SeekBar android:id="@+id/saisie"
       android:layout_width="0dp" 
       android:layout_height="wrap_content" 
       android:max="50"
       android:layout_weight="3"
   />
   <TextView android:id="@+id/resultat" 
       android:layout_width="0dp" 
       android:layout_height="wrap_content" 
       android:text="0 %"
       android:textSize="22dp"
       android:gravity="center"
       android:layout_weight="1"
   />
</LinearLayout>


Rajouter des boutons pour sélectionner les valeurs extrêmes

Afin de bien maîtriser ces gabarits, je vous propose de rajouter une ligne supplémentaire avec deux boutons qui seront placés à droite, un pour remettre le curseur à zéro, l'autre pour le mettre à la valeur maximale.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="5px"
 >
   <LinearLayout
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content"      
   >
      <SeekBar android:id="@+id/saisie"
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:max="50"
          android:layout_weight="3"
      />
      <TextView android:id="@+id/resultat" 
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:text="0 %"
          android:textSize="20dp"
          android:textStyle="bold"
          android:gravity="center"
          android:layout_weight="1"
      />
   </LinearLayout>
   <LinearLayout
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content"  
       android:gravity="right"
   >
      <Button android:id="@+id/mini"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Mini"
          android:textStyle="bold"
          android:onClick="mini"
      />
      <Button android:id="@+id/maxi" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Maxi"
          android:textStyle="bold"
          android:onClick="maxi"
      />
   </LinearLayout>   
</LinearLayout>  

Le fichier de description est beaucoup plus conséquent puisque nous avons plus de widgets dans notre interface graphique. Remarquez l'imbrication de conteneurs LinearLayout. Le conteneur englobant propose une orientation verticale qui lui-même possède deux conteneurs LinearLayout, chacun choisissant sa propre stratégie de placement des widgets. Vous avez ci-dessous le code Java qui prend en compte ces nouvelles fonctionnalités.

fr.manu.test.TestWidgets
package fr.manu.test;

import android.app.*;
import android.os.*;
import android.widget.*;

public class TestWidgets extends Activity implements SeekBar.OnSeekBarChangeListener {   
   private TextView resultat;
   private SeekBar saisie;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       saisie = (SeekBar) findViewById(R.id.saisie);
       resultat = (TextView) findViewById(R.id.resultat); 
       saisie.setOnSeekBarChangeListener(this);
   }

   public void onProgressChanged(SeekBar saisie, int valeur, boolean utilisateur) {
      resultat.setText(valeur+" %");
   }

   public void onStartTrackingTouch(SeekBar saisie) {   }

   public void onStopTrackingTouch(SeekBar saisie) {   }
   
   public void mini(View vue) { saisie.setProgress(0);  }
   
   public void maxi(View vue) { saisie.setProgress(saisie.getMax());  }
}  

Tout est relatif - RelativeLayout

Comme son nom l'indique, le conteneur RelativeLayout place les widgets relativement les uns aux autres widgets du conteneur et de son conteneur parent. Vous pouvez ainsi placer le widget X en dessous et à gauche du widget Y ou faire en sorte que le bord inférieur du widget Z soit aligné avec le bord inférieur du conteneur, etc.

Pour utiliser correctement un conteneur RelativeLayout, il est nécessaire de pouvoir faire référence à d'autres widgets dans le fichier de description XML et ainsi de disposer d'un moyen d'indiquer leurs positions relatives.

Positions relatives à un conteneur

Les relations les plus simples à mettre en place sont celles qui lient la position d'un widget à celle de son conteneur. Toutes les propriétés proposées ci-dessous doivent systématiquement prendre soit la valeur true, soit la valeur false.

  1. android:layout_alignParentTop : précise que le haut du widget doit être aligné avec celui du conteneur.
  2. android:layout_alignParentBottom : précise que le bas du widget doit être aligné avec celui du conteneur.
  3. android:layout_alignParentLeft : précise que le bord gauche du widget doit être aligné avec le bord gauche du conteneur.
  4. android:layout_alignParentRight : précise que le bord droit du widget doit être aligné avec le bord droit du conteneur.
  5. android:layout_centerHorizontal : précise que le widget doit être centré horizontalement dans le conteneur.
  6. android:layout_centerVertical : précise que le widget doit être centré verticalement dans le conteneur.
  7. android:layout_centerInParent : précise que le widget doit être centré horizontalement et verticalement dans le conteneur.

Le remplissage du widget est pris en compte lors de ces alignements. Ceux-ci reposent sur la cellule globale du widget (c'est-à-dire sur la combinaison de sa taille naturelle et de son remplissage.

Notation relative dans les propriétés

Les propriétés restantes concernant RelativeLayout possède comme valeur l'identité d'un widget du conteneur. Pour ce faire :

  1. Associer des identifiants (attributs android:id) à tous les éléments que vous aurez besoin de désigner, sous la forme (@+id/...).
  2. Faite ensuite référence à un widget particulier en utilisant son identifiant, privé du signe plus (@id/...)

Si, par exemple, le widget A est identifié par @+id/widgetA, le widget B peut le désigner dans l'une de ses propriétés par @id/widgetA.
.

Positions relatives aux autres widgets

Quatre propriétés permettent de contrôler la position d'un widget par rapport aux autres :

  1. android:layout_above : indique que le widget doit être placé au-dessus de celui qui est désigné dans cette propriété.
  2. android:layout_below : indique que le widget doit être placé sous celui qui est désigné dans cette propriété.
  3. android:layout_toLeftOf : indique que le widget doit être placé à gauche de celui qui est désigné dans cette propriété.
  4. android:layout_toRightOf : indique que le widget doit être placé à droite de celui qui est désigné dans cette propriété.

Cinq autres propriétés permettent de contrôler l'alignement d'un widget par rapport à un autre :

  1. android:layout_alignTop : indique que le haut du widget doit être aligné avec le haut du widget désigné dans cette propriété.
  2. android:layout_alignBottom : indique que le bas du widget doit être aligné avec le bas du widget désigné dans cette propriété.
  3. android:layout_alignLeft : indique que le bord gauche du widget doit être aligné avec le bord gauche du widget désigné dans cette propriété.
  4. android:layout_alignRight : indique que le bord droit du widget doit être aligné avec le bord droit du widget désigné dans cette propriété.
  5. android:layout_alignBaseLine : indique que les lignes de base des deux widgets doivent être alignées.

La dernière propriété de cette liste permet d'aligner des labels et des champs de saisie afin que le texte semble "naturel". En effet, les champs de saisie étant matérialisés par une boîte, contrairement aux labels, android:layout_alignTop alignerait le haut de la boîte du champ avec le haut du label, ce qui ferait apparaître le texte du label plus haut dans l'écran que le texte saisi dans le champ.

Si nous souhaitons, par exemple, que le widget B soit placé à droite du widget A, l'élément XML du widget B doit donc contenir android:layout_toRight = "@id/widgetA" (où @id/widgetA est l'identifiant du widget A).

Ordre d'évaluation

Avant la version 1.6, Android ne lisait qu'une seule fois le fichier XML et calculait donc en séquence la taille et la position de chaque widget. Ceci signifiait que nous ne pouvions pas faire référence à un widget qui n'avait pas été défini plus haut dans le fichier, ce qui compliquait un peu les choses.

Désormais, Android traite les règles en effectuant deux passes : vous pouvez donc utiliser des références vers des widgets qui sont définis plus loin dans le fichier.

En reprenant le tout dernier projet, voici comment nous pouvons implémenter notre fichier de description au moyen de RelativeLayout.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:padding="5px"
 >
   <TextView android:id="@+id/resultat" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:paddingLeft="5px"
       android:text="0 %"
       android:textSize="22dp"
       android:gravity="center"
       android:layout_alignParentRight="true"
   />    
   <SeekBar android:id="@+id/saisie"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:max="50"
       android:layout_toLeftOf="@id/resultat"
   />
</RelativeLayout>


Vous remarquez que j'identifie le TextView avant la description du SeekBar. Il semblerait, alors que dans cet exemple nous sommes avec Android 2.1, que le compilateur ne lise qu'une seule fois le fichier de description. Comme cela n'est pas une grosse contrainte, je vous invite donc à placer le widget de référence en premier dans le conteneur le supportant.

L'avantage avec ce gabarit, vu les réglages effectués, c'est que le SeekBar s'autodimension en rapport avec la largeur variable du TextView, alors que dans le cas précédent, avec un LinearLayout horizontal, la largeur du SeekBar faisait systématiquement les 3/4 de la largeur de l'écran.

Conteneurs mixtes

Ici aussi, nous allons rajouter les boutons "Mini" et "Maxi" pour contrôler le curseur de saisie vers les valeurs extrêmes. Nous plaçons la première ligne de l'interface au travers d'un gabarit RelativeLayout. Voici ce que cela donne :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="5px"
 >
   <RelativeLayout 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content"
    >
      <TextView android:id="@+id/resultat" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:paddingLeft="5px"
          android:text="0 %"
          android:textSize="22dp"
          android:gravity="center"
          android:textStyle="bold"
          android:layout_alignParentRight="true"
      />    
      <SeekBar android:id="@+id/saisie"
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:max="50"
          android:layout_toLeftOf="@id/resultat"
      />
   </RelativeLayout>
   <LinearLayout
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content"  
       android:gravity="right"
   >
      <Button android:id="@+id/mini"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Mini"
          android:textStyle="bold"
          android:onClick="mini"
      />
      <Button android:id="@+id/maxi" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Maxi"
          android:textStyle="bold"
          android:onClick="maxi"
      />
   </LinearLayout>   
</LinearLayout>

Bien entendu, nous pouvons mélanger les différents types de conteneur. Le gabarit RelativeLayout paraît plus adapté lorsqu'une widget doit prendre le plus de place possible. Dans le cas où les widgets possèdent des largeurs fixes, le gabarit LinearLayout semble plus simple à manipuler.

Prendre un seul conteneur

Nous pouvons aller plus loin dans notre démarche et faire en sorte que tous les composants de l'interface soient placés les uns par rapport aux autres, en utilisant finalement qu'un seul conteneur. Cela est possible avec ce conteneur finalement assez intéressant qu'est le RelativeLayout.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:padding="5px"
 >
   <TextView android:id="@+id/resultat" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:paddingLeft="5px"
       android:text="0 %"
       android:textSize="22dp"
       android:gravity="center"
       android:textStyle="bold"
       android:layout_alignParentRight="true"
   />    
   <SeekBar android:id="@+id/saisie"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:max="50"
       android:layout_toLeftOf="@id/resultat"
   />
   <Button android:id="@+id/maxi"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:text="Maxi"
       android:textStyle="bold"
       android:onClick="maxi"
       android:layout_below="@id/resultat"
       android:layout_alignRight="@id/resultat"
   />
   <Button android:id="@+id/mini" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:text="Mini"
       android:textStyle="bold"
       android:onClick="mini"
       android:layout_toLeftOf="@id/maxi"
       android:layout_alignTop="@id/maxi"
   /> 
</RelativeLayout>

Vous remarquez qu'un même widget peut avoir plusieurs précisions de placement. L'avantage de cette structure c'est que nous mettons en oeuvre un seul conteneur. L'inconvénient toutefois, c'est que vous devez effectivement donner plus de précision de placement des composants les uns envers les autres.

Présenter sous forme de tableau - TableLayout

Le conteneur TableLayout vous permet de positionner les widgets dans une grille. Vous pouvez ainsi définir le nombre de lignes et de colonnes, les colonnes qui peuvent se réduire ou s'agrandir en fonction de leur contenu, etc.

TableLayout fonctionne de concert avec le conteneur TableRow. Alors que TableLayout contrôle le comportement global du conteneur, les widgets eux-mêmes sont placés dans un ou plusieurs TableRow, à raison d'un par ligne de la grille.

Pour utiliser ce conteneur, il faut savoir gérer les widgets en lignes et en colonnes, et traiter ceux qui sont placés à l'extérieur des lignes.
.

Placement des cellules dans les lignes

C'est vous qui déclarez les lignes en plaçant les widgets comme des fils d'un élément TableRow, lui-même file d'un TableLayout. Vous contrôlez donc directement la façon dont apparaissent les lignes dans le tableau. C'est Android qui détermine automatiquement le nombre de colonnes mais, en réalité, vous le contrôlez de façon indirecte.

Vous possèderez au moins autant de colonnes qu'il y a de widgets dans la ligne la plus longue. Si vous avez trois lignes, par exemple - une ligne avec deux widgets, une avec trois widgets et une avec quatre widgets, le tableau possèdera en définitive d'au moins quatre colonnes.

Quelques propriétés
  1. android:layout_span : Toutefois, un widget peut occuper plusieurs colonnes si vous utilisez cette propriété en lui spécifiant le nombre de colonnes sur lesquelles doit s'étendre le widget concerné. Cette propriété ressemble donc à l'attribut colspan utilisé dans les tableaux HTML.
  2. android:layout_column : Généralement, les widgets sont placés dans la première colonne disponible (attention, la première colonne est indicée à 0). Vous pouvez également placer un widget sur une colonne précise en vous servant de cette propriété et en lui spécifiant le numéro de la colonne voulue.
Fils de TableLayout qui ne sont pas des lignes

Généralement, les seuls fils directs de TableLayout sont des éléments TableRow. Cependant, vous pouvez également placer des widgets entre les lignes. En ce cas, TableLayout se comporte un peu comme un conteneur LinearLayout ayant une orientation verticale. Les largeurs de ces widgets seront automatiquement fixées à fill_parent pour remplir le même espace que la ligne la plus longue.

Réduire, étirer et refermer

Par défaut, la taille de chaque colonne sera la taille "naturelle" de son widget le plus large (en tenant compte des widgets qui s'étendent sur plusieurs colonnes). Parfois, cependant, cela ne donne pas le résultat escompté et il est alors nécessaire d'intervenir plus précisément sur le comportement de la colonne.

  1. android:stretchColumns : Pour ce faire, vous pouvez utiliser cette propriété de l'élément TableLayout, dont la valeur peut être un seul numéro de colonne (débutant à zéro). ou une liste de numéros de colonnes séparés par des virgules. Ces colonnes seront alors étirées pour tout l'espace disponible de la ligne, ce qui est utile lorsque votre contenu est plus étroit que l'espace restant.
  2. android:shrinkColumns : Inversement, cette propriété de TableLayout, qui prend les mêmes valeurs, permet de réduire la largeur effective des colonnes en découpant leur contenu en plusieurs lignes (par défaut, le contenu des widgets n'est pas découpé). Ceci permet d'éviter qu'un contenu trop long pousse certaines colonnes à droite de l'écran et quelles soient donc invisibles.
  3. android:collapseColumns : A ce sujet, vous pouvez également tirer parti de cette propriété en indiquant là aussi un numéro ou une liste de numéros de colonnes. Celles-ci seront alors initialement "refermées", ce qui signifie qu'elles n'apparaîtrons pas, bien qu'elles fassent partie du tableau.

    A partir de votre programme, vous pouvez refermer ou réouvrir les colonnes à l'aide de la méthode correspondante à la propriété setColumnCollapsed() du widget TableLayout.

    Ceci permet aux utilisateurs de contrôler les colonnes importantes qui doivent apparaître et celles qui peuvent être cachées car elles ne sont pas utiles à certains moments.

En outre, les méthodes setColumnStretchable() et setColumnShrinkable() permettent respectivement de contrôler l'étirement et la réduction des colonnes en cours d'exécution.

En reprenant le dernier projet, où nous rajoutons simplement une ligne de séparation, voici comment nous pouvons implémenter notre fichier de description au moyen de TableLayout.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:stretchColumns="1"
 >
   <TableRow>
      <SeekBar android:id="@+id/saisie"
          android:layout_height="wrap_content" 
          android:max="50"
          android:layout_span="3"
      />
      <TextView android:id="@+id/resultat" 
          android:layout_height="wrap_content" 
          android:text="0 %"
          android:textSize="20dp"  
          android:textStyle="bold"
          android:gravity="center"
      />
   </TableRow>
   <View 
      android:layout_height="2px" 
      android:background="#FF8800"
   />
   <TableRow>
      <Button android:id="@+id/mini"
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Mini"
          android:textStyle="bold"
          android:onClick="mini"
          android:layout_column="2"
      />
      <Button android:id="@+id/maxi" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="Maxi"
          android:textStyle="bold"
          android:onClick="maxi"
      />
   </TableRow>   
</TableLayout>

Le code source, lui, reste inchangé :

fr.manu.test.TestLayout
package fr.manu.test;

import android.app.*;
import android.os.*;
import android.widget.*;

public class TestLayout extends Activity implements SeekBar.OnSeekBarChangeListener {   
   private TextView resultat;
   private SeekBar saisie;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       saisie = (SeekBar) findViewById(R.id.saisie);
       resultat = (TextView) findViewById(R.id.resultat); 
       saisie.setOnSeekBarChangeListener(this);
   }

   public void onProgressChanged(SeekBar saisie, int valeur, boolean utilisateur) {
      resultat.setText(valeur+" %");
   }

   public void onStartTrackingTouch(SeekBar saisie) {   }

   public void onStopTrackingTouch(SeekBar saisie) {   }
   
   public void mini(View vue) { saisie.setProgress(0);  }
   
   public void maxi(View vue) { saisie.setProgress(saisie.getMax());  }
}

Gérer les défilements - ScrollView

Les écrans des téléphones sont généralement assez petits, ce qui oblige les développeurs à employer quelques astuces pour présenter beaucoup d'informations dans un espace réduit. L'une des astuces consiste à utiliser le défilement, afin que seule une partie de l'information soit visible à un instant donné, le reste étant disponible en faisant défiler l'écran vers le haut ou vers le bas.

ScrollView est un conteneur qui fournit un défilement à son contenu. Vous pouvez donc utiliser un gestionnaire de disposition qui peut produire un résultat trop pour certains écrans et l'envelopper dans un ScrollView tout en continuant d'utiliser la logique de ce gestionnaire. L'utilisateur ne verra alors qu'une partie de votre présentation et aura accès au reste via des barres de défilement.

Voici par exemple un élément ScrollView qui enveloppe un TableLayout.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <TableLayout 
       android:layout_width="fill_parent" 
       android:layout_height="fill_parent" 
       android:stretchColumns="0">
       <TableRow>
          <View android:layout_height="80px" android:background="#440000" />
          <TextView android:text="#440000" android:layout_gravity="center_vertical"/>
       </TableRow>  
       <TableRow>
          <View android:layout_height="80px" android:background="#884400" />
          <TextView android:text="#884400" android:layout_gravity="center_vertical"/>
       </TableRow>  
       <TableRow>
          <View android:layout_height="80px" android:background="#AA8844" />
          <TextView android:text="#AA8844" android:layout_gravity="center_vertical"/>
       </TableRow>  
       <TableRow>
          <View android:layout_height="80px" android:background="#FFAA88" />
          <TextView android:text="#FFAA88" android:layout_gravity="center_vertical"/>
       </TableRow>  
       <TableRow>
          <View android:layout_height="80px" android:background="#FFFFAA" />
          <TextView android:text="#FFFFAA" android:layout_gravity="center_vertical"/>
       </TableRow>  
       <TableRow>
          <View android:layout_height="80px" android:background="#FFFFFF" />
          <TextView android:text="#FFFFFF" android:layout_gravity="center_vertical"/>
       </TableRow>         
   </TableLayout>
</ScrollView>  

Sans le ScrollView, la grille occuperait au moins 480 pixels (six lignes de 80 pixels chacune, selon la définition de l'élément View). Certains terminaux peuvent avoir des écrans capables d'afficher autant d'informations, mais la plupart seront plus petits. Le ScrollView permet alors de conserver la grille tout en présentant qu'une partie à la fois.

Vous remarquez que nous ne voyons que quatre lignes. En glissant votre doigt sur l'écran, vous pouvez faire défiler l'écran afin de faire apparaître les lignes restantes. Vous remarquez également que le bord droit du contenu est masqué par la barre de défilement - pour éviter ce problème, vous pourriez ajouter des pixels de remplissage sur ce côté.

Depuis Android 1.5, nous disposons également du conteneur HorizontalScrollView qui fonctionne comme ScrollView, mais en horizontal. Ce défilement peut être utile pour les formulaires qui sont trop larges pour tenir en entier à l'écran. Notez que ni HorizontalScrollView ni ScrollView ne vous permettent de défiler dans les deux directions : vous devez choisir entre vertical ou horizontal.

Ajouter des onglets - TabHost et TabWidget

La taille de l'écran d'un téléphone étant limitée, vous serez souvent amené à utiliser des onglets pour afficher tout ce que votre application peut mettre à disposition de l'utilisateur.

Avec Android, nous disposons d'un conteneur TabHost qui fonctionne comme tel - une protion de ce qu'affiche l'activité est liée à des onglets qui, lorsque nous cliquons dessus, permettent de passer d'une partie de la vue à une autre. Une activité utilisera par exemple un onglet pour la saisie d'un emplacement de géolocalisation et un autre pour afficher cet emplaceemnt sur une carte.

Mise en oeuvre

Par rapport aux autres conteneurs, la mise en oeuvre des onglets est un peu plus sophistiqué. Vous devez d'une part vous occuper de la vue, comme d'habitude, mais vous devez également faire une gestion appropriée dans le code Java. Par ailleurs, il faudra être attentif à cela, avec Android, les "boutons" et les contenus des onglets sont des entités totalement distinctes.

Pour mettre en place des onglets dans une vue, vous avez besoin des widgets et des conteneurs suivants :
  1. TabHost : est le conteneur général qui englobe les boutons et les contenus des onglets.
  2. TabWidget : implémente la ligne des boutons des onglets, qui contient les labels et, éventuellement, des icônes.
  3. FrameLayout : est le conteneur des contenus des onglets : chaque contenu d'onglet est un fils du FrameLayout. Je rappelle que ce gabarit empile les éléments fils les uns sur les autres tel une accumulation de calques. Par défaut, toutes les vues filles s'alignent sur le coin supérieur gauche.
La version actuelle d'Android exige de respecter les règles suivantes pour que ces trois composants puissent agir de concert :
  1. L'identifiant du TabWidget doit être exactement @android:id/tabs.
  2. L'identifiant du FrameLayout doit être exactement @android:id/tabcontent.

Ensuite, dans le code Java, il sera nécessaire d'atteindre d'une part l'ensemble des onglets ainsi que les contenus relatifs à chaque onglet. Pour cela, vous devez donc identifier le TabHost et les différentes pages visualisant le contenu de chaque onglet particulier.

Afin de valider tous ces différents critères, je vous propose de réaliser une activité qui permet d'afficher l'heure soit de façon analogique, soit sous forme digitale :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>

<!-- TabHost qui contient tous les éléments de nos onglets -->
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/onglets"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

   <!-- Deux structures suivant l'orientation verticale -->
    <LinearLayout
         android:orientation="vertical"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">

         <!-- TabWidget qui sert à afficher les onglets (boutons)  -->
         <TabWidget 
             android:id="@android:id/tabs" 
             android:layout_width="fill_parent"
             android:layout_height="wrap_content" />

         <!-- Tous les contenus des onglets  -->
         <FrameLayout 
             android:id="@android:id/tabcontent"            
             android:layout_width="fill_parent"
             android:layout_height="fill_parent"
             >
             <AnalogClock  
                   android:id="@+id/analog"           
                   android:layout_width="fill_parent" 
                   android:layout_height="fill_parent" 
                   android:gravity="center"
                />
             <DigitalClock
                   android:id="@+id/digital" 
                   android:layout_width="fill_parent" 
                   android:layout_height="fill_parent" 
                   android:textStyle="bold"
                   android:textSize="32dp"
                   android:gravity="center"/>
         </FrameLayout> 
   </LinearLayout>   
</TabHost>

Vous remarquez que les éléments TabWidget et FrameLayout sont des fils directs de TabHost et que l'élément FrameLayout a lui-même des fils représentant les différents onglets. Ici, nous en avons deux : une horloge analogique et une horloge digitale. Dans un scénario plus compliqué, chaque onglet serait regroupé dans un conteneur (LinearLayout, par exemple) avec son propre contenu disposant d'un esemble de widgets.

Code Java

Malheureusement, avec les versions actuelles d'Android, dans la partie vue nous ne spécifions à aucun moment la relation entre un bouton d'onglet et son propre contenu. Cela doit se faire au niveau du code Java.

Le code Java doit effectivement indiquer au TabHost quelles sont les vues qui représentent les contenus des onglets et à quoi doivent ressembler les boutons de ces onglets. Tout ceci est encapsulé dans des objets issus de la classe interne TabSpec.

Nous devons donc récupérer une instance de TabSec au travers de la méthode spécifique newTabSpec() de la classe TabHost, nous la remplissons avec les valeurs souhaitées puis nous l'ajoutons dans le bon ordre pour être en relation avec les bon boutons d'onglet. Les deux méthodes essentielles de TabSec sont les suivantes :

  1. setContent() : qui permet d'indiquer le contenu de cet onglet. Généralement, il s'agit de l'identifiant android:id de la vue que nous désirons montrer lorsque l'onglet est choisi.
  2. setIndicator() : qui permet de fournir le titre du bouton de l'onglet. Cette méthode est surchargée afin de permettre de fournir également un objet Drawable représentant l'icône de l'onglet.

Notez que les indicateurs des onglets peuvent, en réalité, être eux-mêmes des vues, ce qui permet de faire mieux qu'une simple étiquette et une icône facultative.

Notez également que vous devez appeler la méthode setup() de l'objet TabHost avant de configurer les objets TabSpec. Cet appel n'est pas nécessaire si votre activité dérive de la classe de base TabActivity.

fr.manu.test.TestLayout
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TabHost;

public class Onglets extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TabHost onglets = (TabHost) findViewById(R.id.onglets);
        onglets.setup(); // appel impératif avant d'ajouter les onglets
        
        // Ajout du premier onglet
        TabHost.TabSpec analogique = onglets.newTabSpec("analog");       
        analogique.setContent(R.id.analog);
        analogique.setIndicator("Analogique");
        onglets.addTab(analogique);
        
         // Ajout du deuxième onglet
        TabHost.TabSpec digitale = onglets.newTabSpec("digital");  
        digitale.setContent(R.id.digital);
        digitale.setIndicator("Digitale");
        onglets.addTab(digitale);        
        
//        onglets.setCurrentTab(0);
    }
}

Lorsque vous utilisez la méthode newTabSpec(), vous devez proposer un marqueur unique sous forme de chaîne de caractères.
.

Les méthodes setContent() et setIndicator() sont intéressantes puisqu'elles retournent un TabSpec. Cette architecture n'est pas anodine puisqu'elle permet en une seule ligne de rajouter un onglet complet en imbriquant les différents appels de méthode. Voici le même exemple qui exploite cette possibilité.

fr.manu.test.TestLayout
package fr.manu.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TabHost;

public class Onglets extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TabHost onglets = (TabHost) findViewById(R.id.onglets);
        onglets.setup(); // appel impératif avant d'ajouter les onglets
        
        // Ajout des deux onglets consécutivement
        onglets.addTab(onglets.newTabSpec("analog").setContent(R.id.analog).setIndicator("Analogique"));
        onglets.addTab(onglets.newTabSpec("digital").setContent(R.id.digital).setIndicator("Digitale"));        
    }
}

Découper ses interfaces avec l'inclusion

Comme dans les interfaces web, votre application comportera souvent des portions d'interfaces communes à l'ensemble de l'application (menu de navigation, en-tête et pied de page, etc.).

Ces portions d'interfaces communes - par exemple pour une application mobile, un bandeau supérieur contenant des informations - devront être dupliquées dans toutes les définitions de l'interface. Ce travail sera fastidieux et n'en sera que plus compliqué à maintenir puisque chaque modification devra être répercutée pour chaque définition d'interface.

Afin de compler cette lacune, la plate-forme Android dispose d'une instruction <include> permettant d'inclure dans la définition d'une interface, des éléments contenus dans une autre interface. Avec ce mécanisme, il suffit juste de modifier la partie de l'interface qui est incluse pour que la modification soit répercutée dans l'ensemble des interfaces.

Reprenons l'activité précédente en scindant cette fois-ci la vue en deux fichiers, le fichier main.xml qui comporte l'ossature générale de la vue et le fichier onglets.xml qui comporte toute la partie contenu des onglets :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/onglets"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
         android:orientation="vertical"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">
         <TabWidget 
             android:id="@android:id/tabs" 
             android:layout_width="fill_parent"
             android:layout_height="wrap_content" />
         <include layout="@layout/onglets" /> // C'est à cet endroit que le contenu du fichier onglets.xml est inclus
   </LinearLayout>   
</TabHost>

Pour inclure une interface dans une autre, utilisez l'élément <include> à l'endroit où vous souhaitez insérer les élements décrits dans un autre fichier de description et renseignez l'attribut layout afin de spécifier l'identifiant de l'interface à inclure à cet emplacement en utilisant la syntaxe "@layout/(nom du fichier)", ici "@layout/onglets".

res/layout/onglets.xml
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@android:id/tabcontent"            
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <AnalogClock  
          android:id="@+id/analog"           
          android:layout_width="fill_parent" 
          android:layout_height="fill_parent" 
          android:gravity="center"
       />
    <DigitalClock
          android:id="@+id/digital" 
          android:layout_width="fill_parent" 
          android:layout_height="fill_parent" 
          android:textStyle="bold"
          android:textSize="32dp"
          android:gravity="center"/>
</FrameLayout>