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.
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 :
Plus précisément, Android est constitué de plusieurs parties dépendantes et nécessaires.
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.
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++.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 :
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.
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.
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.
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 :
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 :
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.
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 :
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.
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.
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.
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.
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.
Certaines opérations sont réalisables à condition d'en obtenir la permission. Ces actions sont de plusieurs formes :
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.
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.
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 :
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.
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.
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 :
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.
/* 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.
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 :
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.
Lorsque vous compilez un projet, le résultat est placé dans le répertoire bin, sous la racine de l'arborescence du projet :
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.
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 :
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.
<?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>
<uses-permission android:name="android.permission.ACCESS_GPS" />
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>.
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.
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.
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 :
<?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>
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 :
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.
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.
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 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.
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.
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( , 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 :
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.
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).
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 :
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( , new Date()); bouton.setText(texte); } }
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.
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.
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.).
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).
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.
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 :
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.
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 :
Nous prendrons le temps de développer chacun de ces gabarits particuliers dans un chapitre qui leur seront consacrés.
.
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).
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.
<?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 :
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.
/* 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; } }
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 :
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.
Un utilisateur malvoyant pourrait ainsi augmenter la taille des textes utilisant l'unité de mesure sp.
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 :
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).
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.
/* 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; } }
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.
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( , new Date()); bouton.setText(texte); } }
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.
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.
<?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" />
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.
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.
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 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.
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.
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 :
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( , 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.
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.
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.
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 :
<?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" />
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.
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.
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.
/* 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; } }
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.
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.
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);
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
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é.
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.
<?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.
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 :
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 :
<?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" />
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 :
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.
<?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);
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 :
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 :
<?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 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 :
<?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);
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.
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 :
<?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>
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); } }
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é.
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.
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.
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 :
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.
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 :
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); } }
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 :
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.
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.
<?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" />
<?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" />
<?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" />
<?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é.
.
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.
<?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" />
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 :
<?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" />
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); } }
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 :
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); } }
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 :
<?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>
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); } }
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 :
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); } }
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.
<?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>
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); } }
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.
<?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" />
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); } }
<?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" />
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".
<?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)" />
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).
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.
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().
<?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" />
<?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" />
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); } }
Nous allons reprendre les mêmes saisies mais cette fois-ci au travers d'une boîte de dialogue DatePickerDialog.
<?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" />
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( , 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(); } }
Nous allons reprendre les mêmes saisies mais cette fois-ci au travers d'une boîte de dialogue TimePickerDialog.
<?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" />
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( , 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(); } }
Si une opération doit durer un certain temps, les utilisateurs doivent pouvoir :
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.
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.
<?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 :
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.
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(); } }
<?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" />
<?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" />
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.
<?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>
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) { } }
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.
<?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>
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
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.
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 :
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.
.
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.
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 :
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.
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 :
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.
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.
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".
<?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.
<?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>
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.
<?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.
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()); } }
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.
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.
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.
Les propriétés restantes concernant RelativeLayout possède comme valeur l'identité d'un widget du conteneur. Pour ce faire :
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.
.
Quatre propriétés permettent de contrôler la position d'un widget par rapport aux autres :
Cinq autres propriétés permettent de contrôler l'alignement d'un widget par rapport à un autre :
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).
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.
<?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.
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 :
<?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.
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.
<?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.
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.
.
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.
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.
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.
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.
<?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é :
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()); } }
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.
<?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.
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.
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.
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.
<?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.
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 :
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.
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( ); analogique.setContent(R.id.analog); analogique.setIndicator( ); onglets.addTab(analogique); // Ajout du deuxième onglet TabHost.TabSpec digitale = onglets.newTabSpec( ); digitale.setContent(R.id.digital); digitale.setIndicator( ); 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é.
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( ).setContent(R.id.analog).setIndicator( )); onglets.addTab(onglets.newTabSpec( ).setContent(R.id.digital).setIndicator( )); } }
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.
<?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".
<?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>