Mémo Android
Michel Llibre - Avril 2022
Table des matières
1 Généralités et définitions 6
1.1 Abbréviations utilisées 6
1.2 Logiciels indispensables à télécharger 6
1.2.1 Android SDK 6
1.2.2 Ant 6
1.2.3 Java 6
1.2.4 Eclipse 6
1.2.5 Path et variables d'environnement 6
1.3 Commandes DOS installées par le SDK Android 7
1.4 Version Android et API level (niveau des API) 7
2 Quelques mots sur le langage XML 7
3 Les applications 8
4 Première application sous ECLIPSE 9
5 Problèmes fréquents sous Eclipse 9
5.1 Librairie manquante 9
5.2 Constantes manquantes 10
6 Appli en mode CONSOLE avec Ant 10
7 Utilisation d'ADB (Android Debug Bridge) 12
7.1 Connexion du smartphone (dit périphérique) 12
7.2 Périphériques et simulateurs 12
7.3 Quelques commandes adb 12
7.4 Sauvegarde du smartphone 12
8 Le fichier AndroidManifest.htlm 13
8.1.1 Installation sur Sd_carte 14
8.1.2 Spécification sdk minimum et visé 14
8.1.3 Autorisation diverses 14
8.1.4 Balise application 14
8.1.4.1 Balise activity 15
8.1.4.1.1 Balise intent-filter 15
9 Localisation des fichiers 17
9.1 Fichiers sources java 17
9.2 Fichiers ressources 17
10 Application minimale android 17
10.1 Approche textuelle 17
10.2 Approche graphique avec descripteurs xml 18
11 Les fichiers ressources (values, layout, drawable, menu, raw, xml, ...) 18
11.1 Ressources de la catégorie value (string, array, color, dimen, style) 19
11.1.1 string 19
11.1.2 array 19
11.1.3 color 19
11.1.4 dimen 20
11.1.5 Style 20
11.2 Accès au contenu des fichiers ressources 20
11.2.1 Acces aux éléments de res/value dans java 21
11.2.2 Acces aux éléments de res/layout dans java 21
11.2.2.1 Désérialisation de l'arbre de vues dans java 21
11.2.2.2 GetIdentifier 21
11.2.2.3 Inflation 22
12 Fontes et TextView 22
13 Classes utiles et formats divers 23
13.1 Type MIME 23
13.2 URI, URL et URN 24
13.3 Toast : Affichage message fugitif 25
13.4 Boite de dialogue minimale : AlerDialog.Builder 25
13.5 DialogFragment 26
13.5.1 Non communiquant 26
13.5.2 Communiquant 26
13.5.3 Transmission d'arguments de l'activité au DialogFragment 27
13.5.4 Dialogue de choix dans une liste 28
13.6 Sortie d'un journal. La classe Log 29
14 Cycle de vie d'une activity. Bundle. Sauvegarde des variables 29
14.1 Utilisation d' onSaveInstateState et de onRestoreInstance 32
14.2 Surcharge d' onRetainNonConfigurationInstance 33
15 Les widgets 33
15.1 Le widget générique View 33
15.1.1 Mode de remplissage, gravité et marges 34
15.1.2 Visibilité 34
15.1.3 Callback suite à un click ou autre associée par le fichier xml 34
15.1.4 Utilisation de l'interface OnXyzListener 35
15.1.4.1 La classe parent (généralement l'activité) implémente OnXyzListener 35
15.1.4.2 Utilisation d'une classe spécifique qui implémente OnXyzListener 35
15.1.4.3 Utilisation d'une implémentation anonyme immédiate de OnXyzListener 35
15.1.4.4 Utilisation d'une implémentation anonyme intermédiaire de OnXyzListener 36
15.1.4.5 Événement consommé ou non 36
15.2 Les conteneurs 36
15.2.1 Include 36
15.2.2 LinearLayout 37
15.2.3 RelativeLayout 37
15.2.4 TableRow et TableLayout 38
15.2.5 ScrollView et HorizontalScrollView 38
15.2.6 RadioGroup 38
15.2.7 TabHost (Onglets) 38
15.3 Modification dynamique d'une disposition 39
15.4 Les widgets de base 39
15.4.1 TextView 39
15.4.2 Button 40
15.4.3 ImageView et ImageButton 40
15.4.4 CheckBox 41
15.4.5 RadioButton 41
15.4.6 DatePicker 42
15.4.7 RatingBar 43
15.4.8 EditText 43
15.4.8.1 Callback associée à la touche Suivant/Ok du clavier virtuel 44
15.4.8.2 Callback associée à la perte de Focus 45
15.5 Sélection de données dans les widgets d'affichage : AdapterView 45
15.5.1 Création de l'ArrayAdapter 46
15.5.2 Affectation de l'ArrayAdapter à l'afficheur 47
15.5.3 Accès à l'élément sélectionné par l'usager 47
15.6 ListActivity 47
15.7 ListView personnalisés 49
15.7.1 Personnalisation statique 49
15.7.2 Personnalisation dynamique : extension d'ArrayAdapter 49
15.7.2.1 LayoutInflater. inflate() : traduction d'un fichier xml en vue 50
15.7.2.2 Accélération de l'accès aux vues 50
16 Traitement toucher écran 51
17 Intent et sous-activités 51
17.1 Déclaration dans le fichier AndroidManifest.xml 51
17.2 Intent : Lien de lancement explicite d'une activité 51
17.2.1 Lancement sans attente de retour 52
17.2.2 Lancemenent avec attente de retour 52
17.3 Perte des variables locales 56
17.4 Appel d'une application choisie par le système 56
17.4.1 Intent.ACTION_VIEW 56
17.4.2 Intent.ACTION_DIAL, Intent.ACTION_CALL, Intent.ACTION_ANSWER 56
17.4.3 Intent.ACTION_EDIT, Intent.ACTION_DELETE 57
17.4.4 Intent.ACTION_SEND 57
17.4.5 Intent.ACTION_WEB_SEARCH 57
17.5 Diffuser et recevoir des intents 57
18 Menus 58
18.1 Menu d'options 58
18.1.1 Création 58
18.1.2 Traitement 58
18.2 Menus Contextuel 58
18.2.1 Creation 59
18.2.2 Traitement 59
19 AlertDialog et LayoutInflater et exemple TextWatcher 59
20 Graphique 2D 63
21 Thread, Handler et synchronisation (Java standard) 64
21.1 Interface Runnable (Java standard) 64
21.2 Thread 65
21.3 Taches lancées ou cadencées par un Timer (TimerTask) 66
21.4 La classe de synchronisation Handler 67
21.5 Tache rigoureusement cadencée et communication avec l'UI 68
21.5.1 Utilisation d'objets Message 69
21.5.2 Utilisation d'objet Runnable 70
21.6 Les méthodes synchronized 70
21.7 La classe AsyncTask 71
22 Rotation, sauvegarde état de l'activité 72
22.1 Gestion par surcharge d'onConfigurationChanged 73
22.2 Prise en compte des threads en arrière plan 73
23 Sauvegardes et Système de fichiers 74
23.1 Stockage en mémoire privée 74
23.1.1 Mémoire interne privée 74
23.1.2 Mémoire externe privée 76
23.2 Utilisation du cache privé 76
23.3 Stockage en mémoire externe publique 76
23.3.1 Accès à la carte SD externe 77
23.4 Fonctions java standards de traitement des fichiers et répertoires 78
23.5 Accès en lecture seule aux fichiers "resources" 78
23.6 Sauvegarde des préférences 79
23.6.1 Deux accès manuels aux fichiers de préférences 79
23.6.2 Accès manuel aux données 79
23.6.3 Accès automatique aux fichiers de préférences 80
24 Support de différentes dimensions d'écrans 81
24.1 Attributs des écrans 81
24.2 Méthodologie 82
24.2.1 Définir les tailles avec des dp et sp 82
24.2.2 Définir la liste des appareils supportés 82
24.2.3 Fournir des patrons adaptés aux différentes tailles 82
24.2.4 Fournir des bitmaps adaptés aux différentes densités 82
24.2.5 Compléments 82
24.3 Annexe - Dimension des écrans 82
25 Les capteurs 83
25.1 Quels sont les capteurs en place 84
25.2 Quelles sont les caractéristiques d'un capteur 85
25.3 Mise en service d'un ou plusiers capteurs 85
25.3.1 Enregistrement – Dés-enregistrement 85
25.3.2 Callbacks de mise en service 85
25.4 Traitement des mesures 86
25.5 Les mesures d'orientation 87
25.6 Exemple : Liste des capteurs disponibles sur un appareil 88
26 Bluetooth 91
26.1 Permissions dans le Manifeste 91
26.2 Mise en service 91
26.3 Examen des appariements pré-existants 92
26.4 Ecoute pour couplage 92
26.5 Diffusion pour couplage 93
27 Utilisation du Native Development Kit 93
27.1 Nouveau projet NDK avec Eclipse 93
27.2 Nouveau projet NDK en ligne de commande 94
27.3 Nouveau projet NDK avec Android Studio 95
27.4 Génération d'un fichier entête 96
27.5 Exécuter un example du ndk 96
27.5.1 Sous Eclipse 96
27.5.2 En ligne de commande 97
27.6 Importation dans Android Studio d'un projet créé sous Eclipse ou ANT 97
27.6.1 Erreurs caractères accentués ANSI dans les fichiers sources java 97
27.6.2 Erreur absence de compilateur pour android-21 97
27.6.3 Erreur d'accès au NDK 97
28 Les fragments 97
28.1 Exemple de définition d'un fragment 98
28.2 Incorporation d'un Fragment dans une Activité 99
28.2.1 Incorporation statique d'un fragment dans une activité 99
28.2.2 Incorporation dynamique d'un fragment dans une activité 100
28.3 Cycle de vie d'un fragment 101
28.4 Recherche d'une instance de fragment 104
28.4.1 Cas incorporation statique : Recherche de son identificateur 104
28.4.2 Cas incorporation dynamique 104
28.4.2.1 Mémorisation à la création 104
28.4.2.2 Recherche par son étiquette 105
28.4.2.3 Autres cas 105
28.5 Communiquer avec les fragments 105
28.5.1 Utiliser le Bundle pour transmettre des arguments 106
28.5.2 Passage d'arguments par méthodes ordinaires 108
28.5.3 Fragment Listener 108
28.6 Quelques utilisations du FragmentManager 108
29 Annexe - Tuto installation en ligne de commande avec Ant 108
29.1 Installer java : "SDK java SE" 108
29.2 Installer "Apache ant" 110
29.3 Installer le SDK android 110
30 Raccourcis Android Studio 113
Site web fondamental : http://developer.android.com
L'aide sur les mots clés du langage dans Android Studio s'obtient par SHIFT F1 et marche mieux avec Chrome.
JDK : Java Development KIT ensemble nécessaire pour programmer en java.
SDK : Software Development Kit : Ensemble de logiciels pour développer des applications dans un certain langage. Dans le cas présent ceux qui sont destinés aux machines sous android. Remarque : Sous Vista ou Seven, pour mettre à jour le SDK, il semble qu'il soit préférable d'ouvrir la console en mode super-utilisateur pour ne pas avoir de problème lors de l'écriture sur le disque système.
ADT : Android Development Tools : Plugin Android pour Eclipse permettant de développer les applications android.
AVD : Android Virtual Device, est un émulateur sur le pc d'un appareil fonctionnant sous Android.
UI : User Interface c'est-à-dire l'interface utilisateur qui comprend l'écran et tout ce que l'utilisateur peut voir, entendre toucher, etc. On l'utilisera souvent pour désigner ce qu'on affiche sur l'écran.
XML : Extensible Markup Language ou langage de balisage extensible. Langage qui permet de structurer des données.
cde : abréviation personnelle pour commande (généralement tapée dans une console).
Site : http://developer.android.com/sdk/index.html
Une fois téléchargé et dézippé le bundle-sdk-etc... exécuter le programme SDK Manager en mode administrateur, qui installera effectivement le sdk. Re-exécuter de temps en temps pour les mises à jour, et en particulier exécuter plusieurs fois de suite car certains updates ne se font pas en un seul passage.
Pour compiler Android en mode ligne de commande avec ant, télécharger Apache ant sur le site http://ant.apache.org
Pour avoir l'environnement de développement java et sa documentation (pas indispensable, mais pratique pour accéder à la documentation hors ligne), faire le téléchargement sur le site :
http://www.oracle.com/technetwork/java/javase/downloads/index.html
Pour éditer, compiler et charger Android avec Eclipse, télécharger :
- Eclipse IDE for Java Developers : http://www.eclipse.org/downloads
Pour installer le greffon (plugin) ADT dans Eclipse, lancer Eclipse et faire menu Help > Install New Software, … Add, name : ADT Plugin (par exemple),
URL : https://dl-ssl.google.com/android/eclipse, OK, … cocher la case Developper Tools, Next, .., Finish. Redémarrage : Menu Windows > Preferences, Android, Browse … localiser le répertoire contenant l’android-SDK, puis OK.
En notant DIR_ANDROID_SDK le répertoire où est installé l'Android-sdk (qui chez moi est le répertoire C:\Program Files (x86)\Android\android-sdk) ajouter, si ce n’est déjà fait, ces variables à l'environnement utilisateur.
JAVA_HOME=C:\Program Files\Java\jdk1.6.0_25
ANT_HOME=C:\apache-ant-1.8.2
PATH=DIR_ANDROID_SDK\tools;DIR_ANDROID_SDK\platform-tools;C:\apache-ant-1.8.2\bin
Par ailleurs, ne pas hésiter à consulter la documentation qui se trouve dans DIR_ANDROID_SDK\docs, et les examples qui se trouvent dans DIR_ANDROID_SDK\samples.
emulator : cde qui lance le simulateur précisé en argument (après -avd)
adb : Android Debug Bridge : cde qui permet de communiquer avec un appareil android ou le simlateur (et qui permet d'installer les *.apk)
ant : Commande de compilation. Ant est un logiciel d'automatisation des taches informatiques, un make à la mode xml.
android : cde permettant de mettre à jour le SDK ou les AVDs selon l'argument utilisé. Il existe 2 cdes spécifiques : SDK Manager.exe et AVD Manager.exe.
keytool : To generate a keystore and private key, used to sign your .apk file. Keytool is part of the JDK.
Jarsigner : To sign your .apk file with a private key generated by Keytool. Jarsigner is part of the JDK.
Version Platform |
Année |
API level |
Version code |
Android 7.0 ou N |
2016 |
24 |
Nougat |
Android 6.0 |
2015 |
23 |
Marshmallow |
Android 5.0 |
2014 |
21 |
Lollipop |
Android 4.4 |
2013 |
19 |
Kitkat |
Android 4.1 |
2012 |
16 |
Jelly Bean |
Android 4.0 |
2011 |
14 |
Ice Cream Sandwich |
Android 3.0 |
2011 |
11 |
Honeycomb |
Android 2.3 |
2010 |
9 |
Gingerbread |
Android 2.2 |
2010 |
8 |
Froyo |
Android 2.0 |
2009 |
5 |
Eclair |
Android 1.6 |
2009 |
4 |
Donut |
Android 1.5 |
2009 |
3 |
Cupkake |
Android 1.1 |
2008 |
2 |
Bananas Split |
Android 1.0 |
2007 |
1 |
Apple Pie |
A sauter pour ceux qui connaissent XML.
- Balise : Le mot clé situé après le signe < s'appelle une balise. A chaque balise correspond une fonction spécifique.
- Attributs : Les paramètres de cette fonction sont appelés des attributs. Les attributs présents qui sont instanciés précèdent un signe = .
- Valeur : Ce qui se trouve après le signe =, entre deux guillemets doubles " ou simples ' s'appelle la valeur.
Attribut="Valeur" ou "Attribut='Valeur'
Les attributs sont spécifiques à chaque balise, ainsi que les valeurs qu'ils peuvent prendre, tout cela étant définit par l'application qui utilise le langage XML.
Deux possibilités pour la portée d'une balise :
1.La balise apparaît deux fois, une fois en ouverture et une fois en fermeture comme ceci : <balise1 attributs_eventuels="val_att">Texte éventuel ou autres balises</balise1>. La <balise1..> d'ouverture précède un champ interne et est refermée par la balise de fermeture </balise1>,
2.ou lorsqu'il n'y a pas de corps interne : <balise1 attributs_eventuels="val_att"/> qui fait ouverture et fermeture dans le même champ débutant par < et finissant par />.
Une première ligne optionnelle, définit en général le codage utilisé pour les caractères. Elle a, par exemple, la forme suivante :
<?xml version="1.0" encoding="utf-8"?>
Les différents niveaux de balise ne doivent pas se chevaucher, autrement dit une balise2 interne à la balise1 doit se refermer à l'intérieur de la balise1, avant la fermeture de celle-ci.
Dans les chaînes de caractères, c'est-à-dire entre les guillemets, les caractères < > et & sont interdits et respectivement remplacés par >, < et &.
Entre deux guillemets doubles ", le caractère " est interdit et remplacé par " et entre deux apostrophes ', le caractère ' est interdit et remplacé par &apos.
Les commentaires sont délimités par les balises <!-- et -->. Il ne doit pas y avoir de doubles tirets -- à l'intérieur d'une zone commentée.
Exemple :
<project name="projNow" default="help">
<property name="target" value="android-10"/>
<property file="ant.properties" />
<loadproperties srcFile="project.properties" />
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>
balises : project, property, loadproperties, import
attributs présents de la balise project : name, default
attributs présents de la balise property : name, value, file
attribut présent de la balise loadproperties : srcFile
attribut présent de la balise import : file
Plusieurs balises du type :
<property name="target" value="android-10"/>
peuvent être remplacées par une balise du type
<property file="fichier.properties" />
en mettant directement les affectations dans le fichier fichier.properties sous la forme :
target=android-10
Etc... voir : http://www.siteduzero.com/tutoriel-3-33440-le-point-sur-xml.html
ou http://zvon.developpez.com/tutoriels/xml/
Le code d'une application est transféré à l'appareil sous forme d'un fichier *.apk soit directement par téléchargement à l'aide du navigateur internet, par exemple depuis Google Play, ou à l'aide du programme adb depuis le pc (voir plus loin).
Chaque application est vu par le système Linux comme un usager différent muni de son propre identifiant id. Chaque application s'exécuter dans un process linux différent et dans sa propre machine virtuelle (VM), isolée ainsi des autres applis. En fait on peut donner le même identifiant id à plusieurs applis et utiliser la même VM.
L'application doit avoir des permissions pour accéder aux certaines données comme la liste des contacts, les messages SMS, les cartes mémoires, la caméra, le Bluetooth, etc.. Les permissions demandées par l'application doivent être accordées par l'usager lors de l'installation, qui malheureusement (pour le moment) doit toutes les accepter s'il veut pouvoir installer l'application.
Il y a 4 types d'applications android : activity, service, content provider et broadcast receiver. On s'intéresse surtout ici aux activités qui se présentent sous forme d'une fenêtre avec (en général) une interface utilisateur.
0) Configurer le simulateur (à faire une seule fois pour toutes) :
Windows > Android SDK > AVD Manager ...New. Donner un name et choisir la target, puis Create AVD. Le simulateur est créé et ajouté à la liste des AVD disponibles.
Remarque : Suite à de mauvaises manipulations, l'AVD Manager s'est mis à créer les AVD dans le répertoire DIR_ANDROID_SDK\.android\avd alors qu'il se trouvait auparavant dans c:\users\llibre. Et dans ce répertoire (qui est sous c:\program Files (x86) ...) il était inaccessible. Pour que l'AVD Manager gère les AVD dans un répertoire accessible il faut créer une variable d'environnement ANDROID_SDK_HOME qui pointe sur c:\users\llibre, dossier qui contient le dossier .android\avd.
1) Création project : File > New > Project... puis Android > Android project. Remplir les champs en nommant l'application monAppli par exemple.
Modifier éventuellement le fichier monAppli.java dans l'arborescence de gauche, sous le répertoire src...
La compilation est automatique.
2) Configurer les chemins : Clic droit sur le projet, Build Path > Configure Build Path, puis fenêtre Java Build Path, onglet Order and Export, cocher la case correspondant à la cible visée si ce n’est pas déjà fait. Si elle n’est pas présente, la créer (cf. étape 0)
3) Chargement (exécution) : sélectionnez le menu Run > Run ... Android Application.
Le simulateur est lancé. Son démarrage est assez long, plusieurs minutes avec le texte android..., puis le dessin ANDROID. Attendre PATIEMMENT l'apparition du menu avec le cadenas à faire glisser vers la droite avec la souris.
Pour créer une application Eclipse à partir de code existant faire :
1) Création : File > New > Project, Android > Android Project > Next, Create Project from existing source, Browse et choisir le répertoire où se trouve le fichier AndroidManifest.xml (à créer préalablement si absent), Next, choisir la cible, puis clic sur Finish.
2) et 3) comme ci-dessus.
Pour importer un projet Eclipse existant, passer par // File > Import > …. NON
File > New > Android Project > Create project from existing source > Browse ... > Finish
Certaines librairies exotiques ne sont pas trouvées automatiquement par Eclipse, en particulier les librairies qui permettent un "support" de compatibilité entre les anciennes versions et les nouveautés, comme par exemple le :
import android.support.v4.app.xxxxx;
Pour ce exemple, il faut importer dans Eclipse la librairie android-support-v4.jar qui se trouve dans :
C:\Applis\android-sdk\extras\android\support\v4
Pour cela :
•ou bien on copie ce fichier dans un répertoire libs du projet au même niveau que le répertoire src
•ou bien on utilise le menu Project\properties\Java build Path\Libraries\Add external Jars… et on sélectionne le fichier jar avec le navigateur.
Le compilateur peut ne pas trouver certains éléments comme des constantesspécifiant un style, ou autre.., car la compilation se fait à un certain niveau de définition des API. Pour choisir le bon niveau de compilation dans Eclipse, clic-droit sur le projet, menu Properties/Android/Project Build Target , cocher parmi les divers niveau proposés celui qui connaît tous les éléments utilisés (essais successifs).
Avant de créer son appli, créer un ou plusieurs smartphones similés (appelés Android Virtual Device ou AVD ou simulateur) :
1) Créer un AVD si ce n'est déjà fait :
>android avd
ou mieux :
>"AVD Manager.exe"
Bien noter le nom de l'AVD (1ere colonne : AVD name), il lui correspond un fichier nomDeLavd.ini et un répertoire nomDeLavd.avd dans le répertoire c:\users\nomutilisateur\.android\avd.
Remarque : Suite à de mauvaises manipulations, l'AVD Manager s'est mis à créer les AVD dans le répertoire DIR_ANDROID_SDK\.android\avd alors qu'il se trouvait auparavant dans c:\users\llibre. Et dans ce répertoire (qui est sous c:\program Files (x86) ...) il était inaccessible. Pour que l'AVD Manager gère les AVD dans un répertoire accessible il faut créer une variable d'environnement ANDROID_SDK_HOME qui pointe sur c:\users\llibre, dossier qui contient le dossier .android\avd.
2) Regarder son numéro dans la liste des simulateurs
>android list targets
Available Android targets:
----------
id: 1 or "android-10"
Name: Android 2.3.3
Type: Platform
Remarque : Le nom qui apparait ici est le Target name (nom du vrai matériel). Il peut être identique ou différent de l'Avd name (nom du matériel simulé qui dans notre cas est android233).
3) Lancer le simulateur :
>emulator -avd nomDeLavd
ou bien
>emulator @nomDeLavd
ou nomDeLavd est l'AVD name.
4) Ensuite on peut créer une application, par exemple l'application par défaut qui écrit Hello ..... Pour cela, dans un répertoire de travail, faire par exemple :
D:\temp>android create project --name projNow --path pathNow --activity Now --package fr.llibre.android --target 1
Created project directory: D:\temp\pathNow
Created directory D:\temp\pathNow\src\fr\llibre\android
Added file D:\temp\pathNow\src\fr\llibre\android\Now.java
Created directory D:\temp\pathNow\res
Created directory D:\temp\pathNow\bin
Created directory D:\temp\pathNow\libs
Created directory D:\temp\pathNow\res\values
Added file D:\temp\pathNow\res\values\strings.xml
Created directory D:\temp\pathNow\res\layout
Added file D:\temp\pathNow\res\layout\main.xml
Added file D:\temp\pathNow\AndroidManifest.xml
Added file D:\temp\pathNow\build.xml
Added file D:\temp\pathNow\proguard.cfg
Dans cet exemple
- pathNow est le nom du répertoire où on va mettre les fichiers de développement de l'application. Si on est déjà dans ce répertoire, on peut mettre "." pour signifier le répertoire courant.
- projNow est le nom du projet. Le fichier *.apk généré par la compilation s'appellera projNow-xxx.apk où xxx dépend du mode de compilation.
- Now est le nom donné à la classe activity java, à l'icône qui apparait dans la fenêtre du choix des activités et à l'application qui apparait dans le menu gestionnaire des applications. On peut différencier ces trois noms, en allant dans le fichier AndroidManifest.xml, cf. ci-après.
- fr.llibre.android est le nom du paquetage qui permettra d'identifier l'activité de manière unique si on la distribue. L'arborescence suivante est créée :
pathNow/src/fr/llibre/android/Now.java.
C'est lourd, mais pour s'en passer, il faut aller trafiquer dans les fichiers xml, alors on fait avec.
- 1 est le numéro de la cible sur laquelle on veut faire tourner l'activité. C'est le numéro choisi à l'étape 2 (android list targets).
5) Compilation : Dans pathNow taper la cde :
>ant clean debug
6) Charger l’application :
Pour charger l'application dans le simulateur faire :
>adb install bin\projNow-debug.apk (par exemple si compilée en mode debug)
Il peut y avoir un échec (problème de synchro ?) et il peut être nécessaire de réitérer la commande.
Ajouter l'option -d entre adb et install pour l'installer sur le périphérique (d pour device).
Elle s'installe également par la commande :
>ant install bin\projNow-debug.apk
mais dans mon cas j'ai un message d'erreur, bien que l'application s'installe correctement.
Description du fichier build.xml :
La compilation est gérée par le logiciel ant qui s'appuie sur la description fournie par le fichier
build.xml :
<?xml version="1.0" encoding="UTF-8"?>
<project name="projNow" default="help">
<property file="local.properties" />
<property file="ant.properties" />
<loadproperties srcFile="project.properties" />
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>
Ce fichier build.xml définit d’abord le nom du projet projNow, puis il définit 3 fichiers où d'autres définitions sont fournies : local.properties, ant.properties et project.properties, et finalement il importe le fichier build.xml standard du SDK android (qui décrit toutes les procédures de compilation, chargement, etc...).
Le fichier ant.properties est vide.
Le fichier local.properties contient :
sdk.dir=C:\\Program Files (x86)\\Android\\android-sdk
Il définit la variable sdk.dir qui est utilisé dans la balise import ci-dessus pour localiser le fichier build.xml standard du SDK android.
Le dernier fichier project.proporties contient :
target=android-10
qui correspond à la cible que nous avons spécifiée par l'argument --target 1 de la cde android create project --name projNow .....
Pour simplifier cette description, on pourrait se passer des 3 fichiers *.properties en mettant les deux définitions dans des balises property ce qui donnerait le fichier build.xml suivant :
<?xml version="1.0" encoding="UTF-8"?>
<project name="projNow" default="help">
<property name="target" value="android-10"/>
<property name="sdk.dir" value="C:\\Program Files (x86)\\Android\\android-sdk"/>
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>
Renommer une Application : Généralement le nom du projet dans le build.xml par exemple Toto est aussi donné à la classe principale, avec une majuscule, et également donné au fichier java : Toto.java, mais aussi au dernier item du package fr.llibre.toto (sans majuscule), nom qui apparaît généralement en première du programme principal Toto.java, mais aussi dan le fichier AndroidManifest.xml sous la forme package="fr.llibre.toto" et <activity android:name="Toto" et éventuellement dans le fichier res/values/string.xml sous la forme <string name="app_name">Toto</string>. Si on change ce nom, il faut le faire 8 fois :
1 nom du projet dans le build.xml, 2. nom du répertoire, 3 nom du fichier java, 4 nom de la classe java, 5 nom du package dans le fichier java, puis 6 nom du package dans le fichier AndroidManifest.xml et 7 nom de l'activité, puis 8 dans le fichier res/values/string.xml.
Pour que le smartphone Samsumg Note 4 connecté par USB au PC soit accessible au logiciel ADB, il faut le débloquer au niveau débug. Pour cela :
Dans le menu Paramètres/Système/A propos de l'appareil, taper 7 fois sur l'item « numéro de version » (build number). Apparait ensuite un nouvel item dans le menu Paramètres/Système qui est « options de développement ». Aller dans ce menu et cocher la case « Debogage USB ».
Liste des simulateurs ou périphériques branchés (plusieurs simulateurs peuvent tourner en même temps) :
>adb devices
List of devices attached
emulator-5554 device
emulator-5556 device
le qualificatif device signifie que le simulateur emulator-5554 est en marche.
Envoi d'une commande au simulateur ou au périphérique :
>adb [perif] <Commande>
où perif peut valoir :
-d : pour le périphérique unique branché sur le port usb
-e : pour le simulateur unique en cours
-s <numero-serie> tel que d'un des périfs simulés ou branchés en cours.
Parmi les commandes disponibles :
install pour charger une application sur un device : > adb -d install helloWorld.apk
push pour copier un fichier/répertoire du pc vers le device : >adb push <local> <remote>
pull pour copier un fichier/répertoire du device vers le pc :> adb pull <remote> <local> où <local> est relatif au pc et <remote> est relatif au device.
wait-for-device pour mettre en attente de la disponibilité (marche) du device
shell pour démarrer un remote_shell
shell [cde] pour envoyer une commande linux particuière au device.
kill-server pour arrêter adb. Nécessaire quand on veut débrancher le smartphone, sinon windows refuse de débrancher le pérphérique usb.
>adb backup -f sauvegarde.ab -apk -shared -all -system
Remarque : attendre que la sauvegarde soit finie pour fermer le terminal.
Plusieurs paramètres sont disponibles afin d’inclure ou d’exclure certaines données de la sauvegarde :
•-apk pour inclure les applications installées
•-noapk pour exclure les applications installées
•-shared pour inclure les cartes mémoire
•-noshared pour exclure les cartes mémoire
•-all pour inclure toutes les applications
•-system pour inclure les applications système
•-nosystem pour exclure les applications système
Il s’agit d’une méthode fiable et sans risques dont le plus grand avantage est qu’elle est compatible avec toutes les versions d’Android.
Pour explorer le contenu afin de récupérer les applications ou les paramètres, on peut utiliser le programme ABE (pour Android Backup Extractor). Il s’agit un JAR (archive Java) disponible gratuitement et sous licence libre.
1.Récupérer la dernière version d’ABE (http://sourceforge.net/projects/adbextractor/files/latest/download) sur SourceForge ;
2.Décompresser l’archive dans un dossier ;
3.Ouvrir un terminal et se placer dans le dossier ;
4.Exécuter la commande ci-dessous en l’adaptant :
java -jar abe.jar unpack sauvegarde.ab archive.tar mot_de_passe
Le fichier résultant est une archive TAR que l'on doit décompresser pour pouvoir accéder aux fichiers de la sauvegarde depuis l'explorateur de fichiers.
Pour effectuer l’opération inverse, c’est à dire reconstruire le fichier de sauvegarde, exécuter la commande suivante :
java -jar abe.jar pack archive.tar sauvegarde.ab mot_de_passe
Enfin pour obtenir des informations sur le fichier de sauvegarde :
java -jar abe.jar info sauvegarde.ab mot_de_passe
Le fichier AndroidManifest.htlm est un fichier qui décrit l'application et les composants (activités, services,...) qu'elle fournit.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.llibre.julien"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="10" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme" >
<activity
android:name="fr.llibre.julien.JulienActivity"
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>
- La première ligne (prologue) est optionnelle : <?xml version="1.0" encoding="utf-8"?>
- La deuxième ligne :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
déclare grace à l'attribut réservé xmlns (name space xml) le langage android qui appartient à l'espace de nom http://schemas.....
- La troisième ligne définit le nom du package : fr.llibre.julien. Chaque application téléchargée doit avoir un nom de package unique, sinon on ne pourra pas la télécharger si une application a déjà le même nom de package.
- Ensuite sont définis la version de notre code (entier strictement croissant), puis son nom de version (arbitraire).
Par défaut l'application est installée en mémoire interne. Si on veux l'installer ailleurs on spécifie, par exemple après le numéro de version :
android:versionName="1.0"
l'installation par défaut désiré pour cette application :
android:installLocation="auto"
avec à la place de "auto" :
•"internalOnly" pour obliger l'installation en mémoire interne.
•"preferExternal" si on préfère l'installation sur sd_carte externe.
Ces balises sont facultatives.
<uses-sdk android:minSdkVersion="13" android:targetSdkVersion="17" />
La balise <uses-sdk ... /> utilisée au plus haut niveau permet de spécifier :
•avec android:minSdkVersion le niveau d'API (voir chapitre 1.4) minimal requis sur l'appareil pour exécuter cette application. Si absent, c'est le niveau 1.
•avec android:targetSdkVersion le niveau d'API de l'appareil visé par l'application. Si l'appareil tourne avec une version plus élevée, (c'est-à-dire plus récente), son système appliquera des correctifs de rétro-compatibilité pour faire fonctionner l'application. Si non spécifié c'est l'attribut de android:minSdkVersion qui est utilisé.
Ces balises sont facultatives.
Android interdit pratiquement tout échange entre une appli et son système hote. L'appli doit demander des autorisations d'accès qui son affichées lors du chargement de l'appli, ce qui permet à l'utilisateur d'arrêter ce chargement s'il ne veut as accorder ces autorisations.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> permet de demander l'accès en lecture à la carte mémoire supplémentaire.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> permet de demander l'accès en écriture (inclus l'accès en lecture) à la carte mémoire supplémentaire.
L'activité est munie normalement munie d'une barre de titre et d'une barre de status. La barre de titre peut être supprimée en ajoutant la ligne suivante à l'intérieur de la rubrique <activity ... /> :
android:theme="@android:style/Theme.NoTitleBar"
et on peut supprimer la barre de titre et la barre de status en ajoutant la ligne suivante :
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
Pour qu'une application puisse faire un appel téléphonique, il faut ajouter la ligne suivante :
<uses-permission android:name="android.permission.CALL_PHONE" />
- La balise application doit être unique dans le manifeste. L'attribut android:label reçoit le nom donné à l'application (fourni par la variable @string/app_name décrite ci-après). C'est le nom qui apparait dans le menu gestionnaire des applications et qui apparaît sous l'icône dans la fenêtre de choix des activités. Si l'attribut android:label n'est pas présent l'application figure dans le gestionnaire des applications avec le nom du package.
La spécification :
android:hardwareAccelerated="true"
permet d'activer l'accélération matérielle des graphiques 2D qui permet de bénéficier des effets graphiques spéciaux (fond dégradé, ...).
A l'intérieur de la balise application, on peut déclarer un autre activity, mais également un service, un receiver, un provider, ou plusieurs de ces éléments.
La spécification :
android:icon="@drawable/ic_launcher"
permet de spécifier l'icone de l'appli qui est, ici, fournie dans le dossier res/drawable sous forme d'un fichier image ic_launcher.png.
Dans cette application on déclare l'activity principale lancée au démarrage (spécificié juste après par les balises intent). L'attibut android:name fournit le nom de la classe java dérivant de cette activity qui correspond ici au fichier ./src/fr/llibre/android/Now.java. C'est la seule balise obligatoire du manifeste (évidemment précédée des champs application et activty).
Dans la balise activity les balises intent-filter indiquent comment l'activité peut être activée par les aures composants. Ici la balise <action android:name="android.intent.action.MAIN" /> précise que l'action qui va être effectuée est le démarrage initial de l'activité, et la balise <category android:name="android.intent.category.LAUNCHER" /> précise que cette activité sera inscrite dans le lanceur d'applications du système pour permettre aux utilisateurs de lancer cette activité.
Dans le cas où on veut que cette application soit lancée par un clic effectué dans un navigateur internet, un lecteur mail ou un navigateur de fichiers sur un fichier ayant l'extension .abc, on joutera les balises intent-filter suivantes :
<!-- Pour l'email -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:pathPattern=".*\\*.abc" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- Pour http -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:host="*" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Pour https -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
<data android:host="*" />
<data android:pathPattern=".*\\*.abc" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Pour un navigateur de fichier et google drive -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:host="*" />
<data android:pathPattern=".*\\*.abc" />
<data android:mimeType="*/*" />
</intent-filter>
où la balise <action android:name="android.intent.action.VIEW" /> spécifie que l'application va traiter le fichier sélectionné,
la balise <category android:name="android.intent.category.DEFAULT" /> signifie que cette activité peut être lancée (balise obligatoire sauf si "android.intent.action.MAIN" ou "android.intent.category.LAUNCHER" sont spécifiées,
et la balise <category android:name="android.intent.category.BROWSABLE" /> spécifie le navigateur de fichiers peut lancer l'application (inutile semble-t-il ??).
Les balises <data android:XXXXX="YYYYYY"/> permettent de spécifier le type de fichier. C'est pas mal compliqué. Ici on spécifie :
•que c'est un fichier : <data android:scheme="file" />
•de type non répertorié : <data android:mimeType="*/*" />
•d'extension .abc : <data android:pathPattern=".*\\.abc"/> et suivants,
•provenant de n'importe quelle machine : <data android:host="*"/>
•Les autres balises data android:scheme sont peut-être inutiles, mais pas sûr.
En l'absence de certaines de ces balises, l'application est appelée pour d'autres types de fichiers. On doit pouvoir en enlever certaines. A tester.
Exemple qui ne marche pas avec le navigateur standard offert par samsung, mais qui marche pour le navigateur de fichier "asus" quand il y a la balise <data android:scheme="content" />.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="*/*" />
<data android:scheme="content" />
<data android:scheme="file"
android:host="*"
android:pathPattern=".*\\.m2j" />
<data android:scheme="file"
android:host="*"
android:pathPattern=".*\\..*\\.m2j" />
<data android:scheme="file"
android:host="*"
android:pathPattern=".*\\..*\\..*\\.m2j" />
<data android:scheme="file"
android:host="*"
android:pathPattern=".*\\..*\\..*\\..*\\.m2j" />
</intent-filter>
Il semble que ça marche en mettant un seul \ à la place des \\.
Les sources java à éditer se trouvent dans .\src\…(package)...\xxxx.java.
Les ressources sont des fichiers statiques fournis avec l'application. Ils sont regroupées dans le répertoire res. On trouve principalement les ressources suivantes :
•res/drawable/ pour les icônes et images (png, jpg,..)
•res/layout/ pour les description xml des patrons des interfaces graphiques utilisateurs,
•res/menu/ pour les description xml des menus,
•res/raw/ pour des fichiers de nature quelconque traités par l'appli.
•res/values/ pour des strings, des identificateurs, des valeurs, ...
•res/xml/ pour des fichiers xml quelconques traités par l'appli.
Il semblerait que les noms des fichiers ressources doivent uniquement comporter des lettres en minuscules ou des chiffres (a-z0-1).
En plus de son fichier AndroidManifest.xml, le code comportera, par exemple, le fichier ./src/fr/llibre/firstapp/act1.java suivant :
package fr.llibre.firstapp;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class FirstAndroidActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText("Hello Michel !");
setContentView(tv);
}
}
La classe principale hérite de la classe Activity (paquet android.app.Activity). Cette classe hérite de la classe de base android Context qui fournit entre autre des services tel que la récupération des ressources, des accès base de données et des préférences.
La méthode public void onCreate(Bundle savedInstanceState) (avec comme argument la classe Bundle du paquet android.os.Bundle) contient le code de l’activité. Que fait-on dans cette méthode ? En premier lieu, on appelle la même méthode de la classe mère : super.onCreate(savedInstanceState);
Et ensuite on envoie un scène à afficher par la méthode setContentView(…) de la classe Activity.
Dans l’approche textuelle, on crée un objet TextView (du paquet android.widget.TextView) , on y met un texte (méthode setText) et on l’affiche sur l’écran par la méthode setContentView(View tv).
L'argument du constructeur de la classe TextView est une instance de la classe Context. Comme la classe principale hérite d'Activity qui elle-même hérite de Context, nous pouvons passer la référence this au constructeur.
De façon plus moderne, le contenu de la vue est décrit dans un fichier xml. L'application minimale précédente a pour fichier source java :
Fichier .\src\fr\llibre\firstapp\firstact.java :
package fr.llibre.firstapp;
import android.app.Activity;
import android.os.Bundle;
public class FirstAct extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mymain);
}
}
La scène à afficher (R.layout.mymain) est prise dans la classe R générée automatiquement (soit par Ecplise ou Android Studio, soit par l'outil aapt du répertoire tool du SDK). Cette classe R offre l'accès à toutes les ressources qui sont décrites par des fichiers xml dans le répertoire res de l'application. Ici ce répertoire res va contenir un sous-répertoire layout dans lequel on décrit des dispositions de vue. Dans ce sous-répertoire res/layout, le fichier mymain.xml va décrire la vue principale :
Fichier .\res\layout\mymain.xml :
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello Michel!" />
Dans ce fichier on décrit une instance de TextView, défini dans l’espace de nom XML Android. Ce TextView occupe toute la largeur de son conteneur, cf android:layout_width="fill_parent", et verticalement il est adapté à la taille de son contenu cf android:layout_height="wrap_content" et son texte est positionné par android:text="Hello Michel!".
Remarque : L’espace de nom (xmlns:android) n’est précisé que dans la balise de l’objet racine (celui qui contient tous les autres objets). Les autres objets, s'il y en a, héritent de cette déclaration.
Les fichiers ressources sont situés sous le répertoire res. Ils contiennent des informations constantes qui seront utilisées par l'application.
En particulier pour faciliter la traduction des chaînes de caractères on les regroupe dans un fichier de ressources du type .\res\values\mystrings.xml comme celui-ci :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="salutation">Hello Michel!</string>
<string name="app_name">myApp</string>
</resources>
Dans le fichier ressource .\res\layout\mymain.xml décrivant la vue de l'activité :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/salutation"/>
</RelativeLayout>
On fait référence au texte "Hello Michel!" en utilisant à la place "@string/salutation".
On remarquera qu'on a donné un identificateur au TextView avec la ligne :
android:id="@+id/tvText".
Le + après le @ dans @+id signifie qu'on définit un nouvel identificateur. Normalement, on y fera référence sans le + sous la forme "@id/tvText", mais si on y fait référence avant qu'il ait eté défini, on met le +.
En plus des sous-répertoires layout qui contient des fichiers xml décrivant des patrons pour des vues affichées à l'écran (comme .\res\layout\mymain.xml) et du répertoire values qui contient des fichiers xml fournissant des types moyennement simples : strings, identificateurs, valeurs (comme .\res\values\mystrings.xml), le répertoire res peut contenir les sous répertoires suivants :
- values pour des types moyennement simples (string, array, color, dimen, voir ci-après),
- layout : gabarits de mise en page d'écran,
- menu pour les description xml des menus,
- raw : fichiers de nature quelconque traités par l'appli et non convertis,
- drawable : fichier images (png, jpg) convertis en images ajustables,
- xml : fichiers xml divers accessibles par resources.getXML().
Ces ressources sont placées dans un fichier xml du répertoire res/value, de la forme :
<?xml version="1.0" encoding="utf-8"?>
<resources>
XXXXXX
</resources>
Elles sont à la place des xxx entre les deux balises <resources>XX...XX</resources>
<string name="app_name">myNewAppli</string>
<string name="leNom">Mon nom est <b>Toto</b></string>
Dans ce dernier string on a utilisé des balises pour string Html : on a encadré Toto par les balises <b>Toto</b> pour que Toto soit affiché en gras (bold). Mais comme le caractère "< " est spécial, on doit le remplacer par son code Html , à savoir "<".
ATTENTION. Si le string Html est défini directement dans le code java, les balises doivent être écrites normalement :
String leNom = "Mon nom est <b>Toto</b>";
On peut utiliser les trois balises bold, italique et souligné : <b>, <i> et <u>.
Pour être affiché dans un View par setText, un string Html doit être converti par la fonction Html.fromHtml(String source) qui renvoie un objet de classe Spanned qui est fourni en argument du setText.
Dans tous les strings, les caractères spéciaux ", 'et \, ... doivent être précédés d'un \.
Les strings peuvent contenir des éléments variables de type %n$d comme ceux traités en java par String.format() et qui sont sous Android directement traités par Resource.getString, Exemple dans res/strings.xml :
<string name="histoire">Vous connaissez l\'histoire de <b><i>%1$s</i></b> qui a %2$d ans ?</string>
Dans le code Android java :
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.textv);
String hist = getResources().getString(R.string.histoire, "Michel", 10);
Spanned mv = Html.fromHtml(hist);
tv.setText(mv);
<array name="mytab">
<item>14</item>
<item>18</item>
</array>
<color name="vert">#00FF00</color>
Il y a 4 formats différents pour décrire les couleurs :
#RGB avec 3 hexa
#ARGB avec 4 hexa (2 octets)
#RRGGBB avec 6 hexa
#AARRGGBB avec 8 hexa (4 octets).
Remarquons que fans le fichier xml, les 6 hexadécimaux précisant la couleurs sont précédés d'un #, alors que dans le code java ils sont précédés de 0x.
Pour définir des dimensions de graphique. Les principales unités connues par android sont :
- px : pixel physique
- in : pouce (ou inche = 25,4mm)
- mm : millimètre
- pt : point = 1/72 de pouce = 0,3528 mm de la hauteur
- dp (ou dip) : density-independent pixel = 1/160 ou 1/320 de la largeur de l'écran (à vérifier)
- sp : scale-independent pixel (voisin de dip, mais pour la taille des caractères)
Exemple :
<dimen name="taille-texte">5sp</dimen>
Si l'on compte utiliser plusieurs fois un ensemble de mêmes caractéristiques pour certains View, on a intérêt de les regrouper dans un ensemble d'attributs de la classe AttributSet dans le code java auquel correspond une balise style dans le code xml. Par exemple :
<style name="monStyle">
<item name="android:textColor">#00FFFF</item>
<item name="android:textSize">20sp</item>
</style>
Dans le fichier xml, on peut définir un nouveau style à partir d'un style existant avec l'attribut parent, et ensuite ajouter des item et/ou surcharger les item existants. Ex :
<style name="newStyle" parent="monStyle">
<item name="android:textColor">#543210</item>
<item name="android:typeface">monospace</item>
</style>
On conserve la taille 20sp, on change la couleur et on spécifie la police.
Pour attribuer ce style à un widget défini dans un layout xml, on utilise l'attribut style (seul, et non android:style comme on aurait tendance à le faire) :
<TextView
android:id="@+id/textv"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Bonjour tout le monde !"
style="@style/newStyle"
/>
Dans le code java, on peut l'affecter à un widget à l'aide de la méthode setTextAppearance(R.style.newStyle) à partir de l'API 23 et btn.setTextAppearance(this, R.style.newStyle) avant l'API 23. On peut également l'affecter lors de la construction d'un widget, comme suit :
TextView btn = new TextView(new ContextThemeWrapper(MainActivity.this,R.style.newStyle));
mais j'ai eu un échec dans le cas ou le premier style (monStyle) héritait d'un parent prédéfini : <style name="newStyle" parent="@android:style/TextAppearance.Medium">
alors que ce cas a marché avec l'affectation dans le layout xml par l'attribut style, et dans le code java par la méthode setTextAppearance.
On a déjà vu que pour utiliser la chaîne "Hello Michel!" depuis un autre fichier ressource xml on y accède par l'intermédiaire de la variable "@string/salutation". Autre exemple : pour utiliser le nom de l'application dans le fichier AndroidManifest.htlm, sous la balise application, on utilisera "@string/app_name" à la place de "myNewApplication", ce qui donnera :
<application android:label="@string/app_name" ... > ....
Supposons qu'on ait copié le fichier icône cw.png dans le répertoire res\drawable\ (une icône est généralement sous forme d'une image png 48x48). Pour l'attribuer à l'application, on spécifira dans le fichier AndroidManifest.htlm, sous la balise application, à l'attribut android:icon la valeur "@drawable/cw", comme ceci :
<application android:icon="@drawable/cw" android:label="@string/app_name">.
En résumé dans un fichier xml, on accède aux ressources par @type/un_nom, où type peut valoir drawable, string, dimen, color, array, id, ...etc, et on peut même créer ses propres catégories.
Pour accéder dans le source java a des instances des éléments définis dans le fichier res/value entre les balises <resources>, on procède comme suit :
Resources myres = getResources();
String strhello = myres.getString(R.string.hello);
en utilisant l'identificateur de l'élément sous la forme R.type.un_nom.
Considérons une vue décrite par un fichier xml, par exemple le fichier res/layout/mymain.xml suivant :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Michel"/>
</RelativeLayout>
L'accès dans le code java aux éléments définis dans ce layout sous forme xml s'appelle la désérialisation. On utilise pour cela la méthode View findViewById(int) de l'Activity. Mais cet accès ne peut se faire qu'après la création de l'arbre des vues de l'Activity qui est réalisée par l'appel de setContentView(R.layout.mymain). Ainsi l'accès au Textview ci-dessus se fera dans le source java comme suit :
setContentView(R.layout.mymain);
TextView tv = (TextView) findViewById(R.id.tvText );
tv.setText("Coucou !");
qui doit être placée après l'instruction setContentView afin que la création de l'arbre des vues de l'Activity soit achevée.
Pour récupérer de manière dynamique l'id d'une ressource à partir de la chaine de caractères représentant son nom, il faut réussir à retrouver son identifiant. Muni de cet identifiant le chargement de la ressource est naturel.
int layoutId =getResources().getIdentifier("nomRessource", "layout", "fr.llibre.exemple" );
La méthode getResources appartient à la classe ContextWrapper qui étend Activity (entre autres) et renvoie un objet de type Resources. Cette classe (Resources) permet de manipuler les ressources de l'application, en particulier leur récupération.
La méthode public int getIdentifier(String name, String defType, String defPackage) permet de retrouver l'identifiant généré par le compilateur pour une ressource particulière. Il suffit de lui passer le nom de la ressource, son type tel qu'il apparait dans le fichier de Ressource R généré et le package racine de l'application.
La méthode la plus simple pour accéder aux éléments d'une vue décrite par un fichier xml l'est par la méthode finfViewById(..) de l'activité mais qui ne fonctionne qu'après que la vue ait été affichée par la méthode setContentView(...). Si on veut accéder à un élement, pour le paramétrer d'une certaine manière, avant son affichage, il faut passer par l'inflation qui est une méthode des classes layout qui fournit l'accès aux éléments en désérialisant l'arbre xml au lieu d'y accéder par l'arbre de vues de l'Activity.
LayoutInflater li = getLayoutInflater();
LinearLayout lay = (LinearLayout) li.inflate(R.layout.activity_main, null, false);
TextView tv = (TextView) lay.findViewById(R.id.tvText);
tv.setText("Coucou !");
setContentView(lay);
Ainsi on peut modifier le contenu du TextView avant de l'afficher (en fait l'exemple n'est pas bon car en le modifiant après, l'effet est le même, vu que le nouveau texte remplace l'ancien instantanément).
La méthode getLayoutInflater() de la classe Activity (que ne possède pas la classe Fragment) fournit un objet qui possède la méthode inflate qui fournit le layout. On peut s'en passer en utilisant une méthode statique inflate des classes layout :
LinearLayout lay = (LinearLayout) LinearLayout.inflate(this, R.layout.mymain , null);
tv = (TextView) lay.findViewById(R.id.tvText);
tv.setText("Coucou !");
setContentView(lay);
Exemple de définition d'un TextView amélioré :
public class TextViewPlus extends TextView {
private static final String TAG = "TextView";
public TextViewPlus(Context context) {
super(context);
}
public TextViewPlus(Context context, AttributeSet attrs) {
super(context, attrs);
setCustomFont(context, attrs);
}
public TextViewPlus(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setCustomFont(context, attrs);
}
private void setCustomFont(Context ctx, AttributeSet attrs) {
TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.TextViewPlus);
String customFont = a.getString(R.styleable.TextViewPlus_customFont);
setCustomFont(ctx, customFont);
a.recycle();
}
public boolean setCustomFont(Context ctx, String asset) {
Typeface typeface = null;
try {
typeface = Typeface.createFromAsset(ctx.getAssets(), asset);
} catch (Exception e) {
Log.e(TAG, "Unable to load typeface: "+e.getMessage());
return false;
}
setTypeface(typeface);
return true;
}
}
attrs.xml: (Where to place res/values)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextViewPlus">
<attr name="customFont" format="string"/>
</declare-styleable>
</resources>
How to use:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:foo="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.mypackage.TextViewPlus
android:id="@+id/textViewPlus1"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:text="@string/showingOffTheNewTypeface"
foo:customFont="my_font_name_regular.otf">
</com.mypackage.TextViewPlus>
</LinearLayout>
Un type MIME est un identifiant de format de données sur internet comprenant au moins deux parties. Les identifiants étaient à l'origine définis dans la RFC 2046 pour leur utilisation dans les courriels à travers du SMTP mais ils ont été étendus à d'autres protocoles comme le HTTP ou le SIP. Pricipaux types :
Application (pluri-usages) :
application/javascript
application/octet-stream
application/ogg
application/pdf
application/xhtml+xml
application/x-shockwave-flash
application/xml
application/zip
Audio :
audio/mpeg (inclus MP3)
audio/x-ms-wma
audio/vnd.rn-realaudio
audio/x-wav
Image :
image/gif
image/jpeg
image/png
image/tiff
image/vnd.microsoft.icon
image/svg+xml
Multipartie (objets composés de plusieurs parties) :
multipart/mixed (courriel)
multipart/alternative (courriel)
multipart/related (courriel utilisé par MHTML).
Texte (lisible ou code source) :
text/css (feuilles de style en cascade)
text/csv (comma-separated values)
text/html
text/javascript (obsolète, utiliser application/javascript)
text/plain (données textuelles)
text/xml
Video :
video/mpeg (MPEG-1, vidéo avec son multiplexé).
video/mp4
video/quicktime
video/x-ms-wmv
video/x-msvideo (vidéo dans un conteneur AVI)
video/x-flv (Flash Video (FLV) par Adobe Systems)
Un URI (uniform resource identifier) est un identifiant unique qui permet d'identifier une ressource physique ou abstraite sur un réseau.
Un URL (uniform resource Locator) est un URI qui fournit un moyen d'accéder à la ressource (http://www.orange.fr est un URI qui identifie la page d'accueil d'Orange encodée en htlm qui peut être obtenue via le protocole http.)
Un URN (Uniform resource Name) est un URI qui identifie une ressource par son nom dans un espace de noms (ex : urn:isbn:0-395-36341-1 identifie un livre à partir de son numéro dans l'International Stantdad Book Number).
La classe java.net.URI permet de composer les URIs. à partir de ses parties ou de les décomposer. Un URI débute toujours par un nom appelé schéma (scheme), comme http, mailto, tel, market ... Un URI est généralement de la forme scheme://hote:port/chemin.
La classe android.net.Uri offre une immuable référence sur un URI.
Exemple de création :
Uri uri = Uri.parse("http://www.google.fr");
Uri uri = Uri.parse("tel:0612345678");
Uri uri = Uri.parse("market://search?q=pub:\"Nicolas et Emmanuel\"");
Uri uri = Uri.fromFile(new File("foo.txt"));
Création à partir des parties :
Uri fromParts(String scheme, String ssp, String fragment);
Méthodes d'extraction :
abstract String getAuthority();
boolean getBooleanQueryParameter(String key, boolean defaultValue);
abstract String getEncodedAuthority();
abstract String getEncodedFragment(); //Gets the encoded fragment part of this URI, everything after the '#'.
abstract String getEncodedPath();
abstract String getEncodedQuery();
abstract String getEncodedSchemeSpecificPart();
abstract String getEncodedUserInfo();
abstract String getFragment(); //Gets the decoded fragment part of this URI, everything after the '#'.
abstract String getHost();
abstract String getLastPathSegment();
abstract String getPath();
abstract List<String> getPathSegments();
Gets the decoded path segments.
abstract int getPort();
abstract String getQuery()
String getQueryParameter(String key);
Set<String> getQueryParameterNames();
List<String> getQueryParameters(String key);
abstract String getScheme();
abstract String getSchemeSpecificPart();
abstract String getUserInfo();
Pour afficher pendant un court instant un information à l'usager on peut utiliser une instance de Toast qui est un widget utilitaire (dérivé de la classes View qui sera présentée plus loin) destiné à cet effet. A titre d'exemple :
Toast.makeText(context, "Hello !", Toast.LENGTH_SHORT).show();
permet d'afficher pendant un court instant le texte Hello ! dans une fenêtre pop-up.
Pour l'afficher un peu plus longtemps mettre comme troisième paramètre Toast.LENGTH_LONG.
Le premier paramètre est généralement le "this" de l'Activity.
La méthode Toast.makeText(...) renvoie un static Toast que l'on affiche par sa méthode show().
Pour afficher une info qui perdure jusqu'à un clic de l'usager on peut utiliser une instance d'AlerDialog.Builder, comme suit :
AlertDialog.Builder adb = new AlertDialog.Builder(this);
adb.setMessage("Bonjour Michel !").setNeutralButton("VU", null).show();
Le texte "Bonjour Michel !" sera affiché dans une boite de dialogue jusqu'au clic sur le bouton marqué
Le deuxième argument (ici null) de setNeutralButton est le nom de la fonction de rappel qui est exécutée suite au clic sur le bouton marqué "VU". Nous donnons ci-après un exemple avec plusieurs boutons activant des callbacks appropriées.
AlertDialog.Builder adb = new AlertDialog.Builder(this);
adb.setMessage("Voulez vous voter pour moi ?");
adb.setPositiveButton("OUI", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(context, "Merci !", Toast.LENGTH_SHORT).show(); }});
adb.setNeutralButton("ABSTENTION", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(context, "Il faut choisir !", Toast.LENGTH_SHORT).show(); }});
adb.setNegativeButton("AUCUN", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(context, "Désolé !", Toast.LENGTH_SHORT).show(); }});
adb.show();
Ici on utilise des callbacks anonymes de type DialogInterface.OnClickListener dont le code est fourni au niveau du paramètre formel. Ces callbacks se contentent d'afficher un Toast.
Pour des boites de dialogue plus complexes, on étend la classe DialogFragment comme suit :
public class MonDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("COUCOU");
builder.setMessage("Voulez-vous choisir ?");
builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// User clicked OK button
}
});
builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// User cancelled the dialog
}
});
return builder.create();
}
}
où on peut aussi ajouter un 3ème bouton neutre par builder.setNeutralButton.
Et on l'active depuis l'activité principale comme ceci :
MonDialogFragment mf = new MonDialogFragment();
mf.show(getSupportFragmentManager(), "myFragmentTag01");
Mais ce fragment ne communique pas de résultat à l'activité appelante !!!
Pour que l'activité appelante reçoive les résultats acquis par le fragment on utilise un mécanisme un peu complexe de listener : Le fragment déclare une interface listener avec des méthodes qui correspondent aux clics sur les 3 boutons que devra surcharger l'activité appelante pour traiter ces clics et qui sont appelées dans les onClickXXX(). Pour pouvoir appeler ces méthodes du listener, il doit être instancié dans le fragment, ce qui est fait dans la méthode onAttach() du fragment, tout simplement en l'affectant au contexte de l'activité appelante qui implémente ce listener.
Exemple coté DialogFragment :
public class MonDialogFragment extends DialogFragment {
/*
L'activité qui crée une instance de ce fragment de boîte de dialogue doit implémenter
cette interface pour recevoir les rappels d'événements. Chaque méthode transmet
le DialogFragment au cas où l'hôte aurait besoin de l'interroger.
*/
public interface MonDialogListener {
public void onDialogPositiveClick(DialogFragment dialog);
public void onDialogNegativeClick(DialogFragment dialog);
public void onDialogNeutralClick(DialogFragment dialog);
}
MonDialogListener listener;
// On surcharger la méthode Fragment.onAttach() pour instancier MonDialogListener
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {listener = (MonDialogListener) context;} catch (ClassCastException e)
{ throw new ClassCastException(context.toString() + " doit implementer MonDialogListener");}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("COUCOU");
builder.setMessage("Etes-vous d'accord ?");
builder.setPositiveButton("Oui", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.onDialogPositiveClick(MonDialogFragment.this);
}
});
builder.setNegativeButton("Non", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.onDialogNegativeClick(MonDialogFragment.this);
}
});
builder.setNeutralButton("Peut-être", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.onDialogNeutralClick(MonDialogFragment.this);
}
});
return builder.create();
}
}
Et coté Activité, on aura :
public class MainActivity extends AppCompatActivity implements MonDialogFragment.MonDialogListener
{
public TextView tvStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvStatus = (TextView) findViewById(R.id.tvStatus);
}
public void onClicBouton(View v)
{
MonDialogFragment mf = new MonDialogFragment();
mf.show(getSupportFragmentManager(), "myFragmentTag01");
}
// Les méthodes de l'interface MonDialogFragment.MonDialogListener
@Override
public void onDialogPositiveClick(DialogFragment dialog) {
tvStatus.setText("Vous êtes d'accord !");
}
@Override
public void onDialogNegativeClick(DialogFragment dialog) {
tvStatus.setText("Vous n'êtes d'accord !");
}
@Override
public void onDialogNeutralClick(DialogFragment dialog) {
tvStatus.setText("Vous ne savez pas !");
}
}
Comme tout Fragment (cf plus loin) le constructeur d'un DialogFragment ne prend pas d'arguments. Pour lui en passer on peut faire comme ceci :
// Création de l'instance
MonDialogFragment mf = new MonDialogFragment();
// On lui associe les arguments, par ex, un tableau de strings
Bundle args = new Bundle();
args.putStringArray("listDev", listDev);
mf.setArguments(args);
// On l'affiche
mf.show(getSupportFragmentManager(), "myFragmentTag01");
Et dès le début du onCreateDialog du DialogFragment on récupère les arguments :
public Dialog onCreateDialog(Bundle savedInstanceState) {
String[] elems = getArguments().getStringArray("listDev");
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
....
On peut personnaliser le dialogue par le biais d'un layout décrit par fichier xml et inséré par un builder.setView(inflater.inflate(R.layout.dialog_xyz, null))
Plus simplement, pour une liste unique, on peut l'insérer dérirectement par un :
builder.setItems(telphons, new DialogInterface.OnClickListener() .... comme décrit ci-après dans cet exemple avec l'interface de communication avec l'activité appelante :
Coté Fragment :
public class MonDialogFragment extends DialogFragment
{
String[] telphons = new String[] {"Bill Gates", "Niels Bohr", "Alex Moine"};
public interface MonDialogListener {
public void onChoix(DialogFragment dialog, String choix);
}
MonDialogListener listener;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {listener = (MonDialogListener) context;} catch (ClassCastException e)
{ throw new ClassCastException(context.toString() + " doit implementer MonDialogListener");}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Choisir un personnage");
builder.setItems(telphons, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int wich) {
listener.onChoix(MonDialogFragment.this, telphons[wich]);
}
});
return builder.create();
}
}
Coté Activité :
public class MainActivity extends AppCompatActivity implements MonDialogFragment.MonDialogListener
{
public TextView tvStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvStatus = (TextView) findViewById(R.id.tvStatus);
}
public void onClicBouton(View v)
{
MonDialogFragment mf = new MonDialogFragment();
mf.show(getSupportFragmentManager(), "myFragmentTag01");
}
@Override
public void onChoix(DialogFragment dialog, String choix) {
tvStatus.setText("Sélection de :" + choix);
}
}
La classe Log peut être utilisée pour écrire dans le journal système avec différents niveaux d'importance des messages :
Importance |
Méthode |
ERROR |
Log.e(String tag, String message) |
WARM |
Log.w(String tag, String message) |
INFO |
Log.i(String tag, String message) |
DEBUG |
Log.d(String tag, String message) |
VERBOSE |
Log.v(String tag, String message) |
Le tag est généralement le nom de l'activité, qui peut être obtenu par getClass().getName(), ou déclaré par :
private static final String tag = "MyActivity";
Une Activity correspond à un tache-écran proposé à l'utilisateur. Elle a 3 états principaux qui sont :
- active (active ou running): elle est visible et détient le focus utilisateur. C'est l'appel des méthodes onCreate(Bundle b) puis onStart() puis onResume() qui la rend active. Si une autre application lui prend le focus et la masque partiellement, la méthode onPause() est appelée et la fait passer à l'état paused.
- suspendue (paused) : elle ne detient plus le focus. Si la méthode onResume() est appelée par le système elle est réactivée. Si c'est la méthode onStop() elle est complètement arrêtée et cachée, mais ses variables existent toujours.
- arrêtée (stopped) : l'activité est arrêtée et invisible. C'est la méthode onStop() qui la conduit à cet état. Depuis cet état la méthode onDestroy() la détruit complètement, alors que onRestart() suivi de onStart() puis onResume() permet de la ré-activer.
A l'exception d'onCreate(Bundle b) que le programmeur met dans le code java de l'activity, on n'a normalement pas à appeler les méthodes onStart(), onResume(), onPause(), onRestart(), onStop() ou onDestroy(). C'est le système qui gère leur enchaînement et s'en charge. Mais il faut connaître cet enchaînement si on désire surcharger une de ces méthodes (de même qu'on surcharge onCreate) pour effectuer certaines opérations pendant son déroulement. Le code correspondant est toujours de la forme :
@Override
public void onXXXX() {
super onXXXX();
// Notre code spécifique
}
Les appels effectués par le système se font systématiquement successivement dans l'ordre suivant :
onCreate(Bundle b) -> onStart() -> onResume() et l'activité est en cours au premier plan,
puis ils se font dans l'ordre suivant onPause() -> onStop() et onDestroy().
•Après onPause() l'activité n'est plus au 1er plan. onResume() est appelé pour l'y faire revenir.
•Après onStop() l'activité n'est plus visible. onStart() puis onResume() sont appelés pour la rendre visible à nouveau.
•Après onDestroy() l'activité est détruite.
On peut appeler la méthode finish() qui permet de terminer une Activité. Si la méthode finish() est appelée au niveau d' onCreate(), le système enchaîne directement par onDestroy(), sans passer par onPause() et onStop().
Une activité est toujours démarrée par la méthode onCreate qui reçoit en argument un Bundle b qui est null au premier démarrage de l'activité.
Un Bundle est une structure dans laquelle on peut sauvegarder des valeurs de type simple (int, String, etc...), puis on peut sauvegarder ce Bundle avant la destruction de l'application (qui peut intervenir par exemple quand l'utilisateur change l'orientation de l'écran) et lorsque notre appli est ré-activée, la méthode onCreate est de nouveau exécutée avec ce bundle ce qui permet de récupérer les valeurs mémorisées.
La sauvegarde du Bundle est effectuée en surchargeant la méthode onSaveInstanceState(Bundle b) qui est appelée parfois avant onPause(), mais plutot après et toujours avant onStop().
On récupère les données stockées dans le Bundle, soit dans onCreate(Bundle b) qui reçoit le Bundle, soit en surchargeant la méthode onRestoreInstanceState(Bundle b) qui est appelée après onStart() et avant onResume(), c'est-à-dire juste après que l'application redevienne visible, mais avant que l'usager ait interagit avec elle.
La version native d'onSaveInstanceState(Bundle b) sauvegarde l'état des objets View à qui le programmeur a attribué une ID avec l'attribut android:id. Lorsqu'on la surcharge il faut donc toujours appeler super en premier. La méthode native d'onRestoreInstanceState(Bundle b) restaure les paramètres sauvegardés des objets View, c'est pour cela qu'il faut également appeler super si on la surcharge.
Remarques :
•La méthode onSaveInstanceState(Bundle b) n'est pas systématiquement appelée, en particulier elle n'est pas appelée lorsque l'usager quitte définitivement l'appli en appuyant sur le bouton Retour.
•La méthode onRestoreInstanceState(Bundle b) n'est pas systématiquement appelée après onStart(). Il semble qu'elle ne le soit que si la tâche ait été arrêtée, 'c'est-à-dire que si onDestroy() ait été appelé.
Si l'utilisateur désire sauvegarder des données persistantes (dans un fichier ou autre) pour les récupérer dans une autre session, le meilleur endroit est dans la surcharge de la méthode onPause().
Résumé :
•Dans onCreate, on instancie les objets (pour qu'ils soient instanciés une seule fois dans le cycle de vie) ;
•Dans onStart, on lance les traitements ;
•Dans onResume, on s'abonne et on remet le contexte utilisateur ;
•Dans onPause, on se désabonne et on enregistre le contexte utilisateur ;
•Dans onStop on arrête les traitements et on désalloue les objets ;
•Dans onDestroy on ne fait rien de spécial (n'est pas appelé systématiquement), si ce n'est le dé-enregistrement des éléments enregistrés (registerXxxx).
La figure ci-après résume les changements d'état. Seule la programmation de onCreate() est obligatoire. La méthode Finish() peut être appelée de n'importe où pour terminer l'application. Elle renvoie directement sur onDestroy().
Toutes ces méthodes onXXXX() sont directement appelées par le système. Le schéma montre seulement dans quel ordre le système les appelle. On peut en surcharger certaines pour placer un traitement au moment opportun, en particulier les méthodes onSaveInstance() et onRestoreInstance() si on veut garantir une continuité dans l'exécution de certaines tâches qui sont par exemple arrêtées lors d'un changement d'orientation de l'appareil, avec perte du contenu des variables locales. Dans ce cas des traitements effectués sur ces variables, après leur initialisation, ou après leur restauration pourra être effectué dans onResume().
Sauté au 1er passage
Lorqu'on change l'orientation de l'appareil, si on n'a pas prévu par une programmation explicite la gestion de ce changement d'orientation (voir chapitre 22), le système arrête la tâche, puis la redémarre par la séquence onStop, onCreate, onStart :
•Les variables non-statiques sont re-créées, ce qui fait que leurs valeurs sont perdues.
•Les variables statiques sont préservées, même parfois après fermeture de l'application par le bouton prévu à cet effet. Si Android n'a pas eu besoin de la mémoire occupée par l'appli, on retrouve ces valeurs au démarrage suivant.
•Par contre, si on n'y prend garde, les variables statiques risquent d'être perdues, par un simple changement d'orientation, si on effectue leurs initialisations dans onCreate ou onStart car ces initialisations sont effectuées à nouveau écrasant les anciennes valeurs qui seront perdues.
Les initialisation simples des variables statiques effectués hors méthodes ne posent pas de problème, par contre, pour les initialisations plus complexes, pour ne les faire qu'une seule fois dans onCreate(Bundle b), on peut tester le Bundle b. S'il est null, c'est le premier passage, et on effectue ces initialisations, sinon, c'est inutile car elles ont déjà été faites.
Remarque : Une activité considérée par le programmeur comme définitivement détruite, même après une fermeture au moyen du bouton utilisé habituellement dans ce but, peut très bien être mise en sommeil par android et réveillée au prochain lancement par l'usager, conservant ainsi les dernières valeurs des variables statiques simples, alors que le programmeur peut penser lancer une nouvelle instance qui démarre avec des variables initiales neuves. Pour être certain de démarrer avec des variables statiques neuves, il faut effectuer l'initialisation de ces variables dans la méthode onCreate(Bundle b), au premier passage détecté par le test à null du Bundle b.
Lors de l'arrêt d'une activité, si la vue affichée présentait des champs d'édition remplis le contenu des champs possédant une id est automatiquement sauvegardé. Par contre ceux qui n'en possèdent pas sont perdus et comme ils n'ont pas d'id, il n'y a pas de moyen de les sauvegarder par programmation s'il n'est connu d'aucune variable.
Les variables globales peuvent être sauvegardées en les déclarant static. Mais dans le cas où on a différentes instances d'une même classe avec des variables de classe que l'on souhaite différencier, on ne peut utiliser des variables de type static. On est alors amené à utiliser les méthodes onSaveInstance() et onRestoreInstance().
Dans le cas où on n'a à sauvegarder que des objets de types élémentaires (boolean, int, long, float et String) la méthode la plus simple consiste à surcharger onSaveInstateState(Bundle outState) qui est appelée par le système avant onStop() avant de suspendre l'activité. Par exemple
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putString("prenom",edit.getText().toString());
}
les objets élémentaires sauvegardés peuvent être récupérés à la fin de la méthode onCreate(Bundle savedState). Par exemple par :
if(savedState != null)
edit.setText(savedState.getString("prenom"));
mais aussi (préférablement) en surchargeant la méthode onRestoreInstanceState qui est appelée après onStart (sauf la première fois) :
@Override
protected void onRestoreInstanceState(Bundle inState)
{
super.onRestoreInstanceState(inState);
if( inState != null)
edit.setText(inState .getString("prenom"));
}
S'il y a des objets complexes à récupérer, on ne peut pas les mettre dans l'état. On peut les regrouper dans une instance memTask d'une classe MemTask, qui sera automatiquement sauvegardée par la surcharge de la méthode onRetainNonConfigurationInstance qui renvoie une instance sauvegardée d'une classe quelconque :
@Override
publicObject onRetainNonConfigurationInstance()
{
return(memTask);
}
On peut récupérer cet objet dans onCreate(Bundle savedInstanceState) par :
Object o = getLastNonConfigurationInstance();
if(o! = null){ memTask = (MemTask)o; else memTask = null;
Autre scénario de récupération avec initialisation d'une tâche au démarrage initial et récupération avec mise à jour si redémarrage :
// On récupère la tâche
memTask = (MemTask) getLastNonConfigurationInstance().
if(memTask == null)
{memTask = new MemTask(...); ..} // Début => création
else
{ /* mise à jour activité */ ...} // Redémarrage => mise à jour
Remarque : Il semble que la classe MemTask doive être déclarée static, sinon getLastNonConfigurationInstance() renvoie null dans certains cas, suivant la configuration du gestionnaire de tâche d'Android.
Dans Android, tous les composants graphiques (bouton, animation, champ texte, etc) sont basés sur la classe View (du paquet android.view.View).
java |
xml |
|
android:layout_width="fill_parent",… |
|
android:layout_higth="wrap_content",… |
|
android:background="#000000" |
|
android:visibility="1" |
|
android:contenDescription="blabla" |
|
android:nextFocusDown= |
|
android:nextFocusLeft= |
|
android:nextFocusRight= |
|
android:nextFocusUp= |
|
android:padding= |
|
android:onClick="myCallback" |
|
android:gravity="top|right" |
|
. . . |
View getParent() |
|
View findViewById(id) |
|
View getRootView() |
|
boolean isFocused() |
|
requestFocus() |
|
boolean isEnabled() |
|
setEnabled() |
|
setOnClickListener(OnClickListener i) |
|
OnClick(View v) |
|
Les attributs suivants permettent de préciser la forme de l'objet view :
-android:layout_width ou android:layout_height, valeurs :
•match_parent (ou fill_parent avant l'API 8) pour occuper tout l'espace disponible.
•wrap_content pour n'occuper que la place nécessaire au contenu. Si cette place est supérieure à la taille de la zone parent, le contenu sera coupé (et accessible en faisant glisser le contenu).
•une valeur précise de la taille occupée avec son unite de mesure (px pour pixel physique, dp ou dip pour 1/160 de la largeur de l'écran, sp voisin de dip mais à privilégier pour les polices, mm, in pour inche, pt pour 1/72 d'inche…) ex : 125dip.
-android:layout_gravity pour préciser où placer ce widget dans son parent, à l'aide des valeurs bottom, top, center_vertical,left, right, center_horizontal et center. L'utilisation de cet attribut n'a de sens que si le widget n'occupe pas toute la place disponible et donc qu'il n'utilise pas le paramètre fill_parent ou match_parent pour la dimension en question.
-android:gravity pour préciser le positionnement des éléments dans ce View. Par défaut c'est à partir du haut-gauche.On peut modifier en précisant avec les valeurs bottom, , top, center_vertical, left, right, center_horizontal ou center tout court. En java on peut utiliser la méthode setGravity(int). L'indication de gravité ne peut être prise en compte que dans la mesure où le contenu du View n'occupe que peu de place dans le View lui-même, ce qui suppose qu'on n'utilise pas wrap_content pour la dimension en question, sinon le View conteneur est serré autour de son contenu..
Par défaut, dans un widget TextView ou EditText, le texte ou les nombres sont centrés verticalement et cadrés à gauche. Exemple, pour cadrer au milieu à droite :
android:gravity="right|center_vertical".
Marges et padding :
-android:layout_margin="10px" met 10 pixels de marge tout autour à l'extérieur du View.
-android:layout_marginTop="10px" met 10 px de marge au-dessus du View.
-androi:padding="10px" met 10 pixels de marge tout autour à l'intérieur du View.
-android:paddingTop="10px" met 10 px de marge à l'intérieur en haut du View.
En résumé :
•pour préciser la position d'un widget dans son parent (s'il y a de la marge à l'intérieur de son parent) on utilise android:layout_gravity ou android:layout_marginXYZ
•pour positionner la position de son contenu (par où et comment on le remplit en supposant le contenu plus petit que la place offerte) on utilise android:gravity ou android:paddingXYZ
Si un objet View view a été rendu invisible, il est de nouveau rendu visible par la méthode view.setVisibility(View.VISIBLE) et à nouveau rendu invisible par la méthode
view.setVisibility(View.INVISIBLE), mais sa place vierge est conservée sur l'écran. Pour le rendre invisible et libérer sa place, il faut utiliser view.setVisibility(View.GONE).
C'est la manière la plus simple d'associer une callback à un évènement de type click, long click, touch, etc.
Par exemple pour associer une callback à un click sur un objet, on mettra dans les attributs xml de cet objet l'attribut suivant :
android:onClick="onMyViewClicked"
Dans le code java, on précisera la callback comme suit :
public void onMyViewClicked(View view)
{ /* Ici code a exécuter quand on clique sur la vue de l'objet */}
Si la même callback est associée à plusieurs objet, on dispose de l'argument View view qui indique quel objet view a été cliqué.
Cette méthode qui utilise l'attribut android:onClick est la plus simple pour associer une callback à un clic sur un objet view.
Si la callback n'est pas précisée dans le fichier xml, l'association d'une callback à un objet héritant de View peut se faire en utilisant un listener grâce à une interface spécifique : Un évènement nommé onXyz (ou Xyz est à remplacer par Click, LongClick, Touch, Drag, FocusChange, ...), etc... est traité par la callback onXyz(..) qui est généralement l'unique méthode d'une interface onXYZListener.
Nous donnons ci-après 4 manières d'implanter la callback onClick(..), en sus de la méthode simple présentée au paragraphe précédent. La plus simple est à mon avis la 4ème présentée au paragraphe 15.1.4.4
La classe parent est déclarée avec l'attribut implements OnXyzListener. La callback est associée aux objets objetView pour lesquels on met les instructions : objetView.setOnClickListener(this), où le premier argument est this car c'est la classe elle même qui implémente OnXyzListener.
Il suffit alors de rajouter à la classe la méthode onXyz. Par exemple, dans le cas où l'Activity implements OnXyzListener, elle devra fournir la méthode suivante :
@Override
public void onClick(View v)
{
Toast t = Toast.makeText(this, "Buen Dia", Toast.LENGTH_SHORT);
t.show();
}
Lorsque l'Activity posséde plusieurs objetView cliquables associés à OnXyzListener, comme elle ne fournit qu'une unique callback OnClick(View v) qui sert pour tous les clics sur tous ses objets objetView, on se servira de l'argument v pour sélectionner le traitement associé aux différents objectView (à l'aide d'un switch par exemple).
Par exemple, si on veut qu'elle mémorise des variables de contexte, qui lui seront passées par le constructeur.
static class MyListen implements OnClickListener
{
Context ct;
MyListen(Context c) {ct = c;}
@Override
public void onClick(View v)
{
Toast.makeText(ct, "Bonjour", Toast.LENGTH_SHORT).show();
}
}
et on l'associe à un objetView qui a besoin de cette callback dans la classe ou cet objet apparaît par objetView.setOnClickListener(new MyListen(context));
Dans la liaison du listener à l'objectView, on définit le listener au moyen d'une classe anonyme. Exemple :
objetView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{ Toast.makeText(AhelloActivity.this, "Hello", Toast.LENGTH_SHORT).show();}
});
Dans ce cas la méthode est forcément spécifique de cet objet.
On déclare un objet intermédiaire réalisant une implémentation anonyme de la classe. Par exemple l'objet mylisten ci-après :
private OnClickListener mylisten = new OnClickListener()
{
@Override
public void onClick(View v)
{Toast.makeText(AhelloActivity.this,"Salut", Toast.LENGTH_SHORT).show(); }
};
qu'on associe comme précédemment par :
objetView.setOnClickListener(mylisten);
Cet object mylisten peut être utilisé comme callback par d'autres objectView.
La plupart des callbacks onXyz(..) ne renvoient rient mais certaines renvoient un booléen, comme :
boolean onTouch(View v, MotionEvent event);
qui l'on met à true si l'on a consommé l'événement et à false si on ne l'a pas consommé.
Les conteneurs (les layouts) sont un ensemble de classes dérivées de la classes ViewGroup, elle même dérivée de la classe View. Ils servent à agencer les widgets à l'intérieur d'un gabarit, widgets qui peuvent être d'autres conteneurs.
Le conteneur le plus basique est le FrameLayout qui positionne tous ses widgets enfants en haut à gauche, les uns sur les autres. En général on ne l'utilise que pour charger un widget unique. On peut également y mettre un stock de View les uns sur les autres (des photos par exemple) et avec setVisibility(View.VISIBLE/View.INVISIBLE) n'en laisser voir qu'une à la fois.
Nous présentons ci-après :
•LinearLayout,
•RelativeLayout,
•TableRow et TableLayout,
•ScrollView et HorizontalScrollView,
•RadioGroup,
•TabHost.
Pour effacer le contenu d'un conteneur layout afin de le reconstruire dynamiquement on dispose de la fonction :
layout.removeAllViewsInLayout();
Par ailleurs un conteneur (FrameLayout, LinearLayout, RelativeLayout, TableLayout, ... et leurs contenus) déclaré dans un fichier xml, par exemple fich.xml peut être inclus dans un autre fichier xml par la balise include, comme suit.
<include android:id="@+id/include01" layout="@layout/fich"/>
L'identificateur include01 peut être utilisé pour accéder aux champs internes du layout accédé, exemple :
LinearLayout lh = (LinearLayout) findViewById(R.id.include01) ;
TextView th = (TextView) lh.findViewById(R.id.TextHP);
th.setText("En haut");
Ici, le fichier inclut est un LinearLayout et TextHP est l'identificateur d'un widget TextView inclus dans ce LinearLayout.
Dans la balise include, il semblerait qu'on puisse redéclarer les attributs du Layout de plus haut niveau pour les modifier, mais mes essais pour ce faire ont échoués.
Un LinearLayout est le conteneur de base qui étend ViewGroup < View. Il permet d'aligner ses enfants soit horizontalement, soit verticalement. Ses principales propriétés sont :
L'orientation : android:orientation
-xml : android:orientation="horizontal" ou "vertical",
-java : setOrientation(HORIZONTAL); ou VERTICAL
Mode de remplissage : android:gravity
Comme pour un objet View quelconque, la valeur est une gravité attribuée aux widgets internes qui indique par où se fait le remplissage et/ou se place le contenu s'il n'occupe pas toute la place fournie par le layout.
Pour partager l'espace disponible à l'intérieur entre plusieurs widgets ces widgets utilisent l'attribut
android:layout_weight qui est un Poids attribués aux widgets internes au conteneur. Si ces widgets partagent un espace vertical (cas où le conteneur a l'attribut android:orientation="vertical"), alors leur attribut android:layout_height doit être ="0dp", et si ces widgets partagent un espace horizontal (cas où le conteneur a l'attribut android:orientation="horizontal"), alors leur attribut android:layout_width doit être ="0dp". Ensuite le partage suit les règles suivantes :
•Poids relatifs entre eux : En donnant à android:layout_weight les valeurs ="1", ou "2", ou "3", …. le widget qui a la valeur 3 occupera 3 fois plus de place que celui qui a la valeur 1 (ces valeurs peuvent être décimales et < à 1).
•Poids relatifs en pourcentage : En donnant à android:layout_weight="x" ou x compris 0 et 100 est le pourcentage de la place occupée par ce widget. La somme des x doit être égale à 100.
Positionnement : L'utilisation d' android:layout_gravity pour préciser ou placer ce layout dans son parent à l'aide des valeurs bottom, top, center_vertical,left, right, center_horizontal et center ne semble pas bien efficace.
Dans le fichier layout xml, les widgets internes sont simplement déclarés les uns à la suite des autres.
Dans le code java les widgets internes sont ajoutés par la méthode LinearLayout.addView(View).
L'espacement autour du contenu d'un widget peut être spécifié en java par la méthode setPadding(int left, int top, int right, int bottom), uniquement en pixels.
Les attributs suivants (valeur unique "true") permettent à un conteneur ou à un widget interne à un RelativeLayout d'indiquer sa position en relatif par rapport à son parent et/ou par rapport aux autres widgets.
Position / au conteneur :
-android:layout_alignParentTop, android:layout_alignParentBottom, android:layout_alignParentLeft, android:layout_alignParentRight précise le côté du widget qui est aligné avec le même côté de son conteneur.
-android:layout_centerHorizontal, android:layout_centerVertical, android:layout_centerInParent précise un centrage par rapport au conteneur.
Positions relatives internes :
La valeur des attributs décrits ci-après est l'identificateur d'un autre widget par rapport auquel le widget courant se situe. L'identificateur de cet autre widget est sous la forme "@id/widgetId", ce qui suppose que dans le corps de la définitino de cet autre widget cet identificateur est déclaré par android:id="@id+/widgetId".
-android:layout_above, andoid:layout_below, android:layout_toLeftOf, android:layout_toRightOf indique la position du widget courant par rapport au widget dont l'identificateur est précisé en argument.
-android:layout_alignTop, android:layout_alignBottom, android:layout_alignLeft, android:layout_alignRight indique un côté que le widget courant et l'autre widget ont en commum.
-android:layout_alignBaseLine indique que les lignes de base du texte inclus dans les deux widgets doivent être alignées.
Remarque : depuis la version 1.6 d'android on peut faire référence à "@id/widgetId" avant qu'il ait été déclaré par android:id="@id+/widgetId".
Recouvrement : Dans une RelativeLayout, si des widgets occupent la même zone écran, les derniers widgets déclarés sont au-dessus des widgets déclarés avant.
Un TableRow est un conteneur ligne qui sert de ligne pour les conteneurs de type matrice TableLayout. Les widgets qui sont disposés dans un TableRow sont successivement rangés de la gauche vers la droite, en définissant à chaque fois une colonne dont la numérotation commence à 0. Toutefois un widget peut occuper la largeur de plusieurs colonnes en précisant le nombre de colonnes occupées avec l'attribut android:layout_span, et on peut laisser des colonnes vides en précisant pour un widget un numéro de colonne supérieur au futur numéro disponible avec android:layout_column (qui démarre à 0).
Si on met un widget quelconque directement dans un TableLayout sans être dans un TableRow ce widget s'étend sur toute la largeur disponible. Ainsi pour créer une ligne de séparation bleu on peut insérer entre deux TableRow, le widget View suivant :
<View android:layout_height="2dip" android:background="#0000FF" />
Attribut des widgets internes à un TableRow :
-android:layout_span="n" où n est le nombre de colonnes occupées par le widget.
-android:layout_column="i" où i est le numéro de la 1ère colonne occupée par le widget (débute à 0)
Atrributs de TableLayout :
-android:stretchColumns="i,j,k" indique que l'on veut que les colonnes numéro i, j et k occupent un maximum de largeur (mode fill_parent)
-android:shrinkColums="i,j,k" indique que l'on désire que les colonnes i,j et k occupent le minimum de largeur, quitte faire des retours lignes dans les widgets (mode wrap_content)
-android:collapsesColums="i,j,k" indique que les colonnes i, j et k seront cachées.
Modifications par programmation java :
-setColumnCollapsed(int k, boolean b) : cache/montre la colonne k si b=true/false.
-setColumnStretchable(int k, boolean b)
-setColumnShrinkable(int k, boolean b)
Le ScrollView et l'HorizontalScrollView sont des conteneurs qui offrent la capacité de défilement vertical et horizontal. Les deux ne sont pas utilisables simultanément. Ils ne sont pas utilisables si le widget contenu a déjà la capacité de défiler comme un ListView par exemple.
Ne pouvant contenir qu'un seul widget au premier niveau, ce sera généralement un autre conteneur (LinarLayout ou autre)
Utilisé pour regrouper des RadioButton(s) mutuellement exclusifs (voir paragraphe 15.4.5).
Dans le conteneur TabHost on met un LinearLayout et dans ce LinearLayout on met les deux conteneurs suivants :
•un TabWidget ayant obligatoirement les attributs suivants : android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
•un Framelayout ayant obligatoirement les attributs suivants : android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
Ce FrameLayout va contenir tous les views des onglets, comme si on voulait qu'ils se superposent. C'est dans le code java qu'on sélectionne celui qui est visible.
Exemple de code java de mise en oeuvre :
TabHost tabHost = (TabHost)findViewById(R.id.TabHost01); tabHost.setup();
tabHost.addTab(tabHost.newTabSpec("tag1").setIndicator( "Feuille1").setContent(R.id.Onglet01)); // Ajout du 1er onglet
tabHost.addTab(tabHost.newTabSpec("tag2")..... // idem pour les autres onglets
....
Danc cet exemple TabHost01 est l'id que l'on a choisie et attribuée au TabHost dans son fichier xml, tag1 est un tag qui va servir à identifier cet onglet, Feuille1 est le nom qui va apparaitre sur l'onglet (on peut également ajouter en plus un argument icone dans setIndicator.) et Onglet01 est l'id que l'on a attribué au premier contenu.
On peut mettre un écouteur pour exécuter du code lors du changement d'onglet. Exemple :
// Ajout écouteur sur le clic de changement d'onglet
tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener()
{
public void onTabChanged(String tagId)
{Toast.makeText(OngletsActivity.this,"L'onglet "+tagId+" a été cliqué",Toast.LENGTH_SHORT).show();}
});
La méthode setCurrentTab(int k) permet d'activer par programmation le k-ième onglet (à partir de 0).
La methode iv = thelayout.indexOfChild(view) ou il = thelayout.indexOfChild(autrelayout) permett d'obtenir l'indice de la place occupée par view ou autrelayout dans le layout thelayout.
On peut enlever view ou autrelayout par les méthodes thelayout.removeViewAt(iv) ou thelayout.removeViewAt(il).
On peut mettre un autre élément à ces places par les méthodes thelayout.addView(autreElement, ix) ou thelayout.addView(autreElement, il).
Nous avons déjà vu l'ajout simple d'un élément par thelayout.addView(element).
Par ailleurs rappelons qu'un élément peut être rendu visible par view.setVisibility(View.VISIBLE) et rendu invisible par view.setVisibility(View.INVISIBLE), et on libère sa place avec view.setVisibility(View.GONE).
Ce sont des widgets simples qui hérietnt tous de la classe View. Nous présentons ci-après :
•TextView : Champ texte qui étend View,
•Button : Bouton qui étend TextView,
•ImageView : Image qui étend un TextView,
•ImageButton : Image qui étend un Button,
•EditText : Champ de saisie, multiligne par défaut, qui étend TextView,
•CheckBox : Case à cocher qui étend CompounButton, qui étend Button,
•RadioButton : Radios boutons mutuellement exclusifs qui étendent CompounButton,
•DatePicker : Widget affichant un calendrier qui permet d'acquérir une date,
•RatingBar : Widget affichant des astériques pour acquérir une note comprise entre 0 et 5, voir également SeekBar, AbsSeekBar et ProgressBar.
Le TextView est un champ texte qui étend View.
java |
xml |
|
android:typeface="monospace",… |
|
android:textstyle="bold","italic","bold_italic",… |
setTextColor(0xFF0000) |
android:textColor="#FF0000" |
setText(CharSequence s) |
android:text="bonjour" |
setGravity(int) |
android:gravity="right|center_vertical" |
La propriété android:gravity="left|center_vertical" est mise par défaut, cadrant le texte à gauche.
Button : C’est un bouton qui étend TextView
java |
xml |
|
android:onclick="uneMethode" |
La callback public void uneMethode(View v) {…} est à écrire dans la classe de l'activité qui affiche le bouton.
On peut faire de même, plus lourdement, directement dans la classe java, sans utiliser l'attribut android:onClick, mais dans ce cas la classe doit implémenter l'interface View.onClickListener et la callback s'appelle obligatoirement public void onClick(View v) {…}, et de plus le bouton doit déléguer le traitement du click à la classe comme ceci :
Button btn = (Button) this.findViewById(R.id.button1);
btn.setOnClickListener(this);
La méthode setGravity (ou le paramètre android:gravity) permet de positionner le texte à droite, à gauche, au centre, etc...
ImageView : C’est une image qui étend un TextView
ImageButton : C’est une image qui étend un Button
java |
xml |
|
android:src="@drawable/pixfile" |
|
android:adjustViewBounds="true","false" |
Le fichier spécifié par android:src="@drawable/pixfile" doit être placé dans le répertoire
res/drawable. Ce doit être un fichier png ou jpg (dont le nom sera donc, par exemple pixfile.jpg sans majuscules).
Pour insérer l'image dans le code java, au lieu de le faire dans le fichier xml du répertoire res/layout, on fera par exemple :
ImageView pix = new ImageView(this);
pix.setImageResource(R.drawable.pixfile);
((LinearLayout) findViewById(R.id.monll)).addView(pix);
Exemple d'accès à un fichier image dont le filepath est donné par un string (qui ici se trouve dans le fichier res) :
// On recupère le filepath dans le fichier ressource
Resources myres = getResources();
String filepath = myres.getString(R.string.filepath);
// On Transforme le String filepath en URI
File f = new File(filepath);
Uri u = Uri.fromFile(f);
// On charge le fichier dans un ImageView à partir de son URI
ImageView pix = new ImageView(this);
pix.setImageURI(u);
Autre exemple, avec l'ImageView déclaré dans le fichier res/layout/mytv.xml suivant :
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mypix"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
et le code java :
package fr.llibre.Ahello;
import java.io.File;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ImageView;
public class AhelloActivity extends Activity
{
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mytv);
((ImageView) findViewById(R.id.mypix)).setImageURI(Uri.fromFile(new File("/sdcard/DCIM/Camera/IMG_20130302_215204.jpg")));
}
}
ou mieux à la place de setImageURI(...) :
setImageBitmap(BitmapFactory.decodeFile("/sdcard/DCIM/Camera/IMG_20130302_215204.jpg"));
CheckBox : Case à cocher qui étend CompounButton, qui étend Button.
java |
xml |
boolean isChecked() |
|
setChecked(boolean is) |
|
toggle() |
|
setOnCheckedChangeListener(i) |
|
onCheckedChanged(cb, is) |
|
La classe à qui est transmise l'écoute doit implémenter CompoundButton.OnCheckedChangeListener. Elle écoutera par le biais de la méthode :
void onCheckedChanged(CompoundButton cb, boolean ischecked).
Exemple code xml :
<CheckBox
android:id="@+id/CheckBox01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Conditions acceptées ?"
android:layout_gravity="center_vertical"/>
Code java ajouté à l'activité :
// Listener du CheckBox -> affiche un toast
((CheckBox) findViewById(R.id.CheckBox01)).setOnCheckedChangeListener(
new CheckBox.OnCheckedChangeListener(){
@Override
public void onCheckedChanged(CompoundButton bv, boolean ic)
{ afficheToast("Case cochée : " + ((ic ? "Oui" : "Non")));}
});
Pour mettre le texte de l'autre coté dans un CheckBox horizontal, ajouter dans le fichier xml :
android:button="@null"
android:drawableRight="?android:attr/listChoiceIndicatorMultiple"
RadioButton : Radios boutons mutuellement exclusifs qui étendent CompounButton.
java |
xml |
boolean isChecked() |
|
setChecked(boolean is) |
|
toggle() |
|
check(Id) |
|
clearcheck() |
|
Id = getCheckedRadioButtonId() |
|
setOnCheckedChangeListener(i) |
|
onCheckedChanged(cb, is) |
|
Fonctionnement identique aux checkBox, mais un seul bouton par groupe peut avoir la coche. Un radio bouton doit être placé dans un conteneur RadioGroup. (étend LinearLayout < ViewGroup < View).
Exemple 1 :
Code xml :
<RadioGroup
android:id="@+id/RadioGroup01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_horizontal">
<RadioButton
android:id="@+id/RadioButton01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Oui"
android:checked="true"/>
<RadioButton
android:id="@+id/RadioButton02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Non"/>
</RadioGroup>
Code java ajouté à l'activité :
// Listener du RadioGroup -> affiche un toast
((RadioGroup) findViewById(R.id.RadioGroup01)).setOnCheckedChangeListener(
new RadioGroup.OnCheckedChangeListener(){
@Override
public void onCheckedChanged(RadioGroup gr, int id)
{ afficheToast("Réponse : "+((RadioButton)findViewById(id)).getText());}
});
Exemple 2 :
Code xml : Idem exemple 1 plus ligne suivante ajoutée à chaque RadioButton :
android:onClick="onRadioBoutonClicked"
Code java ajouté à l'activité :
public void onRadioBoutonClicked(View view) {
// Is the button now checked?
// boolean checked = ((RadioButton) view).isChecked();
afficheToast("Réponse : "+((RadioButton)view).getText());
}
Remarque : Il n'y a pas de code java correspondant au code xml android:onClick qui permet d'associer directement une callback au click sur un RadioButton, comme ci-dessus. Mais il y a toujours la solution standard pour toute classe héritant de View d'utiliser l'Interface OnClickListener de cette classe (voir page Erreur : source de la référence non trouvée), ce qui sera plus lourd que la méthode de l'exemple 1 donnée ci-dessus.
Widget affichant un calendrier qui permet d'acquérir une date.
Code xml :
<DatePicker
android:id="@+id/DatePicker01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Code java :
((DatePicker) findViewById(R.id.DatePicker01)).
init(2013, 3, 7, new DatePicker.OnDateChangedListener()
{
@Override
public void onDateChanged(DatePicker view, int y, int m, int d)
{ afficheToast("Date : "+d+"/"+(m+1)+"/"+y); }
});
Attention : la valeur du mois renvoyée commence à 0 pour janvier, d'où l'ajout de 1 à sa valeur.
Voir également SeekBar, AbsSeekBar et ProgressBar.
Widget affichant des astériques pour acquérir une note comprise entre 0 et 5.
Code xml :
<RatingBar
android:stepSize="0.2" <!-- 0.5 par défaut -->
android:id="@+id/RatingBar01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Code java :
((RatingBar) findViewById(R.id.RatingBar01)).setOnRatingBarChangeListener(
new RatingBar.OnRatingBarChangeListener(){
@Override
public void onRatingChanged(RatingBar r, float v, boolean fromUser)
{
afficheToast("Note : "+v);
}
});
C’est un champ de saisie, multiligne par défaut, qui étend TextView.
java |
xml |
|
android:autotext="true","false" |
setHint("Tapez votre nom") |
android:hint="Tapez votre nom" |
|
android:capitalize="true","false" |
|
android:digits= |
|
android:singleLine= |
setLines(5) |
android:lines="5" |
|
android:inputType= |
setGravity(int) |
android:gravity="right|center_vertical" |
La propriété android:gravity="left|center_vertical" est mise par défaut, cadrant le texte à gauche.
La propriété autotext est la correction automatique.
La propriété hint représente le texte en grisé qui est écrit dans la zone à éditer lorsque celle-ci est vide, par exemple une question qui sera remplacée par la réponse tapée par l'usager.
Dans le code java on accède au texte tapé par l'usager par la méthode getText() de la classe EditText qui renvoie l'interface Editable, que l'on transformera (par exemple) en String par sa méthode toString(). Exemple :
EditText txt = ((EditText) findViewById(R.id.myEdit));
String nom = txt.getText().toString();
Toast.makeText(AhelloActivity.this, nom, Toast.LENGTH_SHORT).show();
S'il s'agit d'un nombre, on le recupère par exemple par :
entier = Integer.valueOf(txt.getText().toString());
reel = Double.parseDouble(txt.getText().toString());
Les types d'entrée d'un EditText sont précisés par l'attribut androit:inputType dont les valeurs permettent de configurer 6 types principaux et de nombreux sous-types, en combinant leurs valeurs à la valeur principale au moyen du signe | (ou binaire). Le fait d'utiliser cet attribut rend le widget mono-ligne par défaut. Les 6 principaux types sont :
-androit:inputType="text" type texte quelconque, par défaut en l'absence de la balise,
-androit:inputType="number",
-androit:inputType="phone",
-androit:inputType="datetime",
-androit:inputType="date",
-androit:inputType="time"
Exemple avec sous-types pour spécifier des décimaux signés :
-androit:inputType="number|numberDEcimal|numberSigned"
Pour spécifier un EditText multiligne on fera :
-androit:inputType="text|textMultiline"
on peut également ajouter le sous-type textAutoCorrect,..
Dans le cas d'un EditText multiligne, on peut spécifier également :
-android:minLines="n", le nombre minimal de lignes de texte du widget. A l'édition on en ajoute avec des return. On peut en supprimer, mais il en restera au moins n.
-android:layout_weight="top" ou "bottom", suivant que l'on veut remplir avec le texte par le haut ou par le bas.
L'IME :
L'IME (Input Method Editor) est un clavier virtuel qui apparait en bas de l'écran, en l'absence de clavier physique, lorsque l'usager touche un editText ou le rappelle par la touche Menu. Sa configuration s'adapte à l'inputType. Dans ce clavier, la touche bas-gauche affiche Suivant (Next) ou Ok (Done) selon le cas pour permettre de passer au champ suivant ou de quitter. On peut choisir le comportement offert par la touche par l'attribut :
-android:imeOptions=
•"actionDone" pour quitter
•"actionGo"
•"actionNext" pour passer au champ suivant
•"actionNone"
•"cationPrevious" pour passer au champ précédent
•"actionSearch"
•"actionSend" pour envoyer (un mail par exemple).
Dans le fichier AndroidManifest.htlm, on peut mettre un attribut à la balise activity pour avoir un partage intelligent entre l'affichage et le clavier virtuel :
-android:windowSoftInputMode="adjustResize"
Dans la classe java :
-setOnEditorActionListener() utilisée sur l'editText permet de recevoir une notification de l'action demandée afin de la traiter.
-On peut faire disparaitre le clavier associé à un editText edt de la manière suivante :
InputMethodManager mgr =
(InputMethodManager) getSystemService(INPUT_METHODE_SERVICE);
mgr.hideSoftInputFromWindow(edt.getWindowToken(), 0);
et avec InputMethodManager.HIDE_IMPLICIT_ONLY comme 2ème argument à la place de 0 on a un autre comportement (à tester).
Exemple de callback à l'écoute de la touche Suivant/Ok, associé à la propriété android:imeOptions="actionGo" :
// Validation modification, on recalcule
private OnEditorActionListener myEAL = new OnEditorActionListener()
{
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
EditText e = (EditText) v;
switch (e.getId())
{
case R.id.edMontant :
montant = Double.parseDouble(e.getText().toString());
updatePret();
break;
case R.id.edMensualite :
mensualite = Double.parseDouble(e.getText().toString());
updatePret();
break;
case R.id.edNbmois :
nbmois = Double.parseDouble(e.getText().toString());
updatePret();
break;
case R.id.edTaux :
taux = Double.parseDouble(e.getText().toString());
tauxmensuel = Math.pow(1. + taux/100., 1./12.) - 1.;
updatePret();
break;
}
if (actionId == EditorInfo.IME_ACTION_GO) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
return true;
}
return false;
}
};
Pour cette callback qui dessert 4 EditText, marche il faut qu'ils l'activent en appelant :
etMontant.setOnEditorActionListener(myEAL);
etMensualite.setOnEditorActionListener(myEAL);
etNbmois.setOnEditorActionListener(myEAL);
etTaux.setOnEditorActionListener(myEAL);
Exemple de callback associée à la perte de focus d'un ensemble d'EditText :
private OnFocusChangeListener myFCL = new OnFocusChangeListener()
{
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
EditText e = (EditText) v;
switch (e.getId())
{
case R.id.edMontant : montant = Double.parseDouble(e.getText().toString()); break;
case R.id.edMensualite : mensualite = Double.parseDouble(e.getText().toString()); break;
case R.id.edNbmois : nbmois = Double.parseDouble(e.getText().toString()); break;
case R.id.edTaux : taux = Double.parseDouble(e.getText().toString());
tauxmensuel = Math.pow(1. + taux/100., 1./12.) - 1.;
break;
}
}
}
};
Pour cette callback qui dessert 4 EditText, marche il faut qu'ils l'activent en appelant :
etMontant.setOnFocusChangeListener(myFCL);
etMensualite.setOnFocusChangeListener(myFCL);
etNbmois.setOnFocusChangeListener(myFCL);
etTaux.setOnFocusChangeListener(myFCL);
La gestion des listes se divise en deux parties distinctes : les Adapter (en fait les classes qui en dérivent) qui gèrent la distribution des données dans les AdapterView qui, eux vont les afficher et gérér l'interaction avec l'utilisateur.
L'interface Adapter précise les méthodes pour accéder aux données et les présenter à l'AdapterView. On utilise surtout les classes suivantes qui implémente cette l'interface :
ArrayAdapter (données simples de type tableau)
CursorAdapter (données provenant d'une base de données)
SimpleAdapter (données multiples par enregistrement), ...
Pour afficher les données et pouvoir en sélectionner des données, présentées par exemple sous forme de tableaux, de liste ou autre, on utilise des classes dérivées de la classe abstraite AdapterView telles que ListView, Spinner (liste déroulante), gridWiew, ..
-ListView (colonne de cellules affichant des items). Attributs particuliers :
•android:drawSelectorOnTop="false", sinon l'item sélectionné est caché.
•android:choiceMode="multipleChoice", si on veut des sélections multiples.
-Spinner (liste déroulante), attributs particuliers idem ListView.
-GridView attributs particuliers :
•android:verticalSpacing="40dip"
•android:horizontalSpacing="5dip"
•android:numColumns="auto_fit"
•android:columnWidth="100dip"
•android:stretchMode="columnWidth"
•android:gravity="center"
-AutoCompleteTextView (sous-classe d'EditView) attribut particulier :
•android:completionThreshold="2" affiche les propositions ayant les "2" premières lettres identiques.
-Gallery.
La classe ArrayAdapter<T> fera le lien entre un tableau T[] ou une liste ArrayList<T> d'éléments de classe T et l'afficheur (Attention T ne peut être un type simple, nécessité dans ce cas de passer par les classes Integer, Float, etc).
On utilise un contructeur de la forme :
ArrayAdapter<T> aa = new ArrayAdapter(Context context, int modaff, items); ou
ArrayAdapter<T> aa = new ArrayAdapter(Context context, R.layout.row, R.id.txtv, items);
avec les arguments suivants :
•context est en général l'Activity courante,
•items est soit un tableau T[]ou une liste List<T> des éléments à afficher.
•modaff est l'identifiant du layout d'affichage personnalisé ou un afficheur standard suivant :
- android.R.layout.simple_list_item_1
- android.R.layout.simple_list_item_single_choice (sélection exclusive)
- android.R.layout.simple_list_item_multiple_choice (sélection multiple)
•R.layout.row fait référence au fichier xml qui décrit la ligne courante de l'afficheur, a priori un LinearLayout qui doit contenir au moins un TextView et R.id.txtv fait référence à ce TextView.
La possibilité de choix multiples est offerte au ListView dans sa description xml par :
-android:choiceMode="multipleChoice" ou en java par la méthode
-setChoiceMode(ListView.CHOICE_MODE_MULTIPLE)
Dans ce cas la liste des positions sélectionnées est obtenue par :
-SparseBooleanArray sb = listView.getCheckedItemPositions();
Pour un Spinner, on a les modes suivants :
-android.R.layout.simple_spinner_item
Pour un AutoCompleteTextView on a le mode :
-android.R.layout.simple_dropdown_item_1line
Pour afficher un string dans un GridView, on mettra l'identifier R.layout.cell associé au fichier .res\layout\cell.xml qui décrit un TextView standard.
Considérons un afficheur affView qui est une instance de ListView, Spinner, GridView,.. et l'instance aa d'un Adapter (ArrayAdapter, CursorAdapter, ou SimpleAdapter). L'affectation de l'adapter aa à l'afficheur affView est effectuée par : affView.setAdapter(aa);
ou bien, dans le cas ou l'Activity implémente une listActivity (cf plus loin), comme on le verra plus loin, par un setListAdapter(aa)
Il y a différentes interfaces qui permettent d'implanter des callbacks (fonctions de rappel) qui traitent les actions de l'utilisateur sur les objets affichés par les AdapterView. Citons :
1) onListItemClick qui permet de traiter le clic sur un item de l'AdapterView lorsque l'activité étend une ListActivity. Il n'y a pas de classe à enregistrer. On doit simplement écrire la callback :
public void onListItemClick(ListView parent, View v, int position, long id) { /* Travail à faire */ }
Dans le cas où l'activité n'étend pas une ListActivity on a les callbacks suivantes :
2) OnItemClickListener qui permet de traiter le clic sur un item de l'AdapterView. Pour cela, on doit enregistrer la classe qui implémente cette interface par un :
affView.setOnItemClickListener(cl) où cl est une instance de la classe OnItemClickListener qui implémente l'interface : Exemple
private OnItemClickListener cl = new OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {/* Travail à faire */}
};
Dans la callback position est le numéro de l'item cliqué, dans le widget view situé dans l'AdapterView parent., id est l'identificateur (?) de la ligne de l'item cliqué.
Pour mettre en évidence le widget sélectionné, on peut faire :
view.setSelected(true) ;
3) OnItemSelectedListener qui permet de traiter la sélection d'un item de l'AdapterView. Pour cela, on doit enregistrer la classe qui implémente cette interface par un :
affView.setOnItemSelectedListener(ci).
Les callbacks à programmer ont pour prototype :
void onItemSelected(AdapterView<?> parent,View view,int position,long id) et
void onNothingSelected(AdapterView<?> parent).
Remarque : La sélection ne se fait pas en cliquant, mais en navigant avec les flèches, ce qui fait que lorsqu'on veut sélectionner un item depuis un autre on sélectionne tous les items placés entre durant la navigation. Si on veut faire un traitement sur un item particulier, il vaut mieux le faire sur onItemClick (un click est envoyé quand on fait Enter ou click sur l'item sélectionné).
4) TextWatcher qui permet de surveiller la modification du texte dans un AutoCompleteTextView (sous-classe d'EditView). Pour cela, on doit enregistrer la classe qui implémente cette interface par un :
editView.addTextChangedListener(ci).
Les callbacks à programmer ayant pour prototype :
void onTextChanged(CharSequence s,int start, int before, int count) et
void beforeTextChanged(CharSequence s,int start, int before, int count) et
void afterTextChanged(Editable s). Généralement on ne fait rien dans ces 2 dernières.
Une ListActivity est une Activity qui simplifie la mise en œuvre d'une ListView (sans la spécifier) car elle inclut l'équivalent d'un ListItemClickListener. Il n'y a pas besoin d'implémenter l'interface. Si on a besoin d'accéder à la ListView dans le code java, on y accède par la méthode :
-ListView lv = getListView().
-Par ailleurs dans le main.xml layout de la ListActivity l'identificateur du listView est déclaré de manière particulière par : android:id="@android:id/list" au lieu de android:id="@+id/list".
L'ArrayAdapter du tableau ou de la liste d'items à la ListView est créé classiquement avec les mêmes identifiants de mode d'affichage ou de sélection :
ArrayAdapter<T> aa = new ArrayAdapter(context, id_aff_sel, items);
par contre son affectation à la ListView est simplement effectuée par :
this.setListAdapter(aa);
La callback de sélection a pour prototype :
void onListItemClick(ListView parent,View view,int position,long id);
On peut récupérer l'instance aa par this.getListAdapter();
Par ailleurs s'il n'y a que la liste d'objets T à afficher, il est inutile de faire le setContentView(. .) habituel au début du onCreate(..), le setListAdapter(. .) suffit.
Exemple avec un SimpleAdapter pour un répertoire téléphonique.
public class ListesActivity extends Activity {
ListView vue;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//On récupère une ListView de notre layout en XML, c'est la vue qui représente la liste
vue = (ListView) findViewById(R.id.listView);
/*
* On entrepose nos données dans un tableau qui contient deux colonnes :
* - la première contiendra le nom de l'utilisateur
* - la seconde contiendra le numéro de téléphone de l'utilisateur
*/
String[][] repertoire = new String[][]{
{"Bill Gates", "06 06 06 06 06"},
{"Niels Bohr", "05 05 05 05 05"},
{"Alexandre III de Macédoine", "04 04 04 04 04"}};
/*
* On doit donner à notre adaptateur une liste du type « List<Map<String, ?> » :
* - la clé doit forcément être une chaîne de caractères
* - en revanche, la valeur peut être n'importe quoi, un objet ou un entier par exemple,
* si c'est un objet, on affichera son contenu avec la méthode « toString() »
*
* Dans notre cas, la valeur sera une chaîne de caractères, puisque le nom et le numéro de téléphone
* sont entreposés dans des chaînes de caractères
*/
List<HashMap<String, String>> liste = new ArrayList<HashMap<String, String>>();
HashMap<String, String> element;
//Pour chaque personne dans notre répertoire…
for(int i = 0 ; i < repertoire.length ; i++) {
//… on crée un élément pour la liste…
element = new HashMap<String, String>();
/*
* … on déclare que la clé est « text1 » (j'ai choisi ce mot au hasard, sans sens technique particulier)
* pour le nom de la personne (première dimension du tableau de valeurs)…
*/
element.put("text1", repertoire[i][0]);
/*
* … on déclare que la clé est « text2 »
* pour le numéro de cette personne (seconde dimension du tableau de valeurs)
*/
element.put("text2", repertoire[i][1]);
liste.add(element);
}
ListAdapter adapter = new SimpleAdapter(this,
//Valeurs à insérer
liste,
/*
* Layout de chaque élément (là, il s'agit d'un layout par défaut
* pour avoir deux textes l'un au-dessus de l'autre, c'est pourquoi on
* n'affiche que le nom et le numéro d'une personne)
*/
android.R.layout.simple_list_item_2,
/*
* Les clés des informations à afficher pour chaque élément :
* - la valeur associée à la clé « text1 » sera la première information
* - la valeur associée à la clé « text2 » sera la seconde information
*/
new String[] {"text1", "text2"},
/*
* Enfin, les layouts à appliquer à chaque widget de notre élément
* (ce sont des layouts fournis par défaut) :
* - la première information appliquera le layout « android.R.id.text1 »
* - la seconde information appliquera le layout « android.R.id.text2 »
*/
new int[] {android.R.id.text1, android.R.id.text2 });
//Pour finir, on donne à la ListView le SimpleAdapter
vue.setAdapter(adapter);
}
}
Chaque cellule ligne d'un Listview peut être plus complexe qu'un simple TextView qui est la cellule par défaut quand rien n'est spécifié.
Il y a plusieurs manières de spécifier des cellules personnalisées.
La cellule ligne est décrite par un fichier xml, par exemple row.xml qui, en général, va décrire un LinearLayout horizontal et ses contenus, parmi lesquels au moins un TextView est nécessaire. Ce TextView doit avoir un android:id="@+id/nomtv" afin de pouvoir communiquer R.id.nomtv au constructeur de l'ArrayAdapter, de la manière suivante :
ArrayAdapter<T> aa = new ArrayAdapter(context, R.layout.row, R.id.txtv, items);
où l'on voit qu'à la place de l'argument modeaff il y a les 2 arguments :
R.layouy.row qui permet d'obtenir la description statique de la vue de la cellule ligne,
R.id.txtv qui permet de localiser le TextView dans cette vue.
La méthode précédente est mal adaptée pour faire varier le contenu de la cellule décrite par le fichier row.xml. Pour cela la solution habituelle consiste à étendre la classe ArrayAdapter de la manière suivante :
class MyArrayAdapter extends ArrayAdapter<String>
{
MyArrayAdapter () // Le contructeur appelle le super_constructeur
{
super(MyListActivity.this, R.layout.row, R.id.label, items);
}
// On surcharge uniquement cette méthode
public View getView(int position, View vueIn, ViewGroup parent)
{ // On récupère la vue de la ligne à traiter à l'aide de super qui peut
// être différente de vueIn (à cause d'un éventuel scrolling ?)
vueOut = super.getView(position, vue, parent);
// Ensuite on programme ce qui est à faire dans cette vue à cette position.
. . .
return(vue);
}
}
et c'est une instance aa de ce MyArrayAdapter qui sera passé en argument à setListAdapter(aa).
C'est dans la méthode getView que l'on va personnaliser l'aspect de la ligne en cours de traitement, car cette méthode est activée pour chaque ligne qui va être affichée. On reçoit :
- la position : qui permet de connaître quelle ligne on traite,
- la vue : qui permet d'accéder aux différents Layout et View de la ligne en question. Bien qu'une seule ligne figure dans le fichier xml, Le système génére une ligne différente pour chaque ligne à afficher avec des identificateurs différents pour chaque ligne, et c'est par le biais de ce vue qu'on accède à ces identificateurs individuels,
- le parent, qui généralement n'est utilisé que pour l'appel de super(…, parent)
La méthode getItem(int p) d'ArrayAdapter renvoie l'instance de type T qui se trouve à la position p.
A titre d'exemple d'inflation d'un fichier xml en View montrons comment générer l'affichage des vues précédentes par son intermédiaire. Dans l'exemple ci-dessus, on va se passer de la méthode super.getView(..), qui fournit l'objet View (en l'occurence le LinearLayout) de la ligne qui occupe la place "position". On va générer un objet View d'une ligne générique à l'aide de la méthode LayoutInflater. inflate(..) (qui permet de convertir la description xml en objets View), comme suit :
On remplace la ligne :
if(vue == null) vue = super.getView(position, vue, parent);
par :
if(vue == null)
vue = getLayoutInflater().inflate(R.layout.row, parent, false);
mais ici c'est une vue générique et pas la vue qui correspond à la ligne "position". Il faut donc y mettre son texte :
TextView label = (TextView)vue.findViewById(R.id.label);
label.setText(items[position]);
Le reste est identique.
L'argument parent est le ViewGroup dans lequel la vue est incorporée (le ListView en l'occurence) et le booleen doit être mis à false sinon un parent redondant est ajouté.
Un autre exemple d'inflation se trouve au paragraphe 19
Les accès aux vues par la méthode findViewById sont couteux en temps de calcul. Il y a intérêt à mémoriser ces accès. Pour cela on peut utiliser les méthodes setTag et getTag de la classe View.
Si on a un seul objet à accéder, on peut faire comme ceci :
ImageView icon = (ImageView) vue.getTag();
if(icon == null)
{ icon = (ImageView) vue.findViewById(R.id.icon); vue.setTag(icon);}
On récupère l'objet par getTag(). S'il n'a pas encore été mémorisé, on obtient nul, alors on le crée et on le mémorise par setTag(objet).
Si on a plusieurs objets on peut les mémoriser en utilisant un identificateur xml différent pour chacun (s'ils n'en ont pas on peut en définir dans le fichier .\res\valeus\ids.xml) en faisant : setTag(R.id.nom1, objet1), et les récupérer grâce à leur identificateur : getTag(R.id.nom1).
Un autre moyen de mémoriser plusieurs objets est de les regrouper dans une classe et d'utiliser cette classe avec setTag(), getTag() sans identificateur. Cette classe, disons HouseHolder fonctionne comme suit :
class HouseHolder {
Objet1 obj1;
Objet2 obj2;
HouseHolder(Objet1 o1, Objet2 o2) {obj1 = o1; obj2 = o2;}
}
Dans ce cas, les 2 objets sont mémorisés en utilisant le constructeur et en stockant l'instance du HouseHolder avec setTag(). Tout est récupéré par getTag().
HouseHolder mem =(HouseHolder) vue.getTag();
if (mem == null)
{
o1 = (Objet1) vue.findViewByID(. .);
o2 = (Objet2) vue.findViewById(. .);
mem = new Memo((o1, o2);
vue.setTag(mem);
}
// Ensuite, utilisation des objets mem.obj1, mem.obj2
Le traitement du toucher de l'écran se fait à l'aide de l'interface View.OnTouchListener() qui ne demande que l'implantation de :
@Override
public boolean onTouch(View view, MotionEvent event){}
L'objet event de la classe MotionEvent fournit les informations sur l'évènement détecté . Ces évènements peuvent concerner le toucher de l'écran par un ou plusieurs doigts, un stylet, un joystick, etc..Dans le cas des doigts, il y a le type de contact (ACTION_UP, ACTION_DOWN, ACTION_MOVE, ...) et son numéro (index) lorsque plusieurs contacts ont lieu en même temps. Des méthodes permettent de faire le tri parmi les évènements colportés par l'objet event. Ainsi :
int action = event.getActionMasked() limite le retour aux évènements suivants :
- ACTION_DOWN : 1er appui détecté, event.getX() et event.getY() donnent sa position.
- ACTION_POINTER_DOWN : Nouvel appui détecté. i = getActionIndex() donne son numéro, et event.getX(i) et event.getY(i) sa position.
- ACTION_POINTER_UP : Un appui secondaire terminé.
- ACTION_UP : Fin global d'un appui
- ACTION_MOVE : mouvement détecté entre ACTION_DOWN et ACTION_UP. Les méthodes event.getX(i) et event.getY(i)fournissent les positions actuelles.
Autres actions moins fréquemment utilisées : ACTION_CANCEL, ACTION_SCROLL, ACTION_BUTTON_PRESS, ACTION_BUTTON_RELEASE, ...
Toute activité ou sous-activité susceptible d'être activée doit être déclarée dans le fichier AndroidManifest.xml, dans une section application, sous-section activity, attribut android:name, valeur : le nom de la classe (complet précédé du nom du paquetage ou simple, précédé par un "." si elle est dans le paquetage déclaré dans le fichier. Exemple :
<application ...>
<activity android:name=".myActivity" />
</application>
Le lancement explicite d'une sous-activité peut se faire par la fonction startActivity(intent) qui a pour paramètre une instance de la classe Intent qui servira de lien d'appel de cette sous-activité.On crée ce lien d'appel tout simplement par son constructeur :
Intent intent = new Intent(this, myActivity.class);
qui prend en argument le contexte de l'appelant (l'activité en cours this) et la classe cible (qui dérive de Activity) de la sous-activité à démarrer.
On peut également appeler une activité android prédéfinie par un String "Action", par exemple pour démarrer le bluetooth, il existe le string prédéfini BluetoothAdapter.ACTION_REQUEST_ENABLE. On fera donc :
Intent turnOn = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(turnOn, 0);
Ce lien d'appel sert également à échanger des paramètres entre l'activité appelante et l'activité appelée. Ces paramètres sont rangés dans l'instance intent par les méthodes :
intent.putExtra(String name, Type value);
où Type accepte pratiquement tous les types courants.
La sous-activité appelée récupère l'instance de l'Intent par :
Intent intent = getIntent() ;
et directement un paramètre x associé au nom "clé" par une méthode du genre :
Type x = intent.getTypeExtra("clé") ;
ou Type peut prendre pratiquement tous les types courants, ou bien en deux temps par :
Bundle extra = intent.getExtras();
Type x = extra.getType("clé");
Si l'activité appelée ne doit fournir aucun retour, on l'appellera tout simplement par :
startActivity(intent) ;
avec en argument l'intent créé pour l'activer. Pour terminer la sous-activité et revenir à l'appelant, on appelle la méthode finish();
Attention pour que ce qui est décrit ci-après fonctionne, il ne faut pas que l'activité appelante ait été affectée dans le fichier AndroidManifest.xml de l'attribut android:launchMode=""singleInstance".
Pour être averti de la fin de la sous-activité, le parent lance la fonction :
startActivityForResult(intent, mon_code) ;
ou mon_code est un entier choisi pour identifier cet appel parmi tous les retours qui seront reçus dans la callback écrite à cette intention dans l'appelant. La callback utilisée pour traiter les retours a pour prototype :
protected void onActivityResult(int requestCode, int resultCode, Intent intent)
Si requestCode vaut mon_code, cela signifie qu'on traite le retour de l'appel précédent.
L'entier resultCode est positionné dans la sous-activité juste avant sa terminaison par les méthodes :
setResult(resultCode);
setResult(resultCode, intent);
puis on appelle finish();
Généralement, on positionne RESULT_OK si le travail a été fait correctement, sinon on positionne RESULT_CANCELED.
La méthode setResult(resultCode, intent) permet de renvoyer un intent (soit utiliser le même, c'est-à-dire getIntent(), soit créer un nouveau par un new dans la sous-activité) grace auquel la sous-activité peut renvoyer toutes sortes de données à l'activité appelante.
Si l'activité appelante a été définie dans le fichier AndroidManifest.xml avec l'attribut android:launchMode=""singleInstance", la callback onActivityResult est appelée dès le lancement de la sous-activité et non à sa terminaison, ce qui fait que les éléments renvoyés contiendront n'importe quoi.
Dans l'exemple suivant l'activité principale IntentDemoActivity affiche un texte et un bouton. La callback appelsa d'appui sur ce bouton appelle la sous-activité Sousactivite. Celle-ci affiche un texte, un ratingBar et deux bouttons (Ok et Cancel). Les calbacks d'appui sur ces boutons renvoient setResult(RESULT_OK) et la valeur du ratingBar au moyen d'un intent ou setResult(RESULT_CANCEL) avant de faire finish();.
Dans l'appelant la méthode onActivityResult traite le retour, récupère éventuellement la valeur renvoyée et affiche un message adéquat.
Exemple Intendemo :
Fichier AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.llibre.intentdemo"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="10" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".IntentDemoActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Sousactivite" />
</application>
</manifest>
Fichier res/values/string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World, IntentDemoActivity!</string>
<string name="app_name">IntentDemo</string>
</resources>
Fichier res/layout/main.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="appelsa"
android:text="Button" />
</LinearLayout>
Fichier res/layout/sousview.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RatingBar
android:id="@+id/ratingBar1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:stepSize="0.2" />
<TextView
android:id="@+id/tex2View1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Formulaire réponse" />
<LinearLayout
android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/button_oui"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="myclick"
android:layout_weight="1"
android:text="OK" />
<Button
android:id="@+id/button_non"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="myclick"
android:layout_weight="1"
android:text="CANCEL" />
</LinearLayout>
</LinearLayout>
Ficher fr.llibre.intendemo/IntenDemoActivity.java :
package fr.llibre.intentdemo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class IntentDemoActivity extends Activity {
private final int CODE_SOUSACTIVITE = 951;
private Intent intentSa;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
intentSa = new Intent(this, Sousactivite.class);
}
public void appelsa(View v)
{
startActivityForResult(intentSa, CODE_SOUSACTIVITE);
}
@Override
public void onActivityResult(int req, int rep, Intent data)
{
if(req == CODE_SOUSACTIVITE)
{
switch(rep)
{
case RESULT_OK :
// float v = data.getFloatExtra("score", -1.f);
Bundle extra = data.getExtras();
float v = (extra == null ? -1.f : extra.getFloat("score"));
Toast.makeText(this,"Valeur "+v+". Appui sur Ok !",Toast.LENGTH_LONG).show();
break;
case RESULT_CANCELED :
Toast.makeText(this,"Appui sur Cancel !",Toast.LENGTH_LONG).show();
break;
default :
Toast.makeText(this,"Retour inopiné !",Toast.LENGTH_LONG).show();
break;
}
}
}
}
Ficher fr.llibre.intendemo/Sousactivite.java :
package fr.llibre.intentdemo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.RatingBar;
public class Sousactivite extends Activity {
RatingBar rb;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sousview);
rb = (RatingBar) findViewById(R.id.ratingBar1);
}
public void myclick(View w)
{
switch(w.getId())
{
case R.id.button_oui :
Intent it = new Intent();
float v = rb.getRating();
it.putExtra("score", v);
setResult(RESULT_OK, it);
break;
case R.id.button_non :
setResult(RESULT_CANCELED);
break;
}
finish();
}
}
Lorsque l'activité A appelle la sous-activité B par un startActivity ou par un startActivityForResult il peut y avoir destruction de l'activité appelante A. En effet, suite à cet appel, le système appelle les méthodes A.onPause() et A.onStop() et éventuellement A.onDestroy(). Ainsi si des valeurs doivent être transmises de A à B par l'intermédiaire de l'écriture dans un fichier il faut le faire avant A.onStop(), typiquement dans A.onPause().
Lorsque la sous_activité B à terminé son travail, les méthodes suivantes de l'activité appelante A sont exécutées par le système : éventuellement A.onCreate(Bundle b) si A.onDestroy() avait été appelé, puis A.onStart(), puis A.onActivityResult(..), puis A.onResume().
Si l'activité appelante est détruite par A.onDestroy(), puis recréée par A.onCreate(Bundle b) les variables locales non statiques sont perdues, ce qui peut conduire à des erreurs. Pour pallier à cela :
•On peut déclarer static les variables que l'on veut retrouver inchangées. Il semble que cela ne marche pas dans tous les cas.
•On peut sauvegarder des variables dans le Bundle en surchargeant la méthode onSaveInstanceState(Bundle b) qui est appelée avant onStop(). On peut récupérer ces données en surchargeant la méthode onRestoreInstanceState(Bundle b) qui est appelée entre onStart() et onResume(), c'est-à-dire bien après onCreate(Bundle b) où les variables précédentes ne seront pas disponibles. On peut déplacer la portion de code qui les utilise dans onResume() où elles seront disponibles, ou plus simplement les récupérer directement dans onCreate(Bundle b) à partir du Bundle.
Les intents peuvent servir à activer un navigateur, un appel téléphonique, etc au moyen d'intents spécifiques, créés par une instruction du type :
Intent i = new Intent(String Action);
Intent i = new Intent(String Action, Uri u);
Le premier argument spécifie l'action désirée. Le deuxième argument, l'Uri dépend du premier. Exemples :
Appel du navigateur :
Uri u = Uri.parse("http://www.llibre.fr");
Intent i = new Intent(Intent.ACTION_VIEW, u);
startActivity(i);
Appel d'une carte de géographie autour d'un point latitude,longitude :
Uri u = Uri.parse("geo:43.52543,1.57299");
startActivity(new Intent(Intent.ACTION_VIEW, u));
Appel du composeur de numéros de téléphone :
Uri u = Uri.parse("tel:0612345678");
Intent i = new Intent(Intent.ACTION_DIAL, u);
startActivity(i);
Appel téléphonique :
Uri u = Uri.parse("tel:0612345678");
Intent i = new Intent(Intent.ACTION_CALL, u);
startActivity(i);
mais pour autoriser cet appel, depuis l'application en cours, il faut ajouter la ligne suivante dans son fichier AndroidManifest.xml :
<uses-permission android:name="android.permission.CALL_PHONE" />
en premier niveau à l'intérieur de la section principale <manifest> ... </manifest>.
Pour éditer et supprimer des données.
Pour envoyer un SMS, un E-mail.
Intent i = new Intent(android.content.Intent.ACTION_SEND);
String[] recipients = new String[]{"my@email.com", "",}; i.putExtra(android.content.Intent.EXTRA_EMAIL, recipients); i.putExtra(android.content.Intent.EXTRA_SUBJECT, "Test"); i.putExtra(android.content.Intent.EXTRA_TEXT, "Message");
i.setType("text/plain");
startActivity(Intent.createChooser(i, "Send mail..."));
avec ici l'utilisation d'une boite de dialogue pour choisir l'application dans le cas où il y en a plusieurs qui peuvent faire l'affaire.
Pour faire une recherche sur internet.
Un intent peut être diffusé à l'ensemble des applications du système pour communiquer une ou des informations provenant de notre application ou pour demander la réalisation de certaines actions. Les récepteur de ces Broadcast intents sont appelés des Broadcast Receiver.
Un Broadcast Receiver (qui doit surcharger la méthode onReceive) peut-être créé en dynamique comme une variable statique de la manière suivante :
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Examen si action attendue
if (action.equals(ACTION_ATTENDUE_1))
{ // Traitement action attendue 1 }
if (action.equals(ACTION_ATTENDUE_2))
{ // Traitement action attendue 2 }
}
};
Pour qu'il soit actif, il faut l'enregistrer, par exemple dans le OnCreate de l'Activity :
IntentFilter filter = new IntentFilter(ACTION_ATTENDUE_1);
filter.addAction(ACTION_ATTENDUE_1);
registerReceiver(mReceiver, filter);
On spécifie dans l'IntentFilter les actions que le Broadcast Receiver doit surveiller. Remarquer que lorsqu'il doit surveiller plusieurs actions, la première est spécifiée au new de l' l'IntentFilter et les suivantes sont ajoutées par la méthode addAction.
Le Broadcast Receiver doit être désenregistré dans le OnDestroy de L'Activity :
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
Il y a deux types de menu dans les applications android : Le menu d'options qui s'ouvre lorsqu'on appuie sur le bouton dédié "Menu", et des menus contextuels. Un menu contextuel est associé à un widget et s'ouvre lorsqu'on fait un appui prolongé sur ce widget s'il dispose d'un tel menu.
Losqu'on appuie pour la première fois sur un menu d'option, il apparait en mode d'icône qui n'affiche, en général, que les 6 premiers éléments.
Le menu d'options est construit en implémentant la méthode de rappel suivante :
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(...).setIcon(...);
.....
return (super.onCreateOptionsMenu(menu));
}
Avant chaque appel de menu la callback onPrepareOptionsMenu() est appelée. On peut utiliser cette callback pour effectuer des modifications dans le menu et l'adapter ainsi au contexte courant de l'appli.
On ajoute un MenuItem au menu à l'aide de la méthode add :
MenuItem android.view.Menu.add(int groupId, int itemId, int order, CharSequence title)
•groupId = Menu.NONE, sauf si on veut utiliser l'option setGroupCheckable().
•itemId = un entier identifiant ce menu qui sera utilisé pour identifier l'item choisi par la callback de traitement du menu onOptionsItemSelected(). Généralement on némérote ces identifiants par des public static final int auquels on donne des valeurs entières commençant par Menu.FIRST+1 et en suivant.
•order = Menu.NONE (identifiant d'ordre du choix dans le menu, à NONE pour le moment)
•titre = le texte choisi pour ce menu-item.
On peut associer une icône à ce MenuItem par sa méthode setIcon(), un raccourci alphabétique ou numérique par les méthodes setAlphabeticShortcut() et setNumricShortcut(), lui ajouter une case à cocher simple par la méthode setCheckable(), ou une case à cocher mutuellement exclusive dans un groupe par la méthode setGroupCheckable(groupId);
On peut créer un sous-menu (un seul sous-niveau possible) par la méthode addSubMenu qui prend les mêmes arguments que le méthode add.
Le traitement de la sélection d'un item d'un menu ou sous-menu se fait par la callback onOptionsItemSelected comme suit :
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_ITEM1: ...; return(true);
case MENU_ITEM2: ...; return(true);
}
return(super.onOptionsItemSelected(item));
}
L'appel de la méthode (d'Activity) registerForContextMenu(View view); indique que le widget view est doté d'un menu contextuel.
Ce menu est créé par la callback onCreateContextMenu comme suit :
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
menu.add(Menu.NONE, MENU_CAP, Menu.NONE, "Capitaliser");
menu.add(Menu.NONE, MENU_REMOVE, Menu.NONE, "Supprimer");
}
où les argument de la méthode add sont les mêmes que pour la méthode add du menu d'options.
Ce même menu peut être crée à partir du descriptif menu/context.xml suivant :
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/cap"
android:title="Capitaliser" />
<item android:id="@+id/remove"
android:title="Supprimer" />
</menu>
et avec ce descriptif, le source de la callback se simplifie en :
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
new MenuInflater(this).inflate(R.menu.context, menu);
}
Le traitement de la sélection d'un item du menu contextuel se fait par la callback onContextItemSelected comme suit :
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_CAP: ...; return(true);
case MENU_REMOVE: ...; return(true);
}
return(super.onContextItemSelected(item));
}
Voici un exemple de boite de dialogue comprenant un zone d'édition pour lire un mot, zone d'édition spécifiée par le layout maboite.xml suivant, qui comprend simplement le Textview "mot" suivi de l'EditText :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TextView
android:text="Mot : "
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
/>
</LinearLayout>
La méthode qui présente cette boite est la suivante :
private void showBoite() {
final View view=getLayoutInflater().inflate(R.layout.maboite, null);
new AlertDialog.Builder(this)
.setTitle("Entrer un mot")
.setView(view) // Le layout d'édition est affiché dans l'alertDialog
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton)
{ // action si ok cliqué
}})
.setNegativeButton("Annuler", null) // néant si annuler cliqué
.show();
}
Elle génère d'abord la view du layout d'édition, puis crée l'AlerDialog en y insérant cette view, puis on ajoute les callbacks qui traitent l'appui sur "Ok" et sur "Annuler" (néant dans ce cas).
Exemple de lecture Date/Temps
Le layout.xml : date-dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/llDateTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Date (JJ/MM/AAAA)"
android:textSize="18sp" />
<EditText
android:id="@+id/edJour"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="31"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"
android:textSize="18sp"/>
<EditText
android:id="@+id/edMois"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="12"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"
android:textSize="18sp"/>
<EditText
android:id="@+id/edAnnee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="2017"
android:textSize="18sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="HEURE T.U. (HH/MM/SS)"
android:textSize="18sp"/>
<EditText
android:id="@+id/edHeure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="23"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":"
android:textSize="18sp"/>
<EditText
android:id="@+id/edMinute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="59"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":"/>
<EditText
android:id="@+id/edSeconde"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|end"
android:inputType="number"
android:text="59"
android:textSize="18sp"/>
</LinearLayout>
</LinearLayout>
La méthode qui appelle ce dialogue (avec utilisation d'un TextWatcher) :
private EditText edJour, edMois, edAnnee, edHeure, edMinute, edSeconde;
private int annee, moi, jour, heure, minute, seconde;
public void onClickDate_old(View v) {
View view = (LinearLayout) LinearLayout.inflate(this, R.layout.date_dialog, null);
edJour = (EditText) view.findViewById(R.id.edJour);
edMois = (EditText) view.findViewById(R.id.edMois);
edAnnee = (EditText) view.findViewById(R.id.edAnnee);
edHeure = (EditText) view.findViewById(R.id.edHeure);
edMinute = (EditText) view.findViewById(R.id.edMinute);
edSeconde = (EditText) view.findViewById(R.id.edSeconde);
edJour.addTextChangedListener(new EditWatcher(edJour, 31));
edJour.setText(String.valueOf(jour));
edMois.addTextChangedListener(new EditWatcher(edMois, 12));
edMois.setText(String.valueOf(mois));
edAnnee.addTextChangedListener(new EditWatcher(edAnnee, 9999));
edAnnee.setText(String.valueOf(annee));
edHeure.addTextChangedListener(new EditWatcher(edHeure, 23));
edHeure.setText(String.valueOf(heure));
edMinute.addTextChangedListener(new EditWatcher(edMinute, 59));
edMinute.setText(String.valueOf(minute));
edSeconde.addTextChangedListener(new EditWatcher(edSeconde, 59));
edSeconde.setText(String.valueOf(Math.round(seconde)));
AlertDialog.Builder aldb = new AlertDialog.Builder(this);
aldb.setTitle("DATE - HEURE");
aldb.setView(view); // Le layout d'édition est affiché dans l'alertDialog
aldb.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// Traitement à faire avant sortie par clic sur Ok
try {
annee = Integer.parseInt(edAnnee.getText().toString());
mois = Integer.parseInt(edMois.getText().toString());
jour = Integer.parseInt(edJour.getText().toString());
heure = Integer.parseInt(edHeure.getText().toString());
minute = Integer.parseInt(edMinute.getText().toString());
seconde = Integer.parseInt(edSeconde.getText().toString());
} catch (NumberFormatException exception) {}
}
}
);
aldb.setNegativeButton("Annuler",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// On ne fait rien
}
}
);
aldb.create().show();
}
// Classe permettant de limiter les entrées numériques dans les EditView du dialogue ci-dessus
class EditWatcher implements TextWatcher {
EditText ed;
int maxVal;
boolean ignoreChange = false;
EditWatcher(EditText e, int maxValue) {
ed = e;
maxVal = maxValue;
}
@Override
public void afterTextChanged(Editable s) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!ignoreChange) {
boolean modif = false;
try {
int val = Integer.parseInt(s.toString());
if (val > maxVal) modif = true;
} catch (NumberFormatException exception) {
modif = true;
}
if (modif) {
ignoreChange = true;
ed.setText("");
ed.setSelection(ed.getText().length());
ignoreChange = false;
}
}
}
}
On peut dessiner dans un widget View. Il suffit pour cela de surcharger sa méthode onDraw(Canvas canvas). La méthode la plus simple pour faire un graphique est ainsi de créer une classe qui étend View et de faire ses tracés dans une méthode appelée par onDraw. Exemple :
public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// DemoView demoview = new DemoView(this);
MyView demoview = new MyView(this);
setContentView(demoview);
}
private class MyView extends View {
Paint paintSolid;
Paint paintDash;
DashPathEffect dashPathEffect;
public MyView(Context context) {
super(context);
paintSolid = new Paint();
paintSolid.setTextSize(64);
paintDash = new Paint();
paintDash.setColor(Color.LTGRAY);
paintDash.setStyle(Paint.Style.STROKE);
paintDash.setStrokeWidth(10);
float[] intervals = new float[]{50.0f, 2.0f};
float phase = 0;
DashPathEffect dashPathEffect =
new DashPathEffect(intervals, phase);
paintDash.setPathEffect(dashPathEffect);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = this.getWidth();
int h = this.getHeight();
paintSolid.setStrokeWidth(10);
paintSolid.setColor(Color.BLACK);
paintSolid.setStyle(Paint.Style.STROKE);
canvas.drawRect(0.1f*w, 0.1f*h,0.9f*w,0.9f*h,paintSolid);
canvas.drawLine(0.1f*w, 0.1f*h,0.9f*w,0.9f*h,paintDash);
paintSolid.setStyle(Paint.Style.FILL);
paintSolid.setStrokeWidth(0.5f);
canvas.drawText("Bonjour Michel",0.3f*w,0.2f*h,paintSolid);
paintSolid.setColor(Color.RED);
canvas.drawRect(0.6f*w, 0.6f*h,0.8f*w,0.8f*h,paintSolid);
}
}
}
Dans le créateur du widget public MyView(Context context), après avoir appelé super, on effectue les initialisations des objets que l'on ne veut pas avoir à recalculer à chaque appel de onDraw(), et dans cette méthode, après l'appel de super, on effectue les tracés, à l'aide de méthodes du genre canvas.drawXyz(…). On peut dessiner des lignes, des formes, du texte, etc. Ces méthodes prennent toujours un objet Paint en dernier argument. C'est à lui que l'on affecte les attributs du tracé. En particulier, il y a 3 principaux styles :
•Paint.Style.STROKE pour le tracé des contours sans remplissage,
•Paint.Style.FILL pour le remplissage sans tracé du contour,
•Paint.Style.FILL_AND_STROKE pour le tracé du contour avec remplissage
Dans le cas de tracé de texte ces 3 styles s'appliquent au tracé des lettres et si la police est suffisamment grande (fixée par Paint.setTextSize(int)) on aura que le contour des lettres avec Paint.Style.STROKE. Pour qu'elles soient pleines, il faut utiliser Paint.Style.FILL ou Paint.Style.FILL_AND_STROKE.
La méthode Paint.setStrokeWidth(float d) fixe la largeur d du trait (même dans le cas des textes). D'autres méthodes permettent de définir la forme des bouts, etc. Attention : Des lignes de largeur inférieure à 1 pixel seront parfois invisibles.
Pour effectuer des traits tiretés, il faut utiliser un Paint où l'on aura défini la méthode au moyen de la méthode Paint.setPathEffect(DashPathEffect dpe), les dimensions des traits et espaces successifs. Avec par exemple :
DashPathEffect dpe = new DashPathEffect(new float[]{50.0f, 2.0f}, 0.f);
où le dernier argument est un offset (s'il vaut 10, le premier tiret n'aura que 40 de long au lieu de 50). La dimension du tableau doit être paire, les éléments pairs étant tracés et les impairs sautés.
L'interface Runnable qui peut être utilisée dans n'importe quel contexte (thread principal ou non), ne fait qu'imposer la présence d'un méthode void run(). Par exemple le code suivant définit un objet Runnable :
private Runnable myRunnable = new Runnable() {
@Override
public void run() { /* Ici le code a exécuter */ }
};
L'exécution du thread se produira, par exemple, lorsque le runnable recevra un message d'activation posté par un Handler (cf plus loin) comme par exemple :
Handler myHandler = new Handler();
myHandler.post(myRunnable);
Exemple d'exécution différée de 2 secondes d'une tache :
new Handler().postDelayed(new Runnable(){@Override public void run(){tvStatus.setText("Bonjour Michel");}},2000);
Un Thread (autre que le principal) est un processus concurrent créé par une classe qui doit implémenter l'interface Runnable. La méthode run() est utilisée pour mettre le code exécuté au niveau de ce thread.
Exemple simple de création d'un Thread et de son lancement :
Thread t = new Thread() {
public void run() {
System.out.println("Mon traitement");
}
};
t.start();
Autre exemple en passant par l'intermédiare d'une classe qui éténd la classe thread :
package fr.llibre.thread
public class MonThread extends Thread {
@Override
public void run() {
System.out.println("Mon traitement");
}
}
///////
package fr.llibre.thread
public class TestThread {
public static void main(String[] args) {
MonThread t = new MonThread();
t.start();
}
}
Les constructeurs les plus utilisés sont :
•Thread() ou Thread(String nom) qui permet de donner un nom au thread.
•Thread(Runnable cible) ou Thread(Runnable cible, String nom).
Dans le deuxième cas, la cible est une classe qui étend l'interface Runnable, définissant ainsi le code à exécuter dans le thread. Mais on peut également utiliser une classe anonyme qui implente l'interface Runnable en mettant directement comme argument le code suivant :
new Runnable() { public void run() {/* Ici le code a exécuter */} };
Il faut remarquer que les constructeurs ne lancent pas le thread (voir méthode start() ci-après). Ils le mettent dans l'état NEW. Au premier return rencontré dans la méthode run(), il est mis dans l'état TERMINATED.
En plus de la méthode run(), les méthodes importantes de la classe Thread sont :
•void start(). C'est par elle qu'on lance le thread qui passe à l'état RUNNABLE, c'est-à-dire actif. Auparavant il était dans l'état NEW.
•void yield(). Le thread laisse son tour au thread suivant tout en restant dans l'état RUNNABLE (cad actif).
•static void sleep(long ms). Passe le thread à l'état TIMED_WAITING. Il repassera à l'état RUNNABLE après ms millisecondes. Cette méthode statique peut lever l'interruption InterruptedException. (-> try/catch ou throws)
•void object1.wait() ou object1.wait(long timeout). Le thread se met dans l'état WAITING jusqu'à l'appel par un autre thread des méthodes suivantes,
•void object1.notify(). Le premier thread en wait sur object1 passe à l'état RUNNABLE.
•void object1.notifyAll(). Tous les threads en wait sur object1 passent à l'état RUNNABLE.
•void interrupt(). Le thread a par b.interrupt() positionne l'indicateur interrupted du thread b pour qu'il se termine, mais ce dernier peut très bien l'ignorer.
•static Thread currentThread() qui renvoie une référence sur le thread en cours,
Tous ces éléments sont du java standard.
Exemple de création de Thread pour accéder à internet, ce qui est interdit dans le Thread UI.
Thread myThread = new Thread() {
@Override
public void run() {
satSel = new Sater(TwoLE.makeNew(TwoLE.url[noUrl], noSat));
return;
}
};
myThread.start();
while (myThread .isAlive()) try {sleep(200);}
catch (InterruptedException e) {e.printStackTrace();}
Dans cet exemple, on cherche à créer un objet satSel de la classe Sater qui est déclarée dans le bloc UI, mais le constructeur utilisé accède à internet pour lire des données, ce qui ne peut être effectué dans le bloc UI. On encapsule donc cet appel dans un Thread qu'on lance aussitôt, et on attend sa fin pour utiliser l'objet satSel ainsi créé.
Remarque : Pour accéder à internet il faut mettre la demande de permission suivante dans le manifeste :
<uses-permission android:name="android.permission.INTERNET" />
Les classes qui étendent la classe TimerTask sont utilisées pour effectuer des tâches lourdes dans un thread d'arrière plan (afin de ne pas bloquer l'interface avec l'usager (l'UI). La méthode run() de ces tâches peut être lancée ou cadencée par une instance de la classe Timer au moyen des méthodes schedule(...) et scheduleAtFixedRate(...). Si une périodicité est fixée, avec schedule la période est relative à la fin de l'éxécution précédente, et avec scheduleAtFixedRate les périodes sont relative au début de la 1ère exécution.
Dans l'exemple ci-dessous la tâche MaTask1 est exécutée immédiatement toutes les 1000 millisecondes, et la tâche Matask2 arrête ce cadencement en cancelant les timers qui ont cadencés ces 2 tâches ;
Le 2eme arg des méthodes schedule ou scheduleAtFixedRate est un delai avant la première exécution et le 3ème argument est la périodicité.
import java.util.Timer;
import java.util.Date;
import java.util.TimerTask;
public class TestTimer {
private static Timer timerLoop, timerFin;
public static void main(final String[] args) {
timerLoop = new Timer();
timerLoop.scheduleAtFixedRate(new MaTask1(), 0, 1000);
timerLoop.schedule(new MaTask2(), 12000);
timerFin = new Timer();
}
private static class MaTask1 extends TimerTask {
@Override
public void run() {
System.out.println(new Date() + " Execution de ma tache");
}
}
private static class MaTask2 extends TimerTask {
@Override
public void run() {
timerLoop.cancel();
timerLoop.purge();
timerFin.cancel();
}
}
}
Les méthodes schedule et scheduleAtFixedRate de Timer lancent l'exécution de la méthode run de la classe passée en 1er argument dans un nouveau thread. On peut le vérifier en mettant les impressions suivantes :
System.out.println("Nom Thread dans XXX : "+Thread.currentThread().getName());
dans la routine main et dans la routine run (remplacer XXX par main puis run).
Dans l'exemple suivant, on tue le timer après un certain delai au moyen d'un sleep :
public class TestTimer {
public static void main(final String[] args) {
Timer timer = new Timer();
timer.schedule(new MaTask1(), 0, 1000);
try { Thread.sleep(12000);}
catch (InterruptedException e) {e.printStackTrace();}
timer.cancel();
}
private static class MaTask1 extends TimerTask {
@Override
public void run() {
System.out.println(new Date() + " Execution de ma tache");
}
}
}
Dans Android, tout ce que voit l'utilisateur est géré par un thread appelé user interface thread ou UI thread et seul ce thread à le droid de modifier la View de l'UI. Si des traitement sont effectués dans d'autre thtreads, ils doivent envoyer les résultats à afficher par l'intermédiaire de messages vers la pile de message de ce thread d'où ils seront extraits pour être traités. Pour éviter cela on peut utiliser une classe implémentant l'interface Runnable.
Java fournit la classe Handler dont les objets permettent d'envoyer des messages à des runnables de n'importe quel thread. Par exemple :
Handler myHandler = new Handler();
myHandler.post(myRunnable);
myHandler.post(myRunnable, delai_millisec);
Ainsi dans Addroid on peut utiliser un objet handler pour permettre à un thread en arrière plan de communiquer avec l'UI_thread de l'activité afin de synchroniser l'interface utilisateur sur l'avancement des calculs dans le background_thread. On utilise un objet Handler déclaré dans l'activité pour envoyer et éventuellement réceptionner des objets Message ou Runnable.
Exemple de handler traitant des messages dans le thread UI en proveance d'un autre thread :
private final static int MESSAGE_READ = 2;
private final static int CONNECTING_STATUS = 3;
Handler mHandler = new Handler()
{
public void handleMessage(android.os.Message msg)
{
if(msg.what == MESSAGE_READ)
{
String readMessage = null;
try
{
readMessage = new String((byte[]) msg.obj, "UTF-8");
} catch (UnsupportedEncodingException e) {e.printStackTrace();}
tvRead.setText(readMessage);
}
if(msg.what == CONNECTING_STATUS)
{
if(msg.arg1 == 1) tvStatus.setText("Connecté à : " + (String)(msg.obj));
else tvStatus.setText("Echec connexion !");
}
}
};
et depuis un autre hread on postera selon les circonstances ces messages :
...
mHandler.obtainMessage(CONNECTING_STATUS, -1, -1).sendToTarget();
...
mHandler.obtainMessage(CONNECTING_STATUS, 1, -1, name).sendToTarget();
...
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
Pour faire un cadencement rigoureux, on peut passer par un myTimer.scheduleAtFixedRate mais le run de la TimerTask va s'exécuter dans un thread qui n'a pas le droid de modifier l'UI ! Alors dans le run de la TimerTask on peut réveiller le run d'un runnable de l'UI à l'aide d'un myHandler.post(uiRunnable);
Exemple :
Date nowDate; // lue à intervalles réguliers
Timer myTimer;
Handler myHandler;
private class MyTimerTask extends TimerTask {
@Override
public void run() // s'exécute dans un thread secondaire
{
nowDate = new Date();
myHandler.post(myRunnable);
}
};
private Runnable myRunnable = new Runnable() {
@Override
public void run() // s'exécute dans le thread de l'UI
{
// Utilisation de nowDate pour modifier l'UI
}
};
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/// code
myHandler = new Handler();
myTimer = new Timer();
myTimer.scheduleAtFixedRate(new MyTimerTask(), 0, 1000);
}
Beaucoup plus simplement, on peut se passer de la classe MyTimerTask, et relancer le run de myRunnable, mais la précision du cadencement sera moindre à cause de la variabilité du temps passé dans le run de cette méthode. Exemple :
Date nowDate; // lue à intervalles réguliers
Handler myHandler;
private Runnable myRunnable = new Runnable() {
@Override
public void run() // s'exécute dans le thread de l'UI
{
nowDate = new Date();
// Utilisation de nowDate pour modifier l'UI
myHandler.postDelayed(this, 1000); // Se relance lui-même
}
};
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/// code
myHandler = new Handler();
myHandler.postDelayed(myRunnable, 1000);
}
Un objet dérivé de Handler est déclaré dans l'activité de l'UI par exemple comme ceci :
class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
bar.incrementProgressBy(5);
if (bar.getProgress() == 100) boutonStart.setEnabled(true);
}
}
Ici on y surcharge la fonction void handleMessage(Message msg) qui traite les messages reçus pour faire avancer un ProgressBar. On déclarer un Handler h par
MyHandler h = new MyHandler();
et on s'en sert dans le background_thread pour envoyer un message anonyme obtenu par Message.obtain(). Le message est envoyé par :
h.sendMessage(Message.obtain());
Lorsque le Handler h reçoit le message au travers de la méthode handleMessage(Message msg) qui s'exécute dans l'UI_thread, il fait avancer le ProgressBar bar, et lorsque le progressBar atteint son max, on prépare une nouvelle action.
J'ai essayé de déclarer en global un message non anonyme par :
Message msgON = Message.obtain();
et l'ai utilisé dans le background_thread par h.sendMessage(h.msgON) à la place du message anonyme généré par h.sendMessage(Message.obtain()). Et dans la méthode handleMessage j'ai mis le test :
if(msg == msgON) bar.incrementProgressBy(5);
pour n'incrémenter qu'à la réception de ce message. Cela marche sur le simulateur et pas du tout sur le smarphone, comme si le message était consommé, et détruit ensuite, ce qui fait que l'employer dans une boucle pour signaler un avancement ne marche pas.
Remarque : il y a d'autres variantes de la méthode sendMessage.
Apparemment plus complexe, j'ai trouvé leur utilisation plus souple. On déclare un handler global de base dans l'activité :
Handler h = new Handler();
et pour chaque action que l'on veut synchroniser entre le background_thread et l'UI_thread, on déclare un Runnable, comme par exemple :
Runnable bprog = new Runnable(){ @Override public void run() { bar.incrementProgressBy(20);}};
Runnable bfin = new Runnable(){@Override public void run() { boutonStart.setEnabled(true);}};
Les méthodes run() seront exécutées au niveau de l'UI_thread après qu'au niveau du background_thread on les invoque par :
h.post(bprog);
ou
h.post(bfin);
Simple et efficace.
Pour synchroniser des threads on peut utiliser des méthodes de type synchronized utilisant wait() et notify(). Ci-après un exemple en java standard construit à partir d'exemples du chapitre 11 de "Programmer en java" de Claude Delannoy.
// Classe contenant la routine de traitement a exécuter. Ici impression d'un nombre
class Traitement
{ // Mémorisation des paramètres du traitement
int n;
// La routine de traitement débute par wait() pour attente déblocage
public synchronized void execute() throws InterruptedException
{ wait(); System.out.println ("merci pour " + n) ;}
// Reception des paramètres du traitement puis déblocage
public synchronized void debloque(int n) throws InterruptedException
{ this.n = n; notifyAll() ;}
}
// Thread faisant un appel en boucle infini du l'execution du traitement
class MyThread extends Thread {
Traitement c; // Memo classe contenant le traitement a effectuer
public MyThread(Traitement c) {this.c = c;}
// Appel en boucle infini du l'execution du traitement
public void run()
{ try {while(!isInterrupted()) c.execute() ;} catch (InterruptedException e) {} }
}
// Exemple d'utilisation
public class MyTHreadDemo {
public static void main (String[] args)
{
int n = 0;
Traitement work = new Traitement();
MyThread myThread = new MyThread(work);
myThread.start();
// Tant que le nombre lu est > 0 on le traite.
while(true)
{
System.out.println ("donnez un entier") ;
n = Clavier.lireInt() ;
if (n <= 0) break;
try { work.debloque(n); } catch (InterruptedException e) {e.printStackTrace();}
}
myThread.interrupt(); // Demande de terminaison au thread
}
}
Les méthodes wait(), notify() et notifyAll() sont appellées à l'intérieur d'une méthode de type
synchronized, d'un objet d'une certaine classe qui joue le rôle de verrou (ci-dessus l'objet work de la classe Traitement) :
•L'appel work.wait() met le thread en cours en attente.
•L'appel work.notifyAll() (ou work.notify() ) va débloquer un (ou tous les) thread(s) qui a(ont) été mis en attente par un work.wait().
Une méthode synchronized d'un objet x ne peut être interrompu par une autre méthode synchronized du même objet x. Il y a exclusion mutuelle de l'accès à x pour ces méthodes, même si wait() et/ou notify() ne sont pas utilisés.
On peut limiter le blocage à une petite portion du code d'une méthode. Au lieu de la déclarer synchronized, on utilise un bloc du type :
synchronized(this) {instructions en exclusin mutuelle}
Le verrou porte ici sur l'objet this, mais on peut le faire porter sur un objet quelconque.
L'utilisation d'une classe étendant la classe AsyncTask est un moyen efficace de synchroniser un thread d'arrière plan avec l'UI_thread. Voici le schéma de mise en oeuvre :
class MyAsyncTask extends AsyncTask<T1, T2, T3>
{
@Override
protected void onPreExecute() // Méthode faculative
{/*Exécuté dans l'UI_thread avant l'exécution du back_thread */}
@Override
protected T3 doInBackground(T1... t1) // Méthode obligatoire renvoie type T3
{ /*Partie exécutée ds le background_thread. Travaille sur t1[0], t1[1], ..*/
boucle { ...; publishProgress(t2); ...} // publishProgress(T2)
return(t3); // Valeur renvoyée du type T3
}
@Override
protected void onProgressUpdate(T2... t2) // Méthode faculative
{/*Exécuté dans l'UI_thread après chaque appel de publisProcess(t2)*/}
@Override
protected void onPostExecute(T3 t3) // Méthode faculative
{/*Exécuté dans l' UI_thread après l'execution du background_thread*/}
}
La déclaration AsyncTask<T1, T2, T3> définit les 3 types suivants :
•T1 : Types pris en argument par la méthode doInBackground(T1... t1) (... : ellipse, cad avec un nombre d'arguments de type T1 variable, éventuellement aucun) exécutée dans le thread en arrière plan. Cette méthode est grossièrement la méthode run() du thread en arrière plan.
•T2 : La méthode doInBackground peut utiliser la méthode publishProgress(t2a,t2b,..) pour signaler son avancement à l'UI_Thread. Ces arguments sont reçus dans l'UI_thread par la méthode onProgressUpdate(T2... t2).
•T3 : Type de l'objet renvoyé par la méthode T3 doInBackground. Il est pris en argument par la méthode onPostExecute(T3 t3) qui est exécutée dans l'UI_thread à la fin de l'éxécution du background_thread.
Des quatre méthodes onPreExecute,doInBackground, onProgressUpdate et onPostExecute seule la définition de la méthode doInBackground est obligatoire.
L'ensemble est mis en oeuvre par l'appel de la méthode execute() sur l'AsyncTask :
new MyAsyncTask().execute();
Le déroulement des tâches est le suivant :
1.La méthode void onPreExecute() si elle est définie sera exécutée en premier au niveau de l'UI_Thread.
2.Le background_thread déroule la méthode T3 doInBackground(T1... t1).
3.La méthode void onProgressUpdate(T2... t2) si elle est définie sera exécutée au niveau de l'UI_thread en parallèle au background_thread, synchronisée par les d'appels de la méthode void publishProgress(T2... t2) dans la méthode doInBackground.
4.La méthode void onPostExecute(T3 t3) si elle est définie sera exécutée en dernier au niveau de l'UI_Thread après la terminaison de la méthode T3 doInBackground(T1... t1). Elle prend en argument la valeur renvoyée par cette méthode.
Lorsqu'on tourne le smartphone, l'activité qui affiche est détruite et recrée pour la nouvelle orientation. Certaines activités se reconfigurent et d'autres pas. Quand elles se reconfigurent c'est généralement pour offrir une meilleure disposition de l'UI. Par exemple des boutons sont disposés horizontalement en mode paysage et verticalement en mode portrait.
Le comportement de l'activité en fonction de l'orientation du smarthphone (ou du fait qu'on a déployé le clavier) dépend de deux attributs qu'on peut ajouter à l'intérieur de la balise <activity... ICI ...> du fichier AndroidManifest.xml.
1) android:screenOrientation="xxx" où xxx peut avoir les valeurs suivantes :
•portrait, landscape, reversePortrait, reverseLandscape : L'orientation citée est imposée (pour les 3 dernières, si le smartphone en est capable). Ainsi la tâche ne sera pas arrêtée quand on bouge l'appareil, la vue restant inchagée à la valeur choisie.
•unspecified ou attribut absent : la vue est fonction du capteur de gravité et/ou du déploiement du clavier.
•sensor : une des deux orientations portrait ou landscape sera choisie en fonction du capteur de gravité.
•fullsensor : une des quatre orientations portrait, landscape, reversePortrait, ou reverseLandscape sera choisie en fonction du capteur de gravité.
•sensorPortait : une des deux orientations portrait ou reversePortait sera choisie en fonction du capteur de gravité.
•sensorLandscape : une des deux orientations landscape ou reverseLandscape sera choisie en fonction du capteur de gravité.
•nosensor : gestion par la programmation, mais sans prise en compte du capteur de gravité.
2) android:configChanges="yyy|zzz" ou "yyy" et "zzz" sont deux changements sur le smartphone que l'activité va gérer elle-même. Dans notre cas ce sera "orientation|keyboardHidden" (sans espace avant et après le "|".
Lorsqu'on ne spécifie aucun des paramètres orientation ou keyboardHidden pour l'attribut android:configChanges de la balise <activity...> du fichier AndroidManifest.xml, on peut quand même adapter l'écran de l'activité à l'orientation. Pour cela, on peut définir dans le répertoire res, les fichiers xml layouts pour le mode portrait qui seront mis dans le répertoire res/layout et les fichiers xml layouts pour le mode paysage qui seront mis dans le répertoire res/layout-land (avec les mêmes noms). Si ce dernier répertoire n'existe pas, ce sont ceux de res/layout qui seront utilisés dans les deux cas, mais dans tous les cas, si on tourne l'appareil, l'activité est stoppée, puis relancée en rechargeant à nouveau les ressources du répertoire adéquat. En effet, les ressources précédemment chargées, ainsi que tous les résultats précédemment construits sont perdus.
Dans la balise activity, on ajoute l'attribut android:configChanges="orientation|keyboardHidden" pour spécifier qu'on va gérer l'orientation par programmation, ce qui fait qu'a priori les résultats de nos calculs sont préservés et n'ont pas à être sauvegardés, mais cependant il faut faire attention aux ressources chargées qui dépendent des orientations car elles risquent d'être perdues.
Pour gérer l'orientation on surcharge simplement la méthode onConfigurationChanged qui est appelée au début de l'activité, et après tout changement d'orientation. Exemple :
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LinearLayout container=(LinearLayout)findViewById(R.id.container);
if (newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE) {
container.setOrientation(LinearLayout.HORIZONTAL);
}
else {
container.setOrientation(LinearLayout.VERTICAL);
}
}
Lors d'un changement d'orientation qui se traduit par un changement d'affichage, l'activité qui affiche est détruite puis recrée. Si elle a auparavant lancé un thread en arière plan qui est toujours actif lors de ce changement, il risque d'envoyer ses résultats à une activité qui n'existe plus et la nouvelle activité n'en saura rien. Il faut donc gérer l'éventualité de ce changement, si on ne veut pas tout recommencer à zéro.
Si on peut déclarer statique tous les éléments dont l'affichage est à rafraichir, il n'y a plus de problème. Il suffit de mettre ces éléments à jour depuis le thread d'arrière plan de manière classique via un handler ou via la méthode onProgressUpdate(..) d'une AsyncTask.
Si ces éléments dépendent d'une instance de la classe activité, ils ne sont plus accessibles après sa destruction. Il faut donc mettre à jour les éléments de la nouvelle instance, ce qui veut dire qu'il faut accéder à cette nouvelle instance et pas à l'ancienne.
Dans le cas d'une AsyncTask, Si elle a été créée en tant que tache interne classique (non statique), elle accède de manière directe aux membres de l'instance de sa classe conteneur, ce qui peut être un sujet d'erreur lorsque celle_ci a été détruite. Il est alors conseillé de la déclarer static comme cela il aura moins de risque d'erreur (car alors l' AsyncTask n'a pas d'accès direct aux membres de l'instance conteneur). Pour accéder à cette instance, le plus simple est de mettre la classe conteneur en membre de sa classe fille et de passer l'instance par le constructeur lors de la première création et de la mettre à jour lors de sa re-création. Quand à l' AsyncTask, la classe conteneur peut la créer ou la récupèrer en utilisant par exemple le mécanisme de RetainNonConfigurationInstance dans le onCreate de l'activité :
task = (XXXAsyncTask) getLastNonConfigurationInstance();
if (task == null){ task = new XXXAsyncTask(this); task.execute(); }
else { task.activity = this; .... }
et ne pas oublier de surcharger la méthode onRetainNonConfigurationInstance() pour la sauvegarder lors de la destruction suite à un changement quelconque :
@Override
public Object onRetainNonConfigurationInstance() { return(task);}
Pour de plus amples informations, lire :
http://developer.android.com/training/basics/data-storage/files.html
La mémoire de stockage est divisée en trois types :
1 - La mémoire interne privée qui dans le cas d'un package s'appelant fr.llibre.toto se trouve en /data/data/fr.llibre.toto/files. Le File sur ce répertoire privé peut être obtenu au niveau de l'application par la méthode getFilesDir(). Ce répertoire et son contenu est détruit à la désintallation de l'application. Le contenu est inaccessible par les autres applis, sauf en lecture si certains fihiers ont été créés en mode MODE_WORLD_READABLE ce qui est dépréciée et déconseillé depuis Android 4.4 (?).
2 - La mémoire externe privée qui dans le cas d'un package s'appelant fr.llibre.toto se trouve par exemple en /storage/emulated/0/Android/data/fr.llibre.toto/files. Le File sur ce répertoire privé peut être obtenu au niveau de l'application par la méthode getExternalFilesDir(null). Le répertoire et son contenu sont accessibles par les autres applis, mais tout est détruit à la désintallation de l'application. Des sous-répertoires spécifiques sont définis dans ce répertoire. Leurs File sont obtenus par getExternalFilesDir(String type). Les types suivants sont prévus :
Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MOVIES
Les répertoires renvoyés sont les suivants (sous le repertoire renvoyé avec null) :
Music, Podcasts, Ringtones, Alarms, Notifications, Pictures, Movies.
3 - La mémoire externe publique. Son répertoire de base, dans mon cas , est le répertoire /storage/emulated/0. Le File sur ce répertoire public est obtenu par Environment.getExternalStorageDirectory(). Ce répertoire et son contenu sont accessibles par les autres applis en lecture et écriture. Elles peuvent donc détruire les contenus. Ces éléments survivent à la désintallation de l'application. Des sous-répertoires spécifiques sont définis dans ce répertoire. Leurs File sont obtenus par Environment.getExternalStoragePublicDirectory(String type). Les types sont les mêmes que précédemment avec en plus Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_DCIM.
On peut créer des arborescences de sous-répertoires sous ce répertoire public de base et y écrire des fichiers.
REMARQUE. Il semble que depuis une API récente (19 environ) on ne puisse plus accéder à la carte externe supplémentaire en écriture en mode usager (le seul mode normalement autorisé). Par contre on peut y accéder en lecture. Pour profiter de l'espace mémoire de cette carte, on peut créer les fichiers dans la mémoire externe publique, puis à l'aide de la liaison USB avec un pc, les copier sur le pc, puis les recopier sur la carte externe qui est encore accessible en écriture depuis le pc par la liaison USB.
Le File de ce répertoire privé peut être obtenu au niveau de l'application par la méthode getFilesDir().
Les méthodes :
-FileInputStream openFileInput(String name)
-FileOutputStream openFileOutput(String name, int mode);
permettent d'ouvrir un fichier dans ce répertoire. L'entier mode offre les 4 options qui sont :
Context.MODE_PRIVATE : accès limité à notre application
Context.MODE_WORLD_READABLE : lisible par tout le monde
Context.MODE_WORLD_WRITEABLE : éditable par tout le monde (dépréciée à partir d'Android 4.4 ?)
Context.MODE_Append : accès en ajout
Exemple d'ouverture d'un fichier de sortie dans ce répertoire :
try {
FileOutputStream fos;
fos = openFileOutput("bonjour.txt", Context.MODE_WORLD_READABLE);
try {
fos.write("Salut Michel");
fos.close();
} catch (IOException e) {e.printStackTrace();}
} catch (FileNotFoundException e) {e.printStackTrace();}
Autre exemple :
try {
final String TESTSTRING = new String("Hello Android");
FileOutputStream fOut = openFileOutput("samplefile.txt", MODE_WORLD_READABLE);
OutputStreamWriter osw = new OutputStreamWriter(fOut);
osw.write(TESTSTRING);
osw.flush();
osw.close();
// et on relie le fichier pour vérifier
FileInputStream fIn = openFileInput("samplefile.txt");
InputStreamReader isr = new InputStreamReader(fIn);
char[] inputBuffer = new char[TESTSTRING.length()];
isr.read(inputBuffer);
String readString = new String(inputBuffer);
boolean isTheSame = TESTSTRING.equals(readString);
Log.i("File Reading stuff", "success = " + isTheSame);
} catch (IOException ioe) {ioe.printStackTrace();}
On peut copier ces fichiers sur le pc par la commandes ADB suivante :
>adb pull /data/data/fr.llibre.toto/files/bonjour.txt
qui copie le fichier bonjour.txt du répertoire privé vers le répertoire courant sur le pc. Pour cela il faut qu'il ait été crée dans le mode Context.MODE_WORLD_READABLE
La commande suivante :
>adb push new_file.txt /data/data/fr.llibre.toto/files/new_file.txt
qui permettait d'envoyer le fichier new_file.txt situé dans le répertoire courant du pc vers le répertoire privé de l'application dans le smartphone ne marche plus depuis la version 4.4 (Permission denied).
Exemple de lecture par l'application d'un fichier se trouvant dans cette zone :
try {
FileInputStream fin;
fin = openFileInput("new_file.txt");
DataInputStream rd = new DataInputStream(fin);
texte += "\nLecture fichier new_files.txt";
try {
while(null != (ligne = rd.readLine())) texte += "\n" + ligne;
fin.close();
} catch (IOException e) {e.printStackTrace();}
} catch (FileNotFoundException e) {e.printStackTrace();}
Principales méthodes :
File getFilesDir() : Renvoie une instance File du répertoire privé.
File getDir(String name, int mode) : Récupère un File sur un répertoire existant ou crée dans la zone privée. mode vaut Context.MODE_PRIVATE ou Context.MODE_WORLD_READABLE ou Context.MODE_WORLD_WRITABLE.
boolean deleteFile(String name) : Détruit le fichier name du répertoire privé
String[] fileList() : Liste des fichiers du répertoire privé (équivalent de getFilesDir().list()).
Le File sur ce répertoire privé peut être obtenu au niveau de l'application par la méthode getExternalFilesDir(null). Le répertoire et son contenu sont accessibles par les autres applis, mais tout est détruit à la désintallation de l'application. Des sous-répertoires spécifiques sont définis dans ce répertoire. Leurs File sont obtenus par getExternalFilesDir(String type). Les types suivants sont prévus :
Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MOVIES.
On peut utiliser le répertoire cache pour créer de petits (< 1 Mb) fichiers. On obtient un File sur ce répertoire par la méthode :
File FilegetCacheDir()
qui pour le package fr.llibre.toto renvoi le répertoire : /data/data/fr.llibre.toto/cache.
Le File sur ce répertoire public (/storage/emulated/0 dans mon cas) est obtenu par Environment.getExternalStorageDirectory().
Pour savoir si le smartphone possède un stockage externe public disponible, on peut utiliser la fonction Environment.getExternalStorageState() qui renvoie un string précisant la disponibilité de ce stockage. On pourra par exemple utiliser le code suivant :
/* Test si l'external storage est disponible en écriture */
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* Test si l'external storage est disponible au moins en lecture */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
Savoir si ce stockage est démontable (API >= 21) :
boolean isExternalStorageRemovable ()
Savoir si ce stockage est émulé :
boolean isExternalStorageEmulated ()
Dans mon cas (/storage/emulated/0) la réponse est true.
Il peut exister plusieurs stockages partagés (si plusieurs utilisateurs en particulier).A partir de l'API 21, la même fonction avec le nom d'un répertoire en argument, permet de savoir si ce stockage est dispo.
String state = Environment.getExternalStorageState(File path); // API ≥ 21
Le string renvoyé peut prendre les valeurs suivantes : MEDIA_UNKNOWN, MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_CHECKING, MEDIA_NOFS, MEDIA_MOUNTED, MEDIA_MOUNTED_READ_ONLY, MEDIA_SHARED, MEDIA_BAD_REMOVAL, or MEDIA_UNMOUNTABLE.
Avant l'API 21 et après l'API 19 cette fonction s'appelait :
String state = Environment.getStorageState(File path); // 19 ≤ API < 21
dépréciée à partir de l'API 21.
De même, à partir de l'API 21 on peut savoir si ces stockages secondaires sont démontables ou émulées :
boolean isExternalStorageRemovable (File path)
boolean isExternalStorageEmulated (File path)
Remarque :
Sur mon smartphone /storage/emulated/0 et /storage/emulated/legacy sont 2 montages de la même zone mémoire. Par ailleurs /sdcard et /mnt/sdcard0 sont deux liens vers ce dernier. Ces 4 appellations concernent donc le même contenant (et contenu).
Depuis Android 4.4 l'accès à la carte externe est très difficile pour les applications non système. Code pour obtenir le path de la carte externe s'il y en a une :
/**
* @return Chemin vers la carte SD externe si elle existe ou émulée si n'existe pas
*/
public File getExternalSDCardDirectory()
{
File sdcard = Environment.getExternalStorageDirectory(); // -> /storage/emulated/0
String[] directories = getStorageDirectories(); // Renvoi /storage/emulated/0 et /storage/extSdCard
if(directories.length > 1) for (String s : directories) {
if (!s.equals(Environment.getExternalStorageDirectory().getAbsolutePath())) sdcard = new File(s);
}
else if(directories.length == 1) sdcard = new File(directories[0]);
return sdcard;
}
/**
* Returns Tous les chemins vers les cartes SD externes ou emulée
*/
public String[] getStorageDirectories()
{
// Final set of paths
Set<String> rv = new HashSet<String>();
// Primary physical SD-CARD (not emulated)
String rawExternalStorage = System.getenv("EXTERNAL_STORAGE"); // -> /storage/emulated/legacy
// All Secondary SD-CARDs (all exclude primary) separated by ":"
String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE"); // ->/storage/extSdCard
// Primary emulated SD-CARD
String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET"); // -> storage/emulated
if(TextUtils.isEmpty(rawEmulatedStorageTarget)) {// Si pas de SD_card émulée
if(TextUtils.isEmpty(rawExternalStorage)) rv.add("/storage/sdcard0"); // to default.
else rv.add(rawExternalStorage); // Device has physical external storage; use plain paths.
}
else {// External storage paths should have userId burned into them.
String rawUserId;
if(Build.VERSION.SDK_INT < 17) rawUserId = "";
else {
String path = Environment.getExternalStorageDirectory().getAbsolutePath();
String[] folders = path.split(File.separator); // -> "", "storage", "emulated","0"
String lastFolder = folders[folders.length - 1]; // -> "0"
boolean isDigit = false;
try { Integer.valueOf(lastFolder); isDigit = true; } // Teste si trouvé un chiffre
catch(NumberFormatException ignored) {}
rawUserId = isDigit ? lastFolder : "";
}
// /storage/emulated/0[1,2,...]
if(TextUtils.isEmpty(rawUserId)) { rv.add(rawEmulatedStorageTarget); }
else { rv.add(rawEmulatedStorageTarget + File.separator + rawUserId); }
}
// Add all secondary storages
if(!TextUtils.isEmpty(rawSecondaryStoragesStr))
{
// All Secondary SD-CARDs splited into array
String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator);
Collections.addAll(rv, rawSecondaryStorages);
}
return rv.toArray(new String[rv.size()]);
}
Sur mon smartphone :
System.getenv("EXTERNAL_STORAGE") retourne le string "/storage/emulated/legacy"
System.getenv("SECONDARY_STORAGE") retourne "/storage/extSdCard" (peut en retourner plusieurs séparés par des :)
System.getenv("EMULATED_STORAGE_TARGET") retourne "/storage/emulated"
Remarque : Sur mon smartphone /mnt/extSdCard est un lien sur /storagr/extSdCard.
On peut accéder de manière standard aux fichiers et répertoires autorisés avec les méthodes java standards.
Création d'un objet File à partir du nom du fichier :
File file = new File(String filePath);
Test si c'est un répertoire ou un fichier :
file.isDirectory(); // == true si répertoire
file.isFile(); == true si fichier
Création d'un FileWriter (File en sortie) à partir d'un File :
try {
FileWriter fw = new FileWriter(file);
try {
String texte = edittxt.getText().toString(); // On récupère le texte
fw.write(texte, 0, texte.length()); // On l'écrit dans le fichier
fw.close(); // On le ferme.
txtview.setText("Ecriture fichier " + filePath + " effectuée");
} catch (IOException e)
{ txtview.setText("Echec écriture fichier " + filePath); fw.close(); break;}
} catch (IOException e) { txtview.setText("Echec ouverture fichier " + filePath);}
Création d'un FileInputStream (File en entrée) à partir du nom :
try {
FileInputStream f = new FileInputStream(filePath);
byte [] buffer = new byte[513];
try{
int n = f.read(buffer, 0, 512); // On lit 512 octets dans ce fichier
if (n >0)
{
edittxt.setText(new String(buffer, 0, n));
txtview.setText("En-tête fichier " + filePath);
}
else txtview.setText("Fichier " + filePath + " vide !");
f.close();
} catch (IOException e) { txtview.setText("Echec lecture fichier " + filePath) ;}
} catch (FileNotFoundException e)
{txtview.setText("Echec ouverture fichier " + filePath + "\n");}
RAPPELS :
L'ouverture en écriture n'est autorisée a priori que dans le répertoire local de l'application. Pour écrire en mémoire partagé, il faut mettre la ligne suivante dans le fichier appli.manifest :
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Dans les versions récentes d'Android, l'ouverture en lecture n'est autorisée que dans le répertoire local de l'application. Pour lire en mémoire partagée, il faudra mettre la ligne suivante dans le fichier appli.manifest :
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
mais cette ligne est inutile si la permission d'écriture est déjà présente.
Les permissions ont beaucoup évoluées et peuvent etre demandées à l'utilisateur depuis l'application. Voir par exemple mon appli M2JVIEW.
Les fichiers placés lors de la compilation dans le répertoire res/raw peuvent être accédés à l'exécution, en lecture seule. Exemple :
Resources myres = getResources();
InputStream myis = myres.openRawResource();
Android offre des méthodes pour sauvegarder des données d'une application et les restaurer lors d'une nouvelle exécution, sous forme de paires (clé, valeurs) dans des fichiers *.xml du répertoire /data/data/fr.llibre.toto/shared_prefs/ dans le cas d'une application du package fr.llibre.toto.
La sauvegarde où la restauration utilise une instance, nommée par exemple myPrefs, de la classe SharedPreferences du package Context. Suivant la méthode utilisée pour créer cette instance on accède à différents types de fichiers de préférences :
Cas 1:
SharedPreferences myPrefs = getSharedPreferences("Prefs_MLL",Context.MODE_PRIVATE);
(méthode de la classe Activity). On choisit de donner un nom particulier au fichier de préférences, ici Prefs_MLL, ce qui permet d'en avoir plusieurs différents. Dans ce cas le fichier de préférences aura pour nom complet :
/data/data/fr.llibre.toto/shared_prefs/Prefs_MLL.xml
Le second argument :
- Context.MODE_PRIVATE interdit son accès aux autres activités,
- Context.MODE_WORLD_READABLE autorise l'accès en lecture et
- Context.MODE_WORLD_WRITABLE autorise l'accès complet.
Cas 2:
SharedPreferences myPrefs = getPreferences(Context.MODE_PRIVATE);
qui est une méthode de la classe Activity. C'est un raccourci pour getSharedPreferences("MyActivity",Context.MODE_PRIVATE); en supposant que MyActivity soit le nom donné à l'Activity. Il en résulte que le fichier de préférences aura pour nom :
/data/data/fr.llibre.toto/shared_prefs/MyActivity.xml
Remarques : Dans tous les cas tester si myPrefs n'est pas null avant de s'en servir.
En fait il n'est pas nécessaire de connaître l'existence du fichier, car on n'y accède pas directement.
Les données sont sauvegardées sous la forme d'un couple (clé, valeur) par l'intermédiaire d'une instance d'un SharedPreferences.Editor. Exemple :
SharedPreferences.Editor myPrefEdit = myPrefs.edit();
myPrefEdit.putString("USERNAME","mike");
myPrefEdit.putInt("TAILLE",183);
myPrefEdit.commit(); // Indispensable pour provoquer l'enregistreement effectif.
Si la clé n'existait pas elle est crée.
On récupère les valeurs de ces clés directement comme ceci :
String name = myPrefs.getString("USERNAME",null);
int val = myPrefs.getInt("TAILLE",0);
où le deuxième argument est une valeur à utiliser par défaut en l'absence du couple (clé,valeur).
On peut aussi récupérer l'ensemble des paramètres comme suit :
Map <String, ?> map = myPrefs.getAll();
et ensuite accéder aux valeurs comme ceci :
if (map.containsKey("USERNAME")) nom = (String) map.get("USERNAME"); else nom = "";
if (map.containsKey("TAILLE")) taille = (Integer) map.get("TAILLE"); else taille = 0;
Voici par exemple le contenu généré après le commit() le fichier /data/data/fr.llibre.preferences/shared_prefs/MyPrefs.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="TAILLE" value="183" />
<string name="USERNAME">mike</string>
</map>
Listener de modification de paramètre :
Pour être averti d'un changement effectué sur une des valeurs des paramètres du fichier des préférences, changement effectué de l'extérieur par exemple, ou depuis une autre sous-Activité, on peut utiliser un listener qui appelle une callback de surveillance que l'on définira dans onCreate(Bundle b) en l'enregistrant comme suit :
myPrefs.registerOnSharedPreferenceChangeListener(this);
if (myPrefs == null) finish(); // par exemple.
(dans le cas où l'activité implémente l'interface OnSharedPreferenceChangeListener)
Dans ce cas la callback serait de la forme suivante :
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key)
{
/* traitement éventuel à effectuer : on teste si prefs est bien myPrefs
et si key correspond à un de nos paramètres et on recupère sa valeur
par exemple */
if(prefs != myPrefs) return;
if (key.equals("USERNAME")) nom = sharedPreferences.getString(key, "");
if (key.equals("TAILLE")) taille = sharedPreferences.getInt("TAILLE", 0);
}
et on peut supprimer l'enregistrement de ce listener dans onStop :
@Override
protected void onStop() {
super.onStop();
myPrefs.unregisterOnSharedPreferenceChangeListener(this);
}
Cas 3:
SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(this);
où this se refère à l'activité. Si on n'est pas dans son domaine, (dans une classe anonyme par exemple) préciser MyActivity.this.
La sauvegarde s'effectue dans le fichier :
/data/data/fr.llibre.toto/shared_prefs/fr.llibre.toto_preferences.xml
Normalement on n'a pas à utiliser ce nom de fichier pour accéder aux préférences avec les méthodes citées au paragraphe précédent. On les met à jour automatiquement à l'aide d'une PreferenceActivity MyPrefActivity dont le code source est par exemple :
package fr.llibre.toto;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class MyPrefActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.mespreferences);
}
}
Elle doit être déclarée dans le fichier AndroidManifest.xml par l'ajout de
<activity android:label="@string/app_name"
android:name="fr.llibre.toto.MyPrefActivity">
</activity>
après la déclaration de l'activité principale.
Cette PreferenceActivity est appelée par un :
startActivity(new Intent(MyActivity.this, MyPrefActivity.class));
par exemple dans la méthode onClick() d'un OnClickListener.
Son exécution va présenter à l'utilisateur un écran de dialogue que l'on définit dans le fichier ressource res/xml/mespreferences.xml et qui ressemble par exemple à ceci :
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<EditTextPreference
android:key="USERNAME"
android:title="Dialogue de saisie d'un texte"
android:summary="Nom du personnage" />
<EditTextPreference
android:key="TAILLE"
android:title="Dialogue de saisie d'un texte"
android:summary="Taille du personnage" />
</PreferenceScreen>
Dans cet exemple l'éditeur des préférences ne présente à l'utilisateur que le choix entre 2 dialogues de saisie de texte.
En sortie de cette activité le retour à l'activité principale se fait dans la méthode onResume(), où on va placer l'acquisition de tous les paramètres qui sont l'objet de choix, par exemple :
@Override
public void onResume() {
super.onResume();
// Acquisition des préférences partagées
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this)
nom = new String(prefs.getString("USERNAME", ""));
String value = prefs.getString("TAILLE", null);
taille = (value == null ? 0 : Integer.valueOf(value));
}
Remarque :
Cette troisième méthode permet d'éditer facilement des propriétés particulières comme des strings (EditTextPreference), des booléens (CheckBoxPreference), des listes (ListPreference), la sonnerie (RingtonePreference),... par contre, pour éditer un nombre, on n'a pas comme dans les 2 cas précédents la possibilité de spécifier le type entier, double ou autre, car l'EditTextPreference ne peut contenir qu'un String.
Remarque importante :
L'écriture de toutes les préférences se fait automatiquement, sans aucune programmation supplémentaire, par l'appel de la PreferenceActivity au moyen du startActivity(new Intent(MyActivity.this, MyPrefActivity.class));
La lecture des préférences se fait de manière classique, comme dans les 2 premiers cas, mais à l'aide d'une SharedPreferences myPrefs, au retour dans l'Activity dans la méthode onResume().
Si, en outre de l'enregistrement automatisé, on veut enregistrer une (ou plusieurs préférences) comme dans les 2 premiers cas avec un SharedPreferences.Editor celui-ci doit être pris sur l'instance myPrefs de SharedPreferences qui a été créé dans la méthode onResume(). Si on en crée un autre pour générer un SharedPreferences.Editor celui-ci ne fonctionne pas.
Density-independent pixel (dp) : unité de dimension à utiliser de préférence. A l'exécution ces dp seront remplacés par un nombre de pixels px donné par px = dp * (dpi / 160) où dpi est la densité de l'écran de l'appareil. Sur un écran de 160 dpi, la dimension en pixels sera exactement égale à celle précisée en dp.
Screen density : 6 densités de pixels généralisées :
•ldpi (low) ≈ 120 dpi
•mdpi (medium) ≈ 160 dpi
•hdpi (high) ≈ 240 dpi
•xhdpi (x-high) ≈ 320 dpi
•xxhdpi (xx-high) ≈ 480 dpi
•xxxhdpi (xxx-high) ≈ 640 dpi
Screen size : 4 tailles d'écran généralisées :
•small (≈ 3.5") en 426dp x 320dp au minimum
•normal (≈ 4") en 470dp x 480dp au minimum
•large 4.5 à 7" en 640dp x 480dp au minimum
•xlarge 7" à 10" en 960dp x 720dp au minimum
Dans le code java, on a accès à la taille généralisée de l'appareil utilisé par exemple par :
if(getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_LARGE) == Configuration.SCREENLAYOUT_SIZE_LARGE) { // c'est un gran écran}
La classe DisplayMetrics donne accès aux dimensions en pixel et à la densité.
Orientation : landscape ou portrait
Pour rendre une application indépendante de la taille de l'écran de l'appareil sur lequel elle sera exécutée, il y intérêt à définir les tailles de tous les objets graphiques en utilisant des dp (device-indépendent pixel) et des polices de texte en utilisant des sp (scale-independent pixel ).
Pour limiter le type d'appareils compatible avec notre application on peut mettre dans l'AndroidManifest.xml la liste des appareils supportés sous la forme suivante :
<supports-screens android:resizeable=["true"| "false"]
android:smallScreens=["true" | "false"]
android:normalScreens=["true" | "false"]
android:largeScreens=["true" | "false"]
android:xlargeScreens=["true" | "false"]
android:anyDensity=["true" | "false"]
android:requiresSmallestWidthDp="integer"
android:compatibleWidthLimitDp="integer"
android:largestWidthLimitDp="integer"/>
en se limitant généralement aux lignes pour lesquelles on met "true".
Un fichier de configuration xml (par ex mymain.xml) mis dans res/layout est a priori adapté à la taille standard. Un fichier mymain.xml mieux adapté à la taille taille peut être fourni dans le répertoire res/layout-taille/ où le mot taille est à remplacer par une des 4 tailles génériques small, normal, large, large-land ou xlarge.
Depuis Android 3.2 (API level 13) ces 4 groupes de tailles pour spécifier des répertoires spécifiques sont obsolètes et sont remplacés par res/layout-w<N>dp, res/layout-h<N>dp, ou res/layout-sw<N>dp où sera mis le fichier mymain.xml précisant, la largeur (cas w), la hauteur (cas h) ou la plus petite des 2 dimensions (cas sw) <N> requise pour la mise en page de notre appli. Par exemple si elle nécessite 600dp en largeur ou en hauteur, on placera notre fichier dans res/layout-sw600dp.
Un fichier bitmap (jpg, png, gif) fourni dans res/drawable sera adapté aux différentes densités mais pour améliorer l'aspect on peut fournir des bitmaps adaptés à différentes densités dans les répertoires res/drawable-densité/ ou densité est à remplacer par ldpi, mdpi, hdpi, xhdpi et xxhdpi, et uniquement pour l’icône de lancement xxxhdpi.
Si on veut qu'un bitmap ne soit pas dimensionné, il faut le fournir dans res/drawable-nodpi.
Voir sur https://developer.android.com/guide/practices/screens_support.html
QVGA : 320x240
WQVGA : 400x240
HVGA : 480x320
VGA : 640x480
WVGA : 768x480
FWVGA : 854x480
SVGA : 800x600
DVGA : 960x640
WSVGA : 1024x576 ou 1024x600
XGA : 1024x768
WXGA : 1280x800 ou 1366x768
FWXGA : 1366x768
XGA+ : 1152x864
WXGA+ : 1440x900
WSXGA+ : 1680x1050
UXGA :1600x1200
WUXGA : 1920x1080
QWXGA : 2048x1152
QXGA : 2048x1536
WQXGA : 2560x1600
QSXGA : 2560x2048
WQSXGA : 3200x2048
QUXGA : 3200x2400
WQUXGA : 3840x2400
etc.…
Voici les capteurs utilisables avec android :
Capteur |
Type |
Description |
TYPE_ACCELEROMETER |
Hard |
Acceleration totale en m/s2 incluant la gravité (VecGamma – VecG). |
TYPE_AMBIENT_TEMPERATURE |
Hard |
Temperature en degrés Celsius (°C). |
TYPE_GRAVITY |
Soft or Hard |
Force de gravité (-VecG) en m/s2. Identique à TYPE_ACCELEROMETER si VecGamma est nul. |
TYPE_GYROSCOPE |
Hard |
Measures a device's rate of rotation in rad/s around each of the three physical axes (x, y, and z). |
TYPE_LIGHT |
Hard |
Measures the ambient light level (illumination) in lx. |
TYPE_LINEAR_ACCELERATION |
Soft or Hard |
Measures the acceleration force in m/s2 that is applied to a device on all three physical axes (x, y, and z), excluding the force of gravity. |
TYPE_MAGNETIC_FIELD |
Hard |
Measures the ambient geomagnetic field for all three physical axes (x, y, z) in μT. |
TYPE_ORIENTATION |
Soft |
Measures degrees of rotation that a device makes around all three physical axes (x, y, z). As of API level 3 you can obtain the inclination matrix and rotation matrix for a device by using the gravity sensor and the geomagnetic field sensor in conjunction with the getRotationMatrix() method. |
TYPE_PRESSURE |
Hard |
Measures the ambient air pressure in hPa or mbar. |
TYPE_PROXIMITY |
Hard |
Measures the proximity of an object in cm relative to the view screen of a device. This sensor is typically used to determine whether a handset is being held up to a person's ear. |
TYPE_RELATIVE_HUMIDITY |
Hard |
Measures the relative ambient humidity in percent (%). |
TYPE_ROTATION_VECTOR |
Soft or Hard |
Measures the orientation of a device by providing the three elements of the device's rotation vector. |
TYPE_TEMPERATURE |
Hard |
This sensor implementation varies across devices and this sensor was replaced with the TYPE_AMBIENT_TEMPERATURE sensor in API Level 14 |
Pour travailler avec les capteurs, il faut d'abord obtenir une référence sur le gestionnaire des capteurs :
private SensorManager mSensorManager;
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Avec cette référence on peut obtenir la liste de tous les capteurs présents sur l'appareil par :
List<Sensor> deviceSensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
A la place de TYPE_ALL on peut utiliser TYPE_GYROSCOPE, TYPE_LINEAR_ACCELERATION, ou TYPE_GRAVITY si on désire la liste des capteurs d'un de ces types.
La fonction getDefaultSensor(type) renvoie le capteur par défaut d'un type donné. Si elle ne renvoie rien c'est qu'il n'y a pas de capteurs de ce type :
if (mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null)
{/* Success! There's a magnetometer.*/}
else {/* Failure! No magnetometer.*/}
Exemple de tentative de choix d'un capteur d'origine Google, version 3 :
mSensor = null;
if (mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) != null){
List<Sensor> gravSensors = mSensorManager.getSensorList(Sensor.TYPE_GRAVITY);
for(int i=0; i<gravSensors.size(); i++) {
if ((gravSensors.get(i).getVendor().contains("Google Inc.")) &&
(gravSensors.get(i).getVersion() == 3)){
// Use the version 3 gravity sensor.
mSensor = gravSensors.get(i);
}}}
if (mSensor == null){
// Use the accelerometer.
if (mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null){
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
else{
// Sorry, there are no accelerometers on your device.
// You can't play this game.
}}
Les méthodes suivantes de la classe Sensor permettent d'obtenir quelques caractéristiques du capteur référencé :
float getResolution() renvoie la résolution en unités capteur
float getMaximumRange() renvoie l'étendue de mesure en unités capteur
float getPower() renvoie la consommation en mA pendant son utilisation,
int getMinDelay() en microsecondes entre 2 mesures, ou 0 si ne renvoie qu'une mesure.
On sélectionne un capteur que l'on désire utiliser, par exemple comme ceci :
Sensor mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
et on l'active, généralement dans la méthode onResume() (ou onStart()) en l'enregistrant chez le gestionnaire de capteur précédemment acquis, par exemple comme ceci :
mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
où on spécifie également la période désirée entre deux mesures. Pour un capteur d'orientation, on a approximativement les valeurs suivantes pour la priode :
•SENSOR_DELAY_FASTEST le plus rapide possible
•SENSOR_DELAY_GAME : 20000 mus (0.02 s cad 50 Hz)
•SENSOR_DELAY_UI : 60000 mus (0.06 s cad 16,7 Hz)
•SENSOR_DELAY_NORMAL 200000 mus (0.2 s cad 5 Hz)
Exemple :
@Override
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
Remarque : En mettant tous les capteurs à SENSOR_DELAY_UI, sur le Galaxy Note II, et en les interrogeant tous en permanence, j'ai mesuré les fréquences suivantes :
Accélération : 18 Hz,
Gravimètre : 18 Hz,
Champ magnétique : 50 Hz
Orientation : 18 Hz
Gyromètre : 100 Hz
Ne pas oublier de désenregistrer l'écouteur avant la suspension de l'activité, par exemple dans onStop() ou onPause() :
@Override
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this, mSensor);
}
On enregistre et on dés-enregistre comme cela tous les capteurs que l'on désire utiliser.
La mise en service se fait au moyen de deux callbacks de l'interface java SensorEventListener écrites dans la classe qui l'implémente (l'activité principale par exemple) :
•public final void onAccuracyChanged(Sensor sensor, int accuracy) : Cette callback est activée lorsque la précision du capteur s est modifiée. L'entier accuracy vaut alors une des 4 valeurs suivantes : SENSOR_STATUS_ACCURACY_LOW, SENSOR_STATUS_ACCURACY_MEDIUM, SENSOR_STATUS_ACCURACY_HIGH, ou SENSOR_STATUS_UNRELIABLE. Généralement on ne fait rien dans cette callback qui se réduit à {}.
•public final void onSensorChanged(SensorEvent event) : Cette callback est activée lorque le capteur dispose de nouvelles valeurs.
La réception des mesures se fait dans la callback onSensorChanged(SensorEvent event) {…} de l'interface java SensorEventListener présentée au paragraphe précédent. La classe SensorEvent représente un capteur et contient toutes ses informations, telle que le type, la date de mesure, la précision et bien sur les données de ce capteur. Les 4 principaux champs de SensorEvent sont :
•Sensor sensor : Le capteur qui a généré l'évènement.
•long timestamp : La date en nanosecondes de l'évènement (temps écoulé depuis la dernière mise en marche de l'appareil), valeur identique à celle qu'aurait donné System.nanoTime() au moment de la mesure. Multiplier par 1.e-9 pour l'obtenir en secondes.
•int accuracy : La précision de cet évènement (SENSOR_STATUS_ACCURACY_LOW, SENSOR_STATUS_ACCURACY_MEDIUM, SENSOR_STATUS_ACCURACY_HIGH, ou SENSOR_STATUS_UNRELIABLE).
•final float[] values() : Le tableau des mesures.
Le tableau des mesures dépend du type de capteur renvoyé par event.sensor.getType().
Exemple de reception de cet évènement et de distribution aux méthodes appropriées à le traiter :
public void onSensorChanged(SensorEvent event) {
switch(event.sensor.getType()){
case Sensor.TYPE_GRAVITY:
onGravityChanged(event);
break;
case Sensor.TYPE_ACCELEROMETER:
onAccelerometerChanged(event);
break;
case Sensor.TYPE_ROTATION_VECTOR:
onOrientationChanged(event);
break;
case Sensor.TYPE_MAGNETIC_FIELD:
onMagneticFieldChanged(event);
break;
case Sensor.TYPE_AMBIENT_TEMPERATURE:
onTemperatureChanged(event);
break;
case Sensor.TYPE_PROXIMITY:
onProximityChanged(event);
break;
case Sensor.TYPE_LIGHT:
onLightChanged(event);
break;
case Sensor.TYPE_PRESSURE:
onPressureChanged(event);
break;
case Sensor.TYPE_GYROSCOPE:
onGyroscopeChanged(event);
break;
}
}
Les mesures tri-axes sont relative à un repère lié à l'appareil avec l'axe z dirigé vers l'utilisateur, l'axe x vers sa droite et l'axe y vers le haut de l'appareil et l'origine au centre de l'écran.
Exemple de traitement des capteurs liés à l'orientation :
private void onGravityChanged(SensorEvent event){
// Composantes de la gravité (sens positif vers le haut) dans le rep. mobile
gravity[0] = event.values[0];
gravity[1] = event.values[1];
gravity[2] = event.values[2];
}
private void onMagneticFieldChanged(SensorEvent event){
// Composantes du champ magnétique (dirigé vers le Nord) ds le rep. mobile
geomagnetic[0] = event.values[0];
geomagnetic[1] = event.values[1];
geomagnetic[2] = event.values[2];
}
private void onOrientationChanged(SensorEvent event){
// Matrice d'orientation à partir du quaternion
float []Rot = new float[9] ;
SensorManager.getRotationMatrixFromVector(Rot, event.values);
// Angles yaw, pitch, Roll à partir de la matrice
float []ypr_fvr = new float[3];
SensorManager.getOrientation(Rot, ypr_fvr);
}
L'angle yaw est le cap de l'axe longitudinal à partir du Nord, positif dans le sens horaire (sens trigo autour de -Z, cf. doc. android)
L'angle pitch est l'assiette de cet axe, positive vers le bas (sens trigo autour de -X, cf. doc. Android).
L'angle roll est l'angle de roulis de l'axe transversal, sens trigo positif dansla rotation autour de l'axe longitunal Y (+ quand le coté droit descend).
Remarque : la routine SensorManager.getRotationMatrix (Rot, RI, gravity, geomagnetic) fournit la même matrice Rot calculée à partir des vecteurs gravité gravity et champ magnétique geomagnetic.
On peut retouver ce résultat par l'algorithme suivant :
double [] I = new double[3]; // Axe horizontal dirigé vers l'Est
double [] J = new double[3]; // Axe horizontal dirigé vers le Nord
double [] K = new double[3]; // Axe vertical dirigé vers le zénit
double gmod = Math.sqrt(gravity[0]*gravity[0] + gravity[1]*gravity[1] +gravity[2]*gravity[2]);
for(int n = 0; n < 3; n++) K[n] = gravity[n]/gmod;
double mk = geomagnetic[0]*K[0] + geomagnetic[1]*K[1] +geomagnetic[2]*K[2];
for(int n = 0; n < 3; n++) J[n] = geomagnetic[n] - mk*K[n];
double jmod = Math.sqrt(J[0]*J[0] + J[1]*J[1] +J[2]*J[2]);
for(int n = 0; n < 3; n++) J[n] /= jmod;
I[0] = J[1]*K[2] - J[2]*K[1];
I[1] = J[2]*K[0] - J[0]*K[2];
I[2] = J[0]*K[1] – J[1]*K[0];
double []mat = {I[0], I[1], I[2], J[0], J[1], J[2], K[0], K[1], K[2]};
float []Rot = new float[9] ;
for (int n = 0; n < 9; n++) Rot[n] = (float) mat[n];
Orientation Mobile / Fixe au moyen des angles LTR :
Ces 3 angles sont :
•le cap de Oy, compté dans le sens trigo, à partir du Nord. C'est l'opposé du yaw fourni par SensorManager.getOrientation,
•l'assiette de Oy positive vers le haut, qui est l'opposée du pitch fourni par SensorManager.getOrientation,
•le roulis autour de Oy, qui est identique au roulis autour de Oy fourni par SensorManager.getOrientation.
double cap = Math.toDegrees(Math.atan2(-I[1], J[1]));
double ass = Math.toDegrees(Math.asin(K[1])); // Positif vers le haut
double rol = Math.toDegrees(Math.atan2(-K[0], K[2]));
Si on n'a besoin que de ces 3 angles, il est inutile de calculer les matrices mat et Rot et on peut se limiter à la composante I[1], selon y de I.
Remarque : La ligne de plus grande pente est donnée par l'angle entre les vecteurs z et Z, c'est-à-dire par arcos(K[2]). On peut donner à cet angle le signe de l'assiette.
Pour améliorer la précision de ces angles il faudrait étalonner les capteurs de gravité et de champ magnétique.
Etalonnage de la gravité
Etant donné une mesure mT = (mx, my, mz) de gravité, on désire obtenir sa direction dT = (dx, dy, dz) le plus précisément possible. Pour cela on cherche à identifier la matrice de gain et biais G telle que :
d = G m
où G serait diagonale si tout était parfait. Pour l'identifier on va faire une série de n mesures m1, m2, …, mn correspondantes à une série de n directions connues d1, d2, …, dn avec n >> 3. On prendra par exemple comme directions connues les 3 directions des axes de base et les 3 directions opposées. On aura ainsi, l'identité :
[d1, d2, …, dn] = G [m1, m2, …, mn]
que l'on écrit :
D = G M avec D = [d1, d2, …, dn] et M = [d1, d2, …, dn]
Les matrice D et M sont des matrice 3×n et G est une matrice 3×3. En post-multipliant par MT, on obtient :
DMT = G MMT
où a priori MMT est une matrice régulière. D'où :
G = DMT(MMT)-1
est une solution des moindres-carrés à notre problème.
Etalonnage du champ magnétique
Il est plus complexe car le capteur est perburbé par son environnement. Il faudrait établir un modèle 3D de ces perturbations. Généralement on se limitera à évaluer une courbe 2D des erreurs de cap (référence – mesure) en fonction du cap de référence, tous les 10° de cap par exemple (36 points de mesures), et on interpolera ensuite une courbe de la correction à ajouter en fonction du cap mesuré.
Voici un exemple d'appli pour lister les capteurs disponibles :
Le fichier java du main : fr.llibre.SensorList.java :
package fr.llibre.sensorlist;
import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.ScrollView;
import android.widget.TextView;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
public class SensorList extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ScrollView asc = new ScrollView(this);
TextView tv = new TextView(this);
asc.addView(tv);
String text = "";
SensorManager gestionCapteurs = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> capteurs = gestionCapteurs.getSensorList(Sensor.TYPE_ALL);
for (int i=0; i<capteurs.size(); i++)
{
Sensor s = capteurs.get(i);
text += s.toString() + "\n"
+ "--Nom : " + s.getName() + "; version : " + s.getVersion() + "\n"
+ "--Vendeur : " + s.getVendor() + "\n"
+ "--Power : " + s.getPower() + "mA\n"
+ "--min Delay : " + s.getMinDelay() + "\n"
+ "--Resolution : " + s.getResolution() + "\n"
+ "--Etendue : " + s.getMaximumRange() + "\n"
//+ "--type : " + s.getStringType() + "\n" // Level 20
//+ "--max Delay : " + s.getMaxDelay() + "\n" // Level 21
// + "--reportMode : " + s.getReportingMode() + "\n" // Level 21
// + "--WakeUp ? " + s.isWakeUpSensor() + "\n" // Level 21
// + "--Dynamique ? " + s.isDynamicSensor() + "\n" // Level 24
;
}
tv.setText(text);
boolean sortieFich = false;
if (sortieFich)
{
String nomFich = System.getenv("EXTERNAL_STORAGE") + File.separator + "MesCapteurs.txt";
File file = new File(nomFich);
try {
FileWriter fw = new FileWriter(file);
try {
fw.write(text, 0, text.length()); // On l'écrit dans le fichier
fw.close(); // On le ferme.
text = "Ecriture éffectuée dans " + nomFich + "\n" + text;
tv.setText(text);
}
catch (IOException e)
{ tv.setText("Echec écriture fichier " + nomFich); fw.close();}
}
catch (IOException e)
{ tv.setText("Echec ouverture fichier " + nomFich);}
}
setContentView(asc);
}
}
Dans le cas du Galaxy Note II, j'obtiens le résultat suivant :
{Sensor name="LSM330DLC Acceleration Sensor", vendor="STMicroelectronics", version=1, type=1, maxRange=19.6133, resolution=0.009576807, power=0.25, minDelay=10000}
--Nom : LSM330DLC Acceleration Sensor; version : 1
--Vendeur : STMicroelectronics
--Power : 0.25mA
--min Delay : 10000
--Resolution : 0.009576807
--Etendue : 19.6133
{Sensor name="LSM330DLC Gyroscope Sensor", vendor="STMicroelectronics", version=1, type=4, maxRange=8.726646, resolution=3.0543262E-4, power=6.1, minDelay=10000}
--Nom : LSM330DLC Gyroscope Sensor; version : 1
--Vendeur : STMicroelectronics
--Power : 6.1mA
--min Delay : 10000
--Resolution : 3.0543262E-4
--Etendue : 8.726646
{Sensor name="AK8963C Magnetic field Sensor", vendor="Asahi Kasei Microdevices", version=1, type=2, maxRange=2000.0, resolution=0.06, power=6.0, minDelay=10000}
--Nom : AK8963C Magnetic field Sensor; version : 1
--Vendeur : Asahi Kasei Microdevices
--Power : 6.0mA
--min Delay : 10000
--Resolution : 0.06
--Etendue : 2000.0
{Sensor name="Orientation Sensor", vendor="AOSP", version=1, type=3, maxRange=360.0, resolution=0.00390625, power=12.35, minDelay=10000}
--Nom : Orientation Sensor; version : 1
--Vendeur : AOSP
--Power : 12.35mA
--min Delay : 10000
--Resolution : 0.00390625
--Etendue : 360.0
{Sensor name="BMP182 Barometer Sensor", vendor="BOSCH", version=1, type=6, maxRange=1000.0, resolution=1.0, power=1.0, minDelay=66700}
--Nom : BMP182 Barometer Sensor; version : 1
--Vendeur : BOSCH
--Power : 1.0mA
--min Delay : 66700
--Resolution : 1.0
--Etendue : 1000.0
{Sensor name="CM36651 Proximity Sensor", vendor="Capella Microsystems, Inc.", version=1, type=8, maxRange=5.0, resolution=5.0, power=0.75, minDelay=0}
--Nom : CM36651 Proximity Sensor; version : 1
--Vendeur : Capella Microsystems, Inc.
--Power : 0.75mA
--min Delay : 0
--Resolution : 5.0
--Etendue : 5.0
{Sensor name="CM36651 Light Sensor", vendor="Capella Microsystems, Inc.", version=1, type=5, maxRange=3000.0, resolution=1.0, power=0.75, minDelay=0}
--Nom : CM36651 Light Sensor; version : 1
--Vendeur : Capella Microsystems, Inc.
--Power : 0.75mA
--min Delay : 0
--Resolution : 1.0
--Etendue : 3000.0
{Sensor name="Screen Orientation Sensor", vendor="Samsung Electronics", version=1, type=65558, maxRange=255.0, resolution=0.0, power=0.0, minDelay=0}
--Nom : Screen Orientation Sensor; version : 1
--Vendeur : Samsung Electronics
--Power : 0.0mA
--min Delay : 0
--Resolution : 0.0
--Etendue : 255.0
{Sensor name="Rotation Vector Sensor", vendor="AOSP", version=3, type=11, maxRange=1.0, resolution=5.9604645E-8, power=12.35, minDelay=10000}
--Nom : Rotation Vector Sensor; version : 3
--Vendeur : AOSP
--Power : 12.35mA
--min Delay : 10000
--Resolution : 5.9604645E-8
--Etendue : 1.0
{Sensor name="Gravity Sensor", vendor="AOSP", version=3, type=9, maxRange=19.6133, resolution=0.009576807, power=12.35, minDelay=10000}
--Nom : Gravity Sensor; version : 3
--Vendeur : AOSP
--Power : 12.35mA
--min Delay : 10000
--Resolution : 0.009576807
--Etendue : 19.6133
{Sensor name="Linear Acceleration Sensor", vendor="AOSP", version=3, type=10, maxRange=19.6133, resolution=0.009576807, power=12.35, minDelay=10000}
--Nom : Linear Acceleration Sensor; version : 3
--Vendeur : AOSP
--Power : 12.35mA
--min Delay : 10000
--Resolution : 0.009576807
--Etendue : 19.6133
Pour pouvoir utiliser le bluetooth, il faut mettre les uses-permission suivantes dans le fichier AndroidManifest.xml, au plus haut-niveau à l'intérieur de la section <manifest> … </manifest>
<uses-permission android:name="android.permission.BLUETOOTH" />
Le mise en service du Bluetooth se fait via un BluetoothAdapter déclaré en variable de la classe Activity par :
BluetoothAdapter mBluetoothAdapter;
On définit également deux codes, avec des valeurs qcq positives qui serviront dans la méthode :
@Override
public void onActivityResult(int req, int rep, Intent data) {…}
à reconnaître quelle activité répond :
// Code choisi pour réponse à la demande de mise en service du bluetooth
private final int REQUEST_ENABLE_BT = 321;
// Code choisi pour réponse à la demande de mise en visibilité bluetooth
private final int REQUEST_DISCOVERABLE_BT = 322;
Le BluetoothAdapter sera généralement créé dans le onCreate de l'Activity par :
// Récupération d'un BluetoothAdapter.
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
Toast.makeText(this, "Bluetooth non supporté !", Toast.LENGTH_LONG).show();
finish(); }
// On examine si le bluetooth est actif, sinon on demande l'activation à
// l'utilisateur en utilisant dans l'intent le code précédemment défini.
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
Dans la méthode onActivityResult(int req, int rep, Intent data), on traitera la réponse à cette demande pour le cas if(req == REQUEST_ENABLE_BT) dont le code a été préalablement défini dans les variables de l'Activity. En particulier on tentera de réaliser des couplages avec d'autres appareils, en se déclarant disponible auprès des autres et en cherchant les autres appareils qui sont disponibles.
En utilisant en plus de la permission standard, la permission suivante :
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
on peut alors se passer de la permission de l'usager et forcer la mise en service du bluetooth par :
if (!mBluetoothAdapter.isEnabled()) {mBluetoothAdapter.enable();}
Avant d'effectuer la découverte des autres appareils, comme expliqué au paragraphe suivant, on a intérêt à interroger l'ensemble des périphériques appariés pour voir si le périphérique désiré est déjà connu, ce que l'on pourra faire avec la méthode suivante :
private void lookForKnownDevices()
{
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
String texte = "lookForKnownDevices\n Nom | Adresse Mac\n";
// Il y a des periph. appairés. Obtention de leur nom et adresse.
for (BluetoothDevice device : pairedDevices) {
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
texte += deviceName + " | " + deviceHardwareAddress + "\n";
}
Toast.makeText(this, texte, Toast.LENGTH_LONG).show();
}
else Toast.makeText(this, "Rien déjà appairé !", Toast.LENGTH_LONG).show();
}
Pour effectuer un appariement entre appareils Bluetooth, il faut se mettre à l'écoute des périphériques diffusant éventuellement leur ouverture, en mettant en service un BroadcastReceiver que l'on crééra par exemple comme suit :
// Create a BroadcastReceiver
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
// Teste si l'action reçue est la découverte d'un periph. bluetooth
String action = intent.getAction();
if (action.equals(BluetoothDevice.ACTION_FOUND))
{
BluetoothDevice device = // On recupère son descripteur
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// On imprime son nom et son adresse
String texte = "BroadcastReceiver\n Nom | Adresse Mac\n";
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
texte += deviceName + " | " + deviceHardwareAddress + "\n";
Toast.makeText(context, texte, Toast.LENGTH_LONG).show();
}
}
};
Pour lancer la découverte on appelle "startDiscovery()" et "cancelDiscovery()" permet de stopper cette recherche. En général le processus dure une grosse dizaine de secondes, suivi d'une page qui affiche les périphériques découverts.
Mais au préalable, il faut mettre en service le BroadcastReceiver ce qui se fait généralement, dans le onCreate de l'Activity, en l'enregistrant :
// Enregistrement du BroadcastReceiver pour la diffusion et découverte
IntentFilter filterBR = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filterBR);
Comme pour tout enregistrement, ne pas oublier de désenregistrer dans le onDestroy() :
unregisterReceiver(mReceiver);
Après s'être assuré que le bluetooth est activé, il faut que l'application demande à l'utilisateur l'autorisation de diffuser son ouverture pendant un certain temps pour pouvoir être découvert par les autres périphériques. Cela se fera par exemple au moyen de la méthode suivante :
private void setDiscoverable() {
// On rend notre appareil visible pendant 10 minutes (attention 0 = infini)
Intent dI = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
dI.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 10*60);
startActivityForResult(dI, REQUEST_DISCOVERABLE_BT);
}
Dans la méthode onActivityResult(int req, int rep, Intent data), on traitera la réponse à cette demande pour le cas if(req == REQUEST_DISCOVERABLE_BT) dont le code a été préalablement défini dans les variables de l'Activity. En cas d'échec, problème !! Par contre rien à faire a priori en cas de réussite. Notre appareil va passer en mode connectable_discoverable, puis à l'issue de la période de diffusion il va passer en mode connectable simple (avec les appareils avec lesquels il a déjà été appairé).
Si on veut surveiller les changements d'état du mode bluetooth, il suffit d'ajouter l'action suivante :
filterBR.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
à l'Intenfilter utilisé pour enregistrer le BroadcastReceiver mReceiver, avant de l'enregistrer :
// Enregistrement du BroadcastReceiver pour la diffusion et découverte
IntentFilter filterBR = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filterBR.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
registerReceiver(mReceiver, filterBR);
Dans la définition du BroadcastReceiver mReceiver on ajoutera le traitement de cette action, par exemple :
// Réception changement état de visibilité de notre appareil
if (action.equals(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) {
int scan_mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.SCAN_MODE_NONE);
switch (scan_mode)
{
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE :
Toast.makeText(context, "Modif visibilité : devient CONNECTABLE_DISCOVERABLE !", Toast.LENGTH_LONG).show(); break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE :
Toast.makeText(context, "Modif visibilité : devient CONNECTABLE !", Toast.LENGTH_LONG).show(); break;
case BluetoothAdapter.SCAN_MODE_NONE :
Toast.makeText(context, "Modif visibilité : devient NONE !", Toast.LENGTH_LONG).show(); break;
}
}
1) Creer un projet Android FirstNDK sous Eclipse comme d'habitude. Choisir comme nom de package fr.llibre.firsndk (ou autre chose) et comme nom d'activité FirstNDKActivity. Modifier Le fichier fr.llibre.firstndk/FirstNDKActivity.java comme suit :
package fr.llibre.firstndk;
import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;
public class FirstNDKActivity extends Activity {
// On charge la bibliothèque générée par le C
static {System.loadLibrary("mabib");}
// On déclare la fonction "getMessage" qu'on va utiliser
private native String getMessage();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText( getMessage() );
setContentView(tv);
}
}
On peut supprimer le fichier src/layout/main.xml qui ne sera pas utilisé.
2) Ajouter sous Eclipse sous le répertoire FirstNDK le répertoire jni.
3) Ajouter sous le répertoire jni le fichier Android.mk suivant :
#---- debut fichier Android.mk ---------------------
LOCAL_PATH := $(call my-dir)
#Vidange des variables utilisées
include $(CLEAR_VARS)
#Nom de notre module (donc de la bibliothéque générée)
LOCAL_MODULE := mabib
#Liste des fichiers sources utilisés
LOCAL_SRC_FILES := maCroutine.c
#Type de fichier désiré en sortie (ici bibli partagée)
include $(BUILD_SHARED_LIBRARY)
#----- Fin fichier Android.mk-------------------------
4) Ajouter sous le répertoire jni le fichier maCroutine.c suivant :
#include <jni.h>
jstring Java_fr_llibre_firstndk_FirstNDKActivity_getMessage
(JNIEnv *env, jobject thisObj)
{
return (*env)->NewStringUTF(env, "Hello from code C!");
}
5) En mode console, se placer dans le répertoire projet FirstNDK (au-dessus du répertoire jni) et exécuter la commande :
D:\MesProgs\android\ExoMll\FirstNDK>C:\android-ndk-r8e\ndk-build
On obtient la réponse suivante :
C:/android-ndk-r8e/build/core/add-application.mk:128: Android NDK: WARNING: APP_PLATFORM android-14 is larger than android:minSdkVersion 10 in ./AndroidManifest.xml
"Compile thumb : mabib <= maCroutine.c
SharedLibrary : libmabib.so
Install : libmabib.so => libs/armeabi/libmabib.so
6) Exécuter comme d'habitude avec Eclipse
1) Se mettre dans le répertoire où on range ses projets et créer le projet FirstNDK :
>android create project --name FirstNDK --path FirstNDK --activity FirstNDKActivity --package fr.llibre.firstndk --target 1
Cette commande crée le répertoire FirstNDK et tout ce qu'il faut dans ce répertoire.
1) Remplacer le fichier FirstNDKActivity.java qui se trouve dans le répertoire FirstNDK/src/fr/llibre/firstndk par le même que ci-dessus. On peut supprimer le fichier FirstNDK/res/layout/main.xml qui ne sera pas utilisé.
2) Ajouter au fichier AndroidManifest.xml les lignes suivantes, à l'intérieur de la portée de la balise manifest, avant la balise application, par exemple :
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="10" />
Mettre à la place de 10, les valeurs que vous souhaitées, 10 correspondant à la version Android 2.3.3.
En l'absence de cette balise la commande ndk-build, étape 8 produit une erreur.
4) Si le fichier build.xml n'existe pas, taper la commande suivante pour générer :
>android update project -p . -s
5) Créer sous le répertoire FirstNDK le répertoire jni :
>mkdir jni
6) Créer sous ce répertoire jni le même fichier Android.mk que ci-dessus.
7) Créer sous le répertoire jni le même fichier maCroutine.c que ci-dessus.
8) Se placer dans le répertoire projet FirstNDK (au-dessus du répertoire jni) et exécuter la commande :
>C:\android-ndk-r8e\ndk-build
9) Compiler l'application :
>ant debug
10) Lancer l'application :
>adb install bin/FirstNDK-debug.apk
Trouver sur le smartphone l'icone de l'appli et la lancer. Et voilà !!
Obsolète : {
- Au démarrage choisir : Start a New Android Studio
- Page suivante : Application Name : FirstNDK
Company Domain : llibre.fr
- Page suivante : Phone and Tablet. Minimum SDK : API10 (par exemple)
- Page suivante choisir Empty Activity
- Page suivante : Activity Name : FirstNDKActivity
Décocher Generate Layout File
Dans l'éditeur modifier le fichier fr.llibre.firstndk/FirstNDKActivity.java pour lui donner le même contenu que précédemment sous Eclipse ou en ligne de commande.
Créer le répertoire ??/FirstNDK/app/src/main/jni. On peut le faire dans Android Studion de la manière suivante : Volet d'exploration à gauche. Bandeau vertical le plus à gauche cliquer sur 1:Project pour faire apparaître le volet de navigation. Bandeau horizontal au-dessus de ce volet, la première case permet de choisir l'aspect du volet (Projet, Packages, Scratches, Android, …). sélectionner Project. Dans le volet, sous FirstNDK, dérouler app, puis src, puis main qui fait apparaître java, res et AndroidManifest.xml. Faire un clic droit sur main et choisir new / Directory et lui donner jni comme nom.
Comme pour Eclipse en la ligne de commande, mettre dans ce répertoire les deux fichiers maRoutine.c et Android.mk. Ceci peut être fait à l'aide de l'éditeur d'Android Studio.
Editer le fichier gradle.proporties et y ajouter la ligne suivante :
android.useDeprecatedNdk=true
Editer le fichier local.properties et y ajouter la ligne suivante :
ndk.dir=C\:\\Android\\android-ndk-r10e
qui précise le répertoire où se trouve l'Android NDK (modifier en fonction de votre installation).
Editer le fichier ??/FirstNDK/app/build.gradle (ATTENTION, il y a un au-dessus, ??/FirstNDK/build.gradle, ce n'est pas celui-là) et y ajouter, dans la rubrique defaultConfig {…}, par exemple à la fin, ndk {moduleName "mabib"}. Si on oublie, pas de message d'erreur de la part d'Android Studio, mais l'appli ne marche pas !
Compiler le projet (Build/ReBuild Project ou File/Synchronize) et tester : Run/Run 'app'.
}
DANS LA NOUVELLE VERSION DE STUDIO :
"Start a new Android Studio project" et sur la 1ère fenêtre (nom, etc.) cocher la case "Include C++ support", et choisir le type d'activité voulue.
Le système génère une appli toute prête qui appelle une routine C++ qui fournit un nom qui sera affiché dans un label.
Les fichier java sont à mettre dans le répertoire xxx/app/src/main/java/fr/llibre/nomappli et les fichiers C/C++ sont à mettre dans xxx/app/src/main/cpp. Dans ce répertoire les fichiers *.cpp sont compilés en C++ et les fichier *.c sont compilés en C.
Comme on le voit dans le fichier C, le patron de la routine appelée depuis java est relativement complexe. On peut générer un fichier entête *.h qui fournit ce patron et on pourra l'inclure dans le fichier *.c pour éviter les erreurs. Sous l'arborescence Eclipse, ce fichier entête est généré, depuis le répertoire projet, par la commande suivante :
>javah -o jni/monentete.h -classpath C:/Applis/android-sdk/platforms/android-19/android.jar;bin/classes fr.llibre.firstndk.FirstNDKActivity
l'option -o permet de donner un nom au fichier entête, et l'option -classpath permet de préciser le répertoire où se trouve la classe. Le nom de cette classe doit être précédé du package.
Voici le fichier généré :
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class fr_llibre_firstndk_FirstNDKActivity */
#ifndef _Included_fr_llibre_firstndk_FirstNDKActivity
#define _Included_fr_llibre_firstndk_FirstNDKActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: fr_llibre_firstndk_FirstNDKActivity
* Method: getMessage
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_fr_llibre_firstndk_FirstNDKActivity_getMessage
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Les attributs JNIEXPORT et JNICALL sont de l'habillage sans effet. Le nom de la méthode getMessage est précédé par Java_<nom_du_package>_nomClasse_, avec dans nom_du_package des "_" à la place des points.
Sous l'arborescence Android Studio, voici la ligne de commande qui a marché :
javah -o src/main/jni/entete.h -bootclasspath %ANDROID_HOME%/platforms/android-10/android.jar -classpath ./build/intermediates/classes/debug fr.llibre.firstndk.FirstNDKActivity
Pour faire marcher un des examples du ndk sous Eclipse :
1) Charger le projet sous Eclipse :
New /Project... /Android /Android Project From Existing Code, Next/Root Directory, Browse / Select (un ou tous), Finish.
Properties /Android / Project Buid Target : Choisir une cible, Ok.
2) Générer la librairie (cad compiler les fichier C) en ligne de commande dans le répertoire projet (au dessus du répertoire jni) :
>C:\android-ndk-r8e\ndk-build
La bibliothèque générée est placée dans le répertoire libs/armeabi
3) Exécuter le projet.
Dans le répertoire projet taper les commandes :
>android update project -p . -s
>C:\android-ndk-r8e\ndk-build
>ant debug
>adb install bin/<nom_du_projet>-debug.apk
Convertir les fichiers ANSI en UTF-8 (sans BOM) si les caractères accentués posent problème.
Je ne sais pourquoi dans certains projets importés il y a dans le fichier ./app/build.gradle les spécifications suivantes :
compileSdkVersion 21
buildToolsVersion "23.0.2"
dans la rubrique android { …. }, ce qui fait que les fichiers de compilation android-21 sont recherchés alors que je ne les avais pas et que j'avais ceux de la version 23. J'ai donc changé la première ligne par :
compileSdkVersion 23
Pour les projets mixtes java-C/C++, après avoir importé le projet, Android Studio déclare :
Error:(12, 0) NDK integration is deprecated in the current plugin...
Dans les projets que j'ai importés, le fichier gradle.proporties n'existait pas. Je l'ai créé dans la racine du projet, au même niveau que les fichiers build.gradle,et local.propreties, .. avec pour contenu la ligne "android.useDeprecatedNdk=true", sans les guillemets.
Par ailleurs, dans le ficher local.properties qui contenait :
#...Divers commentaires ...
sdk.dir=C\:\\Android\\sdk
cette dernière ligne indiquant l'emplacement du Android SDK j'ai ajouté la ligne :
ndk.dir=C\:\\Android\\android-ndk-r10e
qui précise où se trouve l'Android NDK.
Puis rebuild du projet avec succès cette fois.
Un fragment est une classe réutilisable implémentant une partie d'une activité qui doit être intégrés dans des activités; Il ne peut pas fonctionner indépendamment des activités.
Comme une activité, un fragment est une combinaison d'un fichier de mise en page XML et d'une classe Java. Ce sont des composants autonomes qui peuvent contenir des vues, des événements et de la logique.
Au sein d'une architecture orientée fragment, les activités deviennent des conteneurs de navigation principalement responsables de la navigation vers d'autres activités, de la présentation de fragments et du transfert de données.
Au sein d'une application basée fragment le rôle des activités se limite à la navigation entre activités à travers des intentions, la gestion des fragments en utilisant le gestionnaire de fragment, la réception de données à partir d'intentions et transmission de données entre des fragments et la présentation de composants de navigation tels que drawer et viewpaper.
Les fragments quant à eux vont contrôler les layouts, les views, la logiques de gestion d'événements associée aux vues pertinentes, la logique de gestion d'état telle que la visibilité ou la gestion des erreurs, déclencher des requêtes réseaux via un objet client et récupérer et stocker de données à partir de la persistance.
En résumé, dans une architecture basée sur des fragments, les activités s'occupent de la navigation et les fragments s'occupent des vues et de la logique.
Un bémol à ce beau tableau. La callback associée à l'attribut android:onClick="laCallback" d'un widget dans le fichier xml qui décrit le fragment, cette callback donc est associée à l'activité qui incorpore le fragment et non pas au fragment lui-même, ce qui fait que l'activité doit dans cette callback appeler la fonction adéquate du fragment, ce qui alourdit l'utilisation de cet attribut dans un fichier xml de fragment. Il peut être préférable d'attibuer un listener anonyme au widget dans le code java du fragment, comme ceci (dans le cas du clic sur un bouton) :
bt = (Button) view.findViewById(R.id.theButton);
bt.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {/* code de la callback */}
});
Le layout XML décrivant la vue d'un fragment est tout à fait ordinaire. Il décrit simplement les éléments de la vue comme le suivant (fragment_foo.xml) :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
Avec éventuellement dans la portée Button la ligne suivante :
android:onClick="clickCallback"
mais attention, dans ce cas la méthode clickCallback() doit être déclarée dans l'activity (déjà dit plus haut).
La partie code java du fragment minimale est la suivante (FooFragment.java) :
package llibre.fr.demostaticfragment;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class FooFragment extends Fragment {
// onCreateView gonfle le fichier xml pour en construire une vue et la renvoyer :
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
{return inflater.inflate(R.layout.fragment_foo, parent, false);}
}
où on se contente de gonfler la vue.
Cette incorporation peut être statique, au niveau du layout xml de l'activité, ou dynamique dans le code java de l'activité, mais, dans les deux cas l'activité ne peut être une simple Activity car elle n'offre pas le support du fragment manager pour toutes les versions. L'activité doit étendre soit une AppCompatActivity, et il faut importer android.support.v7.app.AppCompatActivity, soit une FragmentActivity, et il faut importer ?.
L'incorporation statique se fait au niveau du layout xml de l'activité (activity_main.xml), exemple :
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:name="llibre.fr.demostaticfragment.FooFragment"
android:id="@+id/fooFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
Il peut y avoir plusieurs sections <fragment ... />.
android:name donne le nom du code java à incorporer pour ce fragment.
Remarque : On aurait pu mettre LinearLayout , ou autre chose, à la place de FrameLayout.
Les points importants sont :
•la balise fragment en minuscule,
•la balise android:name qui permet de spécifier quelle est la classe qui prend en charge l'implémentation du fragment,
•la balise android:id (resp. android:tag) qui permet de spécifier un identifiant entier (resp. de type String) unique au fragment, ce qui est très important lors de la manipulation dynamique des fragments,
•la manière dont le fragment remplit l'espace via les balises layout_width et layout_height.
Le code java de l'activité doit étendre AppCompatActivity et non pas Activity, et importer android.support.v7.app.AppCompatActivity androidx.appcompat.app.AppCompatActivity.
Dans l'exemple suivant (MainActivity.java), le (ou les) fragment(s) est (sont) automatiquement activé(s) par le biais de l' activity_main.xml. :
package llibre.fr.fragtest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Le manifeste est ordinaire :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="llibre.fr.fragtest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="DemoStaticFragment">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Cette incorporation statique est très simple, mais présente le désavantage d'être consacrée à cette vue statique du (des) fragment(s) incorporé(s). On ne pourra pas modifier. On ne peut remplacer que des fragments ajoutés dynamiquement.
Si dans l'Activity, on a besoin du nom d'un Fragment, par exemple pour faire lui suivre une callback, on utilise la méthode findFragmentById. d'un FragmentManager, éventuellement anonyme (en utilisant directement getSupportFragmentManager().findFragmentById).
FragmentManager fm = getSupportFragmentManager();
FooFragment theFrag = (FooFragment) fm.findFragmentById(R.id.fooFragment);
L'incorporation dynamique au code java se fait en utilisant le FragmentManager. La classe FragmentManager et la classe FragmentTransaction permettent d'ajouter, enlever et remplacer des fragments dans la disposition de l'activité au runtime. Dans ce cas, dans le xml de l'activité on place un conteneur servant de réservation (un placeholder, habituellement un FrameLayout) dans lequel n'importe quel fragment peut être ajouté au runtime.
Exemple de activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/your_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</FrameLayout>
Code java de la MainActivity.java correspondante :
package llibre.fr.demodynfragment;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Begin the transaction
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
// Replace the contents of the container with the new fragment
ft.replace(R.id.your_placeholder, new FooFragment());
// or ft.add(R.id.your_placeholder, new FooFragment());
// Complete the changes added above
ft.commit();
}
}
Remarque : On peut comprimer ces 3 instructions en une seule :
getSupportFragmentManager().beginTransaction().replace(R.id.your_placeholder, new FooFragment()).commit();
Comme les Activity, les fragments ont nombreuses méthodes qui peuvent être surchargées (essentiellement celles en caractères gras rouge) pour s'insérer dans le cycle de vie :
•OnAttach () est appelé quand un fragment est connecté à une activité (on récupère un pointeur sur cette Activité qui n'a pas fini son intialisation. Il vaut mieux attendre OnActivityCreated ()).
•OnCreate () est appelé pour la création initiale du fragment. On y instancie les objets non graphiques. Permet de créer les objets de la classe pour qu'ils soient instanciés une seule fois dans le cycle de vie du fragment ;
•OnCreateView () est appelé lorsque le Fragment doit gonfler une vue. Systématiquement surchargée :
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) possède les arguments suivants :
inflater : Il permet de gonfler le fichier xml de layout ;
container: Si non-null, correspond à la vue parente qui contient le fragment et à laquelle la vue du fragment va s'attacher ;
savedInstanceState : si non-null, le fragment est reconstruit à partir d'un état précédent sauvegardé et transmis par ce paramètre.
Le code typique de la méthode onCreateView gonfle le fichier xml pour en construire une vue et la renvoyer :
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.speaker_detail, container, false);
return view;
}
•OnViewCreated () est appelé après onCreateView () et assure que la racine de la vue du fragment est non-nulle. C'est plutôt ici qu'on instancie les objets graphiques que dans onCreateView (), car on y est sur que la vue existe bien.
•OnActivityCreated () est appelé quand l'activité hôte a achevé sa propre méthode onCreate(). Permet de récupérer un pointeur vers l'activité;
•OnStart () est appelé lorsque le fragment est prêt à être affiché.
•OnResume () - Placer ici l'allocation des ressources lourdes comme l'enregistrement pour l'emplacement, les mises à jour de capteur, etc.
•OnPause () - Placer ici la libération des ressources lourdes. Livrez les modifications.
•OnDestroyView () est appelé quand la vue du fragment est sur le point d'être détruite, mais le fragment est encore conservé. Rarement surchargée.
•OnDestroy () est appelé quand le fragment n'est plus utilisé.
•OnDetach () est appelé quand le fragment n'est plus connecté à l'activité.
Exemple d'utilisation des événements de cycle de vie de fragment divers :
package llibre.fr.fragtest;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.support.v4.app.Fragment;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
public class SomeFragment extends Fragment {
ThingsAdapter adapter;
FragmentActivity listener;
// This event fires 1st, before creation of fragment or any views
// The onAttach method is called when the Fragment instance is associated with an Activity.
// This does not mean the Activity is fully initialized.
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof Activity){
this.listener = (FragmentActivity) context;
}
}
// This event fires 2nd, before views are created for the fragment
// The onCreate method is called when the Fragment instance is being created, or re-created.
// Use onCreate for any standard setup that does not require the activity to be fully created
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ArrayList<Thing> things = new ArrayList<Thing>();
adapter = new ThingsAdapter(getActivity(), things);
}
// The onCreateView method is called when Fragment should create its View object hierarchy,
// either dynamically or via XML layout inflation.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_some, parent, false);
}
// This event is triggered soon after onCreateView().
// onViewCreated() is only called if the view returned from onCreateView() is non-null.
// Any view setup should occur here. E.g., view lookups and attaching view listeners.
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
ListView lv = (ListView) view.findViewById(R.id.lvSome);
lv.setAdapter(adapter);
}
// This method is called when the fragment is no longer connected to the Activity
// Any references saved in onAttach should be nulled out here to prevent memory leaks.
@Override
public void onDetach() {
super.onDetach();
this.listener = null;
}
// This method is called after the parent Activity's onCreate() method has completed.
// Accessing the view hierarchy of the parent activity must be done in the onActivityCreated.
// At this point, it is safe to search for activity View objects by their ID, for example.
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
}
Voici trois méthodes pour chercher l'instance d'un fragment existant dans une activité :
Son identificateur ID : chercher en appelant findFragmentById du FragmentManager
Son étiquette Tag : chercher en appelant findFragmentByTag du FragmentManager
Son avertisseur Pager : chercher en appelant getRegisteredFragment du PagerAdapter
Si le fragment était statiquement incorporé dans le fichier XML de l'activité, avec la donnée d'un android:id, alors son id est simplement obtenue en appelant findFragmentById du FragmentManager :
public class MainActivity extends AppCompatActivity {
FooFragment theFrag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
theFrag = (FooFragment) getSupportFragmentManager().findFragmentById(R.id.fooFragment);
}
}
A titre d'exemple, si on a dans le fichier xml du fragment affecté l'attribut android:onClick="clickCallback" à un élément, la callback va être appelée dans l'activité. Et dans celle-ci, on pourra appeler la callback correspondante (de même nom si on ça nous arrange) du fragment, comme ceci :
public void clickCallback(View v)
{
theFrag.clickCallback(v);
}
Considérons le cas où le fragment est ajouté dynamiquement à l'activité.
Le plus simple est de mémoriser son instance au moment de l'ajout. Exemple :
public class MainActivity extends AppCompatActivity {
FooFragment fooFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
fooFragment = new FooFragment();
ft.replace(R.id.your_placeholder, new FooFragment(), "MyFrag");
ft.commit();
}
}
On peut également affecter une étiquette au fragment au moment de son ajout à l'activité qui permettra ensuite appelant findFragmentByTag du FragmentManager. De récupérer une instance du fragment (mais pas instantanément après le commit).
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.your_placeholder, new FooFragment(), "MyFrag");
ft.commit();
}
public void onClickButton1(View v) {
FooFragment fooFragment = (FooFragment) getSupportFragmentManager().findFragmentByTag("MyFrag");
fooFragment.clickCallback(v);
}
}
Si le fragment a été dynamiquement ajouté au runtime dans une activité à l'intérieur d'un ViewPager à l'aide d'un FragmentPagerAdapter alors on peut le rechercher en l'upgradant en SmartFragmentStatePagerAdapter. Une fois l'adapter en place, on peut avoir accès à n'importe quels fragments du ViewPager en utilisant getRegisteredFragment :
// returns first Fragment item within the pager
adapterViewPager.getRegisteredFragment(0);
Noter que le ViewPager charge les instances à la manière dont les ListView cyclent les items lorsqu'ils apparaissent sur l'écran. Si on essaie d'accéder à un fragment qui n'est pas sur l'écran, la recherche renverra nul.
Les fragments devraient généralement seulement communiquer avec leur activité parentale directe. Les fragments communiquent à travers leur activité parentale permettant à l'activité de gérer les entrées et sorties de données de ce fragment vers d'autres fragments ou activités. L'Activité est le contrôleur qui gére toute interaction entre ses fragments.
Les fragments de dialogue activés de l'intérieur d'un fragment ou des fragments enfant emboîtées font exception à cette règle. Ces deux cas sont des situations où un fragment a des fragments d'enfant emboités a qui il est permis de communiquer vers le haut à leur parent (qui est un fragment).
En règle générale les fragments ne devraient pas directement communiquer avec d'autres fragments. Ils devraient seulement communiquer avec leur activité parentale, car ils devraient être des composants modulaires, autonomes et réutilisables. Les fragments permettent à leur activité parentale de répondre aux intents et callbacks dans la plupart des cas.
La communication entre un fragment et une activité peut se faire de 3 manières différentes :
•Bundle : l'Activité peut construire un fragment et initialiser des données du fragment pendant la construction.
•Method : l'Activité peut passer des données aux fragments en utilisant des méthodes sur les instances des fragment
•Listener : les Fragments peuvent communiquer avec leur activité parentale en utilisant les interfaces de Listeners
Un Fragment ne pouvant avoir qu'un constructeur sans arguments, un méthode simple pour lui en attribuer consiste à créer une méthode statique (qui ne nécessite pas d'instance préalable du fragment) avec des arguments qui retournera le Fragment qu'elle va créer. Comme une méthode statique ne peut mémoriser que des paramètres statiques, on peut utiliser la méthode Bundle.setArguments pour les mémoriser et on utilisera la méthode Bundle.getArguments pour les récupérer ensuite dans le bundle du onCreate du Fragment. Exemple :
public class FooFragment extends Fragment {
TextView tv1;
Button bt1;
int nClicks;
String nomTruc;
int ageTruc;
// Pseudo constructeur (renvoi new FooFragment) prenant des argument
public static FooFragment MakeFooFragment(int someInt, String someTitle) {
FooFragment theFragment = new FooFragment();
Bundle args = new Bundle();
args.putInt("ageTruc", someInt);
args.putString("nomTrucv", someTitle);
theFragment.setArguments(args);
return theFragment;
}
// On surcharge nCreate pour récupérer les arguments
@Override
public void onCreate(Bundle bundle)
{
super.onCreate(bundle);
ageTruc = getArguments().getInt("ageTruc",0);
nomTruc = getArguments().getString("nomTrucv","");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_foo, parent, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
// Setup any handles to view objects here
// EditText etFoo = (EditText) view.findViewById(R.id.etFoo);
tv1 = (TextView) view.findViewById(R.id.textView1);
tv1.setText(nomTruc + " à " + ageTruc + " ans !");
nClicks = 0;
}
public void clickCallback(View v)
{
nClicks++;
tv1.setText("Click sur le botton N° " + nClicks);
}
}
Les arguments sont passés au fragment par la MainActivity à son incorporation dynamique comme ceci :
package llibre.fr.demodyn2fragment;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
public class MainActivity extends AppCompatActivity {
FooFragment theFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
theFragment = FooFragment.MakeFooFragment(5, "Age de Toto");
ft.replace(R.id.your_placeholder, theFragment);
ft.commit();
}
public void onClickButton1(View v) {
theFragment.clickCallback(v);
}
}
Remarque : Si les arguments peuvent être correspondre à des champs statiques du fragment, il est inutile de passer par le bundle.setArguments et bundle.getArguments et par le onCreate pour les récupérer, exemple :
package llibre.fr.demodyn2fragment;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public class FooFragment extends Fragment {
TextView tv1;
Button bt1;
int nClicks;
static String nomTruc;
static int ageTruc;
/Pseudo constructeur (renvoi new FooFragment) prenant des argument
public static FooFragment MakeFooFragment(int someInt, String someTitle) {
FooFragment theFragment = new FooFragment();
Bundle args = new Bundle();
nomTruc = someTitle;
ageTruc = someInt;
return theFragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_foo, parent, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
tv1 = (TextView) view.findViewById(R.id.textView1);
tv1.setText(nomTruc + " à " + ageTruc + " ans !");
nClicks = 0;
}
public void clickCallback(View v)
{
nClicks++;
tv1.setText("Click sur le botton N° " + nClicks);
}
}
Une méthode quelconque de la super-classe Fragment créée, ayant des arguments, peut être utilisée pour transmettre ces arguments. Il suffit d'avoir accès à une instance de ce fragment (cf. 28.4) pour appeler ses méthodes.
A faire
Les principales méthodes du FragmentManager sont :
AddOnBackStackChangedListener : Ajouter un nouvel auditeur pour des changements sur la pile retour du fragment.
BeginTransaction () : Crée une nouvelle transaction pour changer des fragments au runtime.
FindFragmentById (int id) : Trouve un fragment par l'id attribuée au fragment dans le XML de l'activité.
FindFragmentByTag (String tag) : Trouve un fragment par l'étiquette attrinuée au runtime.
PopBackStack () : Enlever un fragment de la pile de retour.
ExecutePendingTransactions () : Obliger les transactions archivées à être appliquées
Ce tuto date de janvier 2013. Les liens ou indications de versions sont peut-être obsolètes, si ce n'est le tout.
Sans utiliser l'environnement de programmation Eclipse, l'installation du SDK Android (SDK = Sofware Developement Kit = ensemble nécessaire pour développer des logiciels) se fait en 3 étapes :
1.Installation du SDK Java, pour faire des applications java.
2.Installation du gestionnaire de compilation Ant.
3.Installation du SDK Android proprement dit.
A cette adresse web :
http://www.oracle.com/technetwork/java/javase/downloads/index.html
Choisir l'icone qui correspond à "Java Platform(JDK) 7u2" (le numéro sera peut être différent), ce qui conduit à la page :
http://www.oracle.com/technetwork/java/javase/downloads/jdk-7u2-download-1377129.html
et là cocher "Accept License Agreement", puis choisir la ligne pour windows x86 (ou x64 pour les processeurs 64 bits), où actuellement il y a :
jdk-7u2-windows-i586.exe
- Suivre les instructions d'installation.
- Ensuite, il faut que la commande java soit accessible dans le Path et que la variable JAVA_HOME soit définie dans l'environnement. Pour voir tout ce qui est défini dans l'environnement, taper :
>set
dans une fenêtre dos. S'il n'y a pas de ligne commençant par JAVA_HOME, il faut la définir.
De plus, si en tapant
>cd \
>java
le système répond java n'est pas reconnu en tant que commande interne, il faut ajouter le répertoire C:\Program Files\Java\jdk1.6.0_25\bin à la variable Path utilisateur (jdk1.6.0_25 c'est dans mon cas, voir le nom exact dans votre cas).
Pour ajouter la variable JAVA_HOME et modifier/ou ajouter sa variable Path, aller dans :
Démarrer/paramètres/Panneau de Configuration/Système/avancé/Variables d'environnement et dans la fenêtre Variables utilisateur pour ??? utiliser les boutons Nouveau pour créer les variables qui n'existent pas ou Modifier ....
Pour vérifier si ça marche, ouvrir une console DOS et se placer dans un répertoire temporaire quelconque, par exemple c:\temp :
> cd c:\temp
dans ce répertoire créer le fichier PremierProg.java contenant les 7 lignes suivantes :
public class PremierProg
{
public static void main (String args[])
{
System.out.println("Bonjour tout le monde !");
}
}
Pour cela le plus simple, sélectionner ces 7 lignes et les copier (CTRL+C), puis dans la console DOS taper :
>copy con: PremierProg.java
et clic droit/coller, puis RETURN, puis CTRL+Z et RETURN.
Puis, pour vérifier, taper la commande :
>type PremierProg.java
et on doit voir les 7 lignes ci-dessus. Ensuite taper :
>javac PremierProg.java
qui va compiler le programme (attention avec "c" à la fin du mot javac) et générer un fichier PremierProg.class. Pour l'exécuter taper :
>java PremierProg
la réponse doit être : Bonjour tout le monde !
Après ça, supprimer PremierProg.class, mais pas PremierProg.java qui va resservir :
>del PremierProg.class
C' est un gestionnaire de compilation du genre makefile. Il n'y a pas besoin de savoir comment il marche, mais, en l'absence d'Eclipse (ou autre environnement) il est nécessaire à la compilation des projets android. On le récupère à cette adresse :
http://ant.apache.org
dans la colonne de gauche cliquer sur : Download/Binary Distributions
puis dans le paragraphe Current Release of Ant, clic droit sur apache-ant-1.8.2-bin.zip (ou plus récent) et faire save target as et choisir un répertoire de sauvegarde. Le dézipper. Moi j'ai directement dézippé dans c: et tout a été dézippé dans C:\apache-ant-1.8.2.
Ensuite, il faut (comme pour java) créer dans l'environnement de windows la variable :
ANT_HOME=C:\apache-ant-1.8.2
(ou bien la valeur du répertoire où apache-ant-1.8.2 a atterri), et ajouter au Path la chaine C:\apache-ant-1.8.2\bin (ou son équivalent), séparé de ce qui y est déjà par un ";".
Pour tester : Dans le répertoire contenant PremierProg.java, créer le fichier build.xml contenant les 7 lignes suivantes :
<?xml version="1.0"?>
<project name="monProj" default="compilation" >
<target name="compilation">
<javac srcdir="." />
<echo>Fin compil</echo>
</target>
</project>
Pour cela,comme déja fait, sélectionner les 7 lignes, taper :
>copy con: build.xml
puis clic droit/coller, puis RETURN, CTRL+Z et RETURN.
puis pour voir si ant marche, taper :
>ant
Chez moi, ça répond :
compilation:
[javac] C:\temp\build.xml:4: warning: 'includeantruntime' was not set, defaulting to build.sysclassPath=last; set to false for repeatable builds
[javac] Compiling 1 source file
[echo] Fin compil
BUILD SUCCESSFUL
Total time: 1 second
Le fichier PremierProg.class doit avoir été créé et la commande
>java PremierProg
doit fournir la même réponse que ci-dessus.
Si ça marche effacer tout le contenu du répertoire c:\temp qui ne sert plus à rien.
Page web : http://developer.android.com/sdk/index.html
Pour windows récupérer : http://dl.google.com/android/installer_r16-windows.exe et l'exécuter.
Comme précédemment il faut ajouter aux variables d'environnement la variable ANDROID_HOME avec la valeur C:\Program Files\Android\android-sdk (ou le répertoire où le sdk android a été installé) et ajouter au Path les chemins suivants (séparés par des ";" des chemins déjà présents) :
C:\Program Files\Android\android-sdk\tools;C:\Program Files\Android\android-sdk\platform-tools
Maintenant, dans une fenêtre dos, dans un répertoire temporaire quelconque, taper :
>android list targets
Chez moi, il répond :
Available Android targets:
----------
id: 1 or "android-10"
Name: Android 2.3.3
Type: Platform
API level: 10
Revision: 2
Skins: HVGA, QVGA, WQVGA400, WQVGA432, WVGA800 (default), WVGA854
ABIs : armeabi
----------
id: 2 or "Google Inc.:Google APIs:10"
Name: Google APIs
Type: Add-On
Vendor: Google Inc.
Revision: 2
Description: Android + Google APIs
Based on Android 2.3.3 (API level 10)
Libraries:
* com.android.future.usb.accessory (usb.jar)
API for USB Accessories
* com.google.android.maps (maps.jar)
API for Google Maps
Skins: WVGA854, WQVGA400, HVGA, WQVGA432, WVGA800 (default), QVGA
ABIs : armeabi
----------
id: 3 or "android-15"
Name: Android 4.0.3
Type: Platform
API level: 15
Revision: 2
Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800
ABIs : armeabi-v7a
----------
id: 4 or "Google Inc.:Google APIs:15"
Name: Google APIs
Type: Add-On
Vendor: Google Inc.
Revision: 1
Description: Android + Google APIs
Based on Android 4.0.3 (API level 15)
Libraries:
* com.google.android.media.effects (effects.jar)
Collection of video effects
* com.android.future.usb.accessory (usb.jar)
API for USB Accessories
* com.google.android.maps (maps.jar)
API for Google Maps
Skins: WVGA854, WQVGA400, WSVGA, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
ABIs : armeabi-v7a
L'essentiel c'est qu'il y ait au moins la première (android 2.3.3).
Ensuite pour tester la création de l'application minimale par défaut taper :
>android create project --name myProj --path monrep --activity Essai --package fr.llibre.test --target android-10
ou :
monrep après --path indique que les créations sont à mettre dans le répertoire monrep,
android-10 est l'identificateur de l'appareil cible, on peut mettre 1 si c'est le premier dans la liste précédente,
fr.llibre.test est le nom du package. Il doit comporter au moins un ".". Il va lui correspondre un répertoire privé sur le smartphone pour les fichiers de l'appli.
myProj est le nom du projet. Le fichier *.apk généré par la compilation s'appellera myProj-xxx.apk où xxx dépend du mode de compilation.
Essai est le nom donné à la classe java Activity, et à l'icône qui apparaît dans la fenêtre du choix des activités et à l'application qui apparaît dans le menu gestionnaire des applications.
On doit obtenir une réponse comme ça :
Created project directory: C:\temp\monrep
Created directory C:\temp\monrep\src\fr\llibre\test
Added file C:\temp\monrep\src\fr\llibre\test\Essai.java
Created directory C:\temp\monrep\res
Created directory C:\temp\monrep\bin
Created directory C:\temp\monrep\libs
Created directory C:\temp\monrep\res\values
Added file C:\temp\monrep\res\values\strings.xml
Created directory C:\temp\monrep\res\layout
Added file C:\temp\monrep\res\layout\main.xml
Added file C:\temp\monrep\AndroidManifest.xml
Added file C:\temp\monrep\build.xml
Added file C:\temp\monrep\proguard.cfg
Ensuite en tapant :
>cd monrep
>dir bin
on constate qu'il n'y a rien. Pour compiler, on tape :
>ant clean debug
(c'est là que ant sert). Il écrit plein de trucs et si ça s'est bien passé ça se termine par :
BUILD SUCCESSFUL
Total time: 3 seconds
En tapant
>dir bin
dans la liste des fichiers créés il doit y en avoir un qui s'appelle myProj-debug.apk. C'est celui-là qu'on peut copier sur le smartphone pour faire un essai.
Pour créer un émulateur de test, taper :
>android avd
et dans l'application qui s'ouvre, s'il n'y a aucun AVD (android virtual Device), en créer un avec le bouton New.
Pour tester l'application avec l'émulateur, taper :
>emulator @android233
(android233 est le nom d'un des AVD). Dans mon cas il répond emulator: WARNING: Unable to create sensors port: Unknown error mais c'est sans importance.
Ensuite installer l'appli sur l'émulateur :
>adb install bin\myProj-debug.apk
Le système me répond :
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
error: device offline
Le simulateur met du temps a être réceptif. Il faut parfois recommencer plusieurs fois.
>adb install bin\myProj-debug.apk
error: device offline
>adb install bin\myProj-debug.apk
27 KB/s (4394 bytes in 0.157s)
pkg: /data/local/tmp/myProj-debug.apk
Success
Sur le smarphone, l'icone Essai doit apparaître. En l'activant on doit avoir le message :
Hello World, Essai
Ensuite on peut désinstaller cette application.
Liste complète dans Help/Default Keymap Reference
CTRL-Q : Accès à l'aide rapide sur la fonction sélectionnée.
SHIFT-F1 : Accès à la doc en ligne sur la fonction sélectionnée.