Notre premier TP ! Nous avions bien sûr déjà fait un petit programme avec le calculateur d'IMC, mais cette fois nous allons réfléchir à tous les détails pour faire une application qui plaira à d'éventuels utilisateurs : un bloc-notes.
En théorie, vous verrez à peu près tout ce qui a été abordé jusque là, donc s'il vous manque une information, pas de panique, on respire un bon coup et on regarde dans les chapitres précédents, en quête d'informations. Je vous donnerai évidemment la solution à ce TP, mais ce sera bien plus motivant pour vous si vous réussissez seuls. Une dernière chose : il n'existe pas une solution mais des solutions. Si vous parvenez à réaliser cette application en n'ayant pas le même code que moi, ce n'est pas grave, l'important c'est que cela fonctionne.
- Objectif
- Spécifications techniques
- Déboguer des applications Android
- Ma solution
- Objectifs secondaires
Objectif
L'objectif ici va être de réaliser un programme qui mettra en forme ce que vous écrivez. Cela ne sera pas très poussé : mise en gras, en italique, souligné, changement de couleur du texte et quelques smileys. Il y aura une visualisation de la mise en forme en temps réel. Le seul hic c'est que… vous ne pourrez pas enregistrer le texte, étant donné que nous n'avons pas encore vu comment faire.
Ici, on va surtout se concentrer sur l'aspect visuel du TP. C'est pourquoi nous allons essayer d'utiliser le plus de widgets et de layouts possible. Mais en plus, on va exploiter des ressources pour nous simplifier la vie sur le long terme. La figure suivante vous montre ce que j'obtiens. Ce n'est pas très joli, mais ça fonctionne.
Vous pouvez voir que l'écran se divise en deux zones :
- Celle en haut avec les boutons constituera le menu ;
- Celle du bas avec l'
EditText
et lesTextView
.
Le menu
Chaque bouton permet d'effectuer une des commandes de base d'un éditeur de texte. Par exemple, le bouton Gras met une portion du texte en gras, appuyer sur n'importe lequel des smileys permet d'insérer cette image dans le texte et les trois couleurs permettent de choisir la couleur de l'ensemble du texte (enfin vous pouvez le faire pour une portion du texte si vous le désirez, c'est juste plus compliqué).
Ce menu est mouvant. En appuyant sur le bouton Cacher, le menu se rétracte vers le haut jusqu'à disparaître. Puis, le texte sur le bouton devient « Afficher » et cliquer dessus fait redescendre le menu (voir figure suivante).
L'éditeur
Je vous en parlais précédemment, nous allons mettre en place une zone de prévisualisation qui permettra de voir le texte mis en forme en temps réel, comme sur l'image suivante.
Spécifications techniques
Fichiers à utiliser
On va d'abord utiliser les smileys du Site du Zéro : .
Pour les boutons, j'ai utilisé les 9-patches visibles à la figure suivante.
Le HTML
Les balises
Comme vous avez pu le constater, nos textes seront formatés à l'aide du langage de balisage HTML. Rappelez-vous, je vous avais déjà dit qu'il était possible d’interpréter du HTML dans un TextView
; cependant, on va procéder un peu différemment ici comme je vous l'indiquerai plus tard.
Heureusement, vous n'avez pas à connaître le HTML, juste certaines balises de base que voici :
Effet désiré |
Balise |
---|---|
Écrire en gras |
|
Écrire en italique |
|
Souligner du texte |
|
Insérer une image |
|
Changer la couleur de la police |
|
L'évènementiel
Ensuite, on a dit qu'il fallait que le TextView
interprète en temps réel le contenu de l'EditText
. Pour cela, il suffit de faire en sorte que chaque modification de l'EditText
provoque aussi une modification du TextView
: c'est ce qu'on appelle un évènement. Comme nous l'avons déjà vu, pour gérer les évènements, nous allons utiliser un Listener
. Dans ce cas précis, ce sera un objet de type TextWatcher
qui fera l'affaire. On peut l'utiliser de cette manière :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | editText.addTextChangedListener(new TextWatcher() { @Override /** * s est la chaîne de caractères qui est en train de changer */ public void onTextChanged(CharSequence s, int start, int before, int count) { // Que faire au moment où le texte change ? } @Override /** * @param s La chaîne qui a été modifiée * @param count Le nombre de caractères concernés * @param start L'endroit où commence la modification dans la chaîne * @param after La nouvelle taille du texte */ public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Que faire juste avant que le changement de texte soit pris en compte ? } @Override /** * @param s L'endroit où le changement a été effectué */ public void afterTextChanged(Editable s) { // Que faire juste après que le changement de texte a été pris en compte ? } }); |
De plus, il nous faut penser à autre chose. L'utilisateur va vouloir appuyer sur Entrée pour revenir à la ligne quand il sera dans l'éditeur. Le problème est qu'en HTML il faut préciser avec une balise qu'on veut faire un retour à la ligne ! S'il appuie sur Entrée, aucun retour à la ligne ne sera pris en compte dans le TextView
, alors que dans l'EditText
, si. C'est pourquoi il va falloir faire attention aux touches que presse l'utilisateur et réagir en fonction du type de touche. Cette détection est encore un évènement, il s'agit donc encore d'un rôle pour un Listener
: cette fois, le OnKeyListener
. Il se présente ainsi :
1 2 3 4 5 6 7 8 9 10 11 | editText.setOnKeyListener(new View.OnKeyListener() { /** * Que faire quand on appuie sur une touche ? * @param v La vue sur laquelle s'est effectué l'évènement * @param keyCode Le code qui correspond à la touche * @param event L'évènement en lui-même */ public boolean onKey(View v, int keyCode, KeyEvent event) { // … } }); |
Le code pour la touche Entrée est 66. Le code HTML du retour à la ligne est <br />
.
Les images
Pour pouvoir récupérer les images en HTML, il va falloir préciser à Android comment les récupérer. On utilise pour cela l'interface Html.ImageGetter
. On va donc faire implémenter cette interface à une classe et devoir implémenter la seule méthode à implémenter : public Drawable getDrawable (String source)
. À chaque fois que l'interpréteur HTML rencontrera une balise pour afficher une image de ce style <img src="source">
, alors l'interpréteur donnera à la fonction getDrawable
la source précisée dans l'attribut src
, puis l'interpréteur affichera l'image que renvoie getDrawable
. On a par exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Exemple implements ImageGetter { @Override public Drawable getDrawable(String smiley) { Drawable retour = null; Resources resources = context.getResources(); retour = resources.getDrawable(R.drawable.ic_launcher); // On délimite l'image (elle va de son coin en haut à gauche à son coin en bas à droite) retour.setBounds(0, 0, retour.getIntrinsicWidth(), retour.getIntrinsicHeight()); return retour; } } |
Enfin, pour interpréter le code HTML, utilisez la fonction public Spanned Html.fromHtml(String source, Html.ImageGetter imageGetter, null)
(nous n'utiliserons pas le dernier paramètre). L'objet Spanned
retourné est celui qui doit être inséré dans le TextView
.
Les codes pour chaque couleur
La balise <font color="couleur">
a besoin qu'on lui précise un code pour savoir quelle couleur afficher. Vous devez savoir que :
- Le code pour le noir est
#000000
. - Le code pour le bleu est
#0000FF
. - Le code pour le rouge est
#FF0000
.
L'animation
On souhaite faire en sorte que le menu se rétracte et ressorte à volonté. Le problème, c'est qu'on a besoin de la hauteur du menu pour pouvoir faire cette animation, et cette mesure n'est bien sûr pas disponible en XML. On va donc devoir faire une animation de manière programmatique.
Comme on cherche uniquement à déplacer linéairement le menu, on utilisera la classe TranslateAnimation
, en particulier son constructeur public TranslateAnimation (float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
. Chacun de ces paramètres permet de définir sur les deux axes (X et Y) d'où part l'animation (from
) et jusqu'où elle va (to
). Dans notre cas, on aura besoin de deux animations : une pour faire remonter le menu, une autre pour le faire descendre.
Pour faire remonter le menu, on va partir de sa position de départ (donc fromXDelta = 0
et fromYDelta = 0
, c'est-à-dire qu'on ne bouge pas le menu sur aucun des deux axes au début) et on va le déplacer sur l'axe Y jusqu'à ce qu'il sorte de l'écran (donc toXDelta = 0
puisqu'on ne bouge pas et toYDelta = -tailleDuMenu
puisque, rappelez-vous, l'axe Y part du haut pour aller vers le bas). Une fois l'animation terminée, on dissimule le menu avec la méthode setVisibility(VIEW.Gone)
.
Avec un raisonnement similaire, on va d'abord remettre la visibilité à une valeur normale (setVisibility(VIEW.Visible)
) et on déplacera la vue de son emplacement hors cadre jusqu'à son emplacement normal (donc fromXDelta = 0
, fromYDelta = -tailleDuMenu
, toXDelta = 0
et toYDelta = 0
).
Il est possible d'ajuster la vitesse avec la fonction public void setDuration (long durationMillis)
. Pour rajouter un interpolateur, on peut utiliser la fonction public void setInterpolator (Interpolator i)
; j'ai par exemple utilisé un AccelerateInterpolator
.
Enfin, je vous conseille de créer un layout personnalisé pour des raisons pratiques. Je vous laisse imaginer un peu comment vous débrouiller ; cependant, sachez que pour utiliser une vue personnalisée dans un fichier XML, il vous faut préciser le package dans lequel elle se trouve, suivi du nom de la classe. Par exemple :
1 | <nom.du.package.NomDeLaClasse>
|
Liens
Plus d'informations :
Déboguer des applications Android
Quand on veut déboguer en Java, sans passer par le débogueur, on utilise souvent System.out.println
afin d'afficher des valeurs et des messages dans la console. Cependant, on est bien embêté avec Android, puisqu'il n'est pas possible de faire de System.out.println
. En effet, si vous faites un System.out.println
, vous envoyez un message dans la console du terminal sur lequel s'exécute le programme, c'est-à-dire la console du téléphone, de la tablette ou de l'émulateur ! Et vous n'y avez pas accès avec Eclipse. Alors, qu'est-ce qui existe pour la remplacer ?
Laissez-moi vous présenter le Logcat. C'est un outil de l'ADT, une sorte de journal qui permet de lire des entrées, mais surtout d'en écrire. Voyons d'abord comment l'ouvrir. Dans Eclipse, allez dans Window > Show View > Logcat
. Normalement, il s'affichera en bas de la fenêtre, dans la partie visible à la figure suivante.
La première chose à faire, c'est de cliquer sur le troisième bouton en haut à droite (voir figure suivante).
Félicitations, vous venez de vous débarrasser d'un nombre incalculable de bugs laissés dans le Logcat ! En ce qui concerne les autres boutons, celui de gauche permet d'enregistrer le journal dans un fichier externe, le deuxième, d'effacer toutes les entrées actuelles du journal afin d'obtenir un journal vierge, et le dernier bouton permet de mettre en pause pour ne plus voir le journal défiler sans cesse.
Pour ajouter des entrées manuellement dans le Logcat, vous devez tout d'abord importer android.util.Log
dans votre code. Vous pouvez ensuite écrire des messages à l'aide de plusieurs méthodes. Chaque message est accompagné d'une étiquette, qui permet de le retrouver facilement dans le Logcat.
Log.v("Étiquette", "Message à envoyer")
pour vos messages communs.Log.d("Étiquette", "Message à envoyer")
pour vos messages de debug.Log.i("Étiquette", "Message à envoyer")
pour vos messages à caractère informatif.Log.w("Étiquette", "Message à envoyer")
pour vos avertissements.Log.e("Étiquette", "Message à envoyer")
pour vos erreurs.
Vous pouvez ensuite filtrer les messages que vous souhaitez afficher dans le Logcat à l'aide de la liste déroulante visible à la figure suivante.
Vous voyez, la première lettre utilisée dans le code indique un type de message : v
pour Verbose
, d
pour Debug
, etc.
Sachez aussi que, si votre programme lance une exception non catchée, c'est dans le Logcat que vous verrez ce qu'on appelle le « stack trace », c'est-à-dire les différents appels à des méthodes qui ont amené au lancement de l'exception.
Par exemple avec le code :
1 2 3 | Log.d("Essai", "Coucou les Zéros !"); TextView x = null; x.setText("Va planter"); |
On obtient la figure suivante.
À la figure suivante, on peut voir le message que j'avais inséré.
Avec, dans les colonnes (de gauche à droite) :
- Le type de message (D pour Debug) ;
- La date et l'heure du message ;
- Le numéro unique de l'application qui a lancé le message ;
- Le package de l'application ;
- L'étiquette du message ;
- Le contenu du message.
On peut aussi voir à la figure suivante que mon étourderie a provoqué un plantage de l'application.
Ce message signifie qu'il y a eu une exception de type NullPointerException
(provoquée quand on veut utiliser un objet qui vaut null
). Vous pouvez voir à la deuxième ligne que cette erreur est intervenue dans ma classe RelativeLayoutActivity
qui appartient au package sdz.chapitreDeux.relativeLayout
. L'erreur s'est produite dans la méthode onCreate
, à la ligne 23 de mon code pour être précis. Enfin, pas besoin de fouiller, puisqu'un double-clic sur l'une de ces lignes permet d'y accéder directement.
Ma solution
Les ressources
Couleurs utilisées
J'ai défini une ressource de type values
qui contient toutes mes couleurs. Elle contient :
1 2 3 4 5 | <resources> <color name="background">#99CCFF</color> <color name="black">#000000</color> <color name="translucide">#00000000</color> </resources> |
La couleur translucide est un peu différente des autres qui sont des nombres hexadécimaux sur 8 bits : elle est sur 8 + 2 bits. En fait, les deux bits supplémentaires expriment la transparence. Je l'ai mise à 00, comme ça elle représente les objets transparents.
Styles utilisés
Parce qu'ils sont bien pratiques, j'ai utilisé des styles, par exemple pour tous les textes qui doivent prendre la couleur noire :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <resources> <style name="blueBackground"> <item name="android:background">@color/background</item> </style> <style name="blackText"> <item name="android:textColor">@color/black</item> </style> <style name="optionButton"> <item name="android:background">@drawable/option_button</item> </style> <style name="hideButton"> <item name="android:background">@drawable/hide_button</item> </style> <style name="translucide"> <item name="android:background">@color/translucide</item> </style> </resources> |
Rien de très étonnant encore une fois. Notez bien que le style appelé translucide
me permettra de mettre en transparence le fond des boutons qui affichent des smileys.
Les chaînes de caractères
Sans surprise, j'utilise des ressources pour contenir mes string
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <resources> <string name="app_name">Notepad</string> <string name="hide">Cacher</string> <string name="show">Afficher</string> <string name="bold">Gras</string> <string name="italic">Italique</string> <string name="underline">Souligné</string> <string name="blue">Bleu</string> <string name="red">Rouge</string> <string name="black">Noir</string> <string name="smileys">Smileys :</string> <string name="divider">Séparateur</string> <string name="edit">Édition :</string> <string name="preview">Prévisualisation : </string> <string name="smile">Smiley content</string> <string name="clin">Smiley qui fait un clin d\oeil</string> <string name="heureux">Smiley avec un gros sourire</string> </resources> |
Le Slider
J'ai construit une classe qui dérive de LinearLayout
pour contenir toutes mes vues et qui s'appelle Slider
. De cette manière, pour faire glisser le menu, je fais glisser toute l'activité et l'effet est plus saisissant. Mon Slider
possède plusieurs attributs :
boolean isOpen
, pour retenir l'état de mon menu (ouvert ou fermé) ;RelativeLayout toHide
, qui est le menu à dissimuler ou à afficher ;final static int SPEED
, afin de définir la vitesse désirée pour mon animation.
Finalement, cette classe ne possède qu'une grosse méthode, qui permet d'ouvrir ou de fermer le menu :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /** * Utilisée pour ouvrir ou fermer le menu. * @return true si le menu est désormais ouvert. */ public boolean toggle() { //Animation de transition. TranslateAnimation animation = null; // On passe de ouvert à fermé (ou vice versa) isOpen = !isOpen; // Si le menu est déjà ouvert if (isOpen) { // Animation de translation du bas vers le haut animation = new TranslateAnimation(0.0f, 0.0f, -toHide.getHeight(), 0.0f); animation.setAnimationListener(openListener); } else { // Sinon, animation de translation du haut vers le bas animation = new TranslateAnimation(0.0f, 0.0f, 0.0f, -toHide.getHeight()); animation.setAnimationListener(closeListener); } // On détermine la durée de l'animation animation.setDuration(SPEED); // On ajoute un effet d'accélération animation.setInterpolator(new AccelerateInterpolator()); // Enfin, on lance l'animation startAnimation(animation); return isOpen; } |
Le layout
Tout d'abord, je rajoute un fond d'écran et un padding au layout pour des raisons esthétiques. Comme mon Slider
se trouve dans le package sdz.chapitreDeux.notepad
, je l'appelle avec la syntaxe sdz.chapitreDeux.notepad.Slider
:
1 2 3 4 5 6 7 8 9 | <sdz.chapitreDeux.notepad.Slider xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/slider" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:padding="5dip" style="@style/blueBackground" > <!-- Restant du code --> </sdz.chapitreDeux.notepad.Slider> |
Ensuite, comme je vous l'ai dit dans le chapitre consacré aux layouts, on va éviter de cumuler les LinearLayout
, c'est pourquoi j'ai opté pour le très puissant RelativeLayout
à la place :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | <RelativeLayout android:id="@+id/toHide" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layoutAnimation="@anim/main_appear" android:paddingLeft="10dip" android:paddingRight="10dip" > <Button android:id="@+id/bold" style="@style/optionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="@string/bold" /> <TextView android:id="@+id/smiley" style="@style/blackText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@id/bold" android:paddingTop="5dip" android:text="@string/smileys" /> <ImageButton android:id="@+id/smile" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/bold" android:layout_toRightOf="@id/smiley" android:contentDescription="@string/smile" android:padding="5dip" android:src="@drawable/smile" style="@style/translucide" /> <ImageButton android:id="@+id/heureux" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/smile" android:layout_centerHorizontal="true" android:contentDescription="@string/heureux" android:padding="5dip" android:src="@drawable/heureux" style="@style/translucide" /> <ImageButton android:id="@+id/clin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/smile" android:layout_alignLeft="@+id/underline" android:layout_alignRight="@+id/underline" android:contentDescription="@string/clin" android:padding="5dip" android:src="@drawable/clin" style="@style/translucide" /> <Button android:id="@+id/italic" style="@style/optionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:text="@string/italic" /> <Button android:id="@+id/underline" style="@style/optionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentRight="true" android:text="@string/underline" /> <RadioGroup android:id="@+id/colors" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" android:layout_below="@id/heureux" android:orientation="horizontal" > <RadioButton android:id="@+id/black" style="@style/blackText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true" android:text="@string/black" /> <RadioButton android:id="@+id/blue" style="@style/blackText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/blue" /> <RadioButton android:id="@+id/red" style="@style/blackText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/red" /> </RadioGroup> </RelativeLayout> |
On trouve ensuite le bouton pour actionner l'animation. On parle de l'objet au centre du layout parent (sur l'axe horizontal) avec l'attribut android:layout_gravity="center_horizontal"
.
1 2 3 4 5 6 7 8 | <Button android:id="@+id/hideShow" style="@style/hideButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dip" android:layout_gravity="center_horizontal" android:text="@string/hide" /> |
J'ai ensuite rajouté un séparateur pour des raisons esthétiques. C'est une ImageView
qui affiche une image qui est présente dans le système Android ; faites de même quand vous désirez faire un séparateur facilement !
1 2 3 4 5 6 7 8 9 10 | <ImageView android:src="@android:drawable/divider_horizontal_textfield" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scaleType="fitXY" android:paddingLeft="5dp" android:paddingRight="5dp" android:paddingBottom="2dp" android:paddingTop="2dp" android:contentDescription="@string/divider" /> |
La seconde partie de l'écran est représentée par un TableLayout
— plus par intérêt pédagogique qu'autre chose. Cependant, j'ai rencontré un comportement étrange (mais qui est voulu, d'après Google…). Si on veut que notre EditText
prenne le plus de place possible dans le TableLayout
, on doit utiliser android:stretchColumns
, comme nous l'avons déjà vu. Cependant, avec ce comportement, le TextView
ne fera pas de retour à la ligne automatique, ce qui fait que le texte dépasse le cadre de l'activité. Pour contrer ce désagrément, au lieu d'étendre la colonne, on la rétrécit avec android:shrinkColumns
et on ajoute un élément invisible qui prend le plus de place possible en largeur. Regardez vous-mêmes :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <TableLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:shrinkColumns="1" > <TableRow android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:text="@string/edit" android:layout_width="fill_parent" android:layout_height="fill_parent" style="@style/blackText" /> <EditText android:id="@+id/edit" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="top" android:inputType="textMultiLine" android:lines="5" android:textSize="8sp" /> </TableRow> <TableRow android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="@string/preview" style="@style/blackText" /> <TextView android:id="@+id/text" android:layout_width="fill_parent" android:layout_height="fill_parent" android:textSize="8sp" android:text="" android:scrollbars="vertical" android:maxLines = "100" android:paddingLeft="5dip" android:paddingTop="5dip" style="@style/blackText" /> </TableRow> <TableRow android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="" /> <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:text=" " /> </TableRow> </TableLayout> |
Le code
Le SmileyGetter
On commence par la classe que j'utilise pour récupérer mes smileys dans mes drawables. On lui donne le Context
de l'application en attribut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * Récupère une image depuis les ressources * pour les ajouter dans l'interpréteur HTML */ public class SmileyGetter implements ImageGetter { /* Context de notre activité */ protected Context context = null; public SmileyGetter(Context c) { context = c; } public void setContext(Context context) { this.context = context; } @Override /** * Donne un smiley en fonction du paramètre d'entrée * @param smiley Le nom du smiley à afficher */ public Drawable getDrawable(String smiley) { Drawable retour = null; // On récupère le gestionnaire de ressources Resources resources = context.getResources(); // Si on désire le clin d'œil… if(smiley.compareTo("clin") == 0) // … alors on récupère le drawable correspondant retour = resources.getDrawable(R.drawable.clin); else if(smiley.compareTo("smile") == 0) retour = resources.getDrawable(R.drawable.smile); else retour = resources.getDrawable(R.drawable.heureux); // On délimite l'image (elle va de son coin en haut à gauche à son coin en bas à droite) retour.setBounds(0, 0, retour.getIntrinsicWidth(), retour.getIntrinsicHeight()); return retour; } } |
L'activité
Enfin, le principal, le code de l'activité :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | public class NotepadActivity extends Activity { /* Récupération des éléments du GUI */ private Button hideShow = null; private Slider slider = null; private RelativeLayout toHide = null; private EditText editer = null; private TextView text = null; private RadioGroup colorChooser = null; private Button bold = null; private Button italic = null; private Button underline = null; private ImageButton smile = null; private ImageButton heureux = null; private ImageButton clin = null; /* Utilisé pour planter les smileys dans le texte */ private SmileyGetter getter = null; /* Couleur actuelle du texte */ private String currentColor = "#000000"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); getter = new SmileyGetter(this); // On récupère le bouton pour cacher/afficher le menu hideShow = (Button) findViewById(R.id.hideShow); // Puis on récupère la vue racine de l'application et on change sa couleur hideShow.getRootView().setBackgroundColor(R.color.background); // On rajoute un Listener sur le clic du bouton… hideShow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View vue) { // … pour afficher ou cacher le menu if(slider.toggle()) { // Si le Slider est ouvert… // … on change le texte en "Cacher" hideShow.setText(R.string.hide); }else { // Sinon on met "Afficher" hideShow.setText(R.string.show); } } }); // On récupère le menu toHide = (RelativeLayout) findViewById(R.id.toHide); // On récupère le layout principal slider = (Slider) findViewById(R.id.slider); // On donne le menu au layout principal slider.setToHide(toHide); // On récupère le TextView qui affiche le texte final text = (TextView) findViewById(R.id.text); // On permet au TextView de défiler text.setMovementMethod(new ScrollingMovementMethod()); // On récupère l'éditeur de texte editer = (EditText) findViewById(R.id.edit); // On ajoute un Listener sur l'appui de touches editer.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // On récupère la position du début de la sélection dans le texte int cursorIndex = editer.getSelectionStart(); // Ne réagir qu'à l'appui sur une touche (et pas au relâchement) if(event.getAction() == 0) // S'il s'agit d'un appui sur la touche « entrée » if(keyCode == 66) // On insère une balise de retour à la ligne editer.getText().insert(cursorIndex, "<br />"); return true; } }); // On ajoute un autre Listener sur le changement, dans le texte cette fois editer.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // Le Textview interprète le texte dans l'éditeur en une certaine couleur text.setText(Html.fromHtml("<font color=\"" + currentColor + "\">" + editer.getText().toString() + "</font>", getter, null)); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { } }); // On récupère le RadioGroup qui gère la couleur du texte colorChooser = (RadioGroup) findViewById(R.id.colors); // On rajoute un Listener sur le changement de RadioButton sélectionné colorChooser.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { // En fonction de l'identifiant du RadioButton sélectionné… switch(checkedId) { // On change la couleur actuelle pour noir case R.id.black: currentColor = "#000000"; break; // On change la couleur actuelle pour bleu case R.id.blue: currentColor = "#0022FF"; break; // On change la couleur actuelle pour rouge case R.id.red: currentColor = "#FF0000"; } /* * On met dans l'éditeur son texte actuel * pour activer le Listener de changement de texte */ editer.setText(editer.getText().toString()); } }); smile = (ImageButton) findViewById(R.id.smile); smile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // On récupère la position du début de la sélection dans le texte int selectionStart = editer.getSelectionStart(); // Et on insère à cette position une balise pour afficher l'image du smiley editer.getText().insert(selectionStart, "<img src=\"smile\" >"); } }); heureux =(ImageButton) findViewById(R.id.heureux); heureux.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // On récupère la position du début de la sélection int selectionStart = editer.getSelectionStart(); editer.getText().insert(selectionStart, "<img src=\"heureux\" >"); } }); clin = (ImageButton) findViewById(R.id.clin); clin.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //On récupère la position du début de la sélection int selectionStart = editer.getSelectionStart(); editer.getText().insert(selectionStart, "<img src=\"clin\" >"); } }); bold = (Button) findViewById(R.id.bold); bold.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View vue) { // On récupère la position du début de la sélection int selectionStart = editer.getSelectionStart(); // On récupère la position de la fin de la sélection int selectionEnd = editer.getSelectionEnd(); Editable editable = editer.getText(); // Si les deux positions sont identiques (pas de sélection de plusieurs caractères) if(selectionStart == selectionEnd) //On insère les balises ouvrante et fermante avec rien dedans editable.insert(selectionStart, "<b></b>"); else { // On met la balise avant la sélection editable.insert(selectionStart, "<b>"); // On rajoute la balise après la sélection (et après les 3 caractères de la balise <b>) editable.insert(selectionEnd + 3, "</b>"); } } }); italic = (Button) findViewById(R.id.italic); italic.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View vue) { // On récupère la position du début de la sélection int selectionStart = editer.getSelectionStart(); // On récupère la position de la fin de la sélection int selectionEnd = editer.getSelectionEnd(); Editable editable = editer.getText(); // Si les deux positions sont identiques (pas de sélection de plusieurs caractères) if(selectionStart == selectionEnd) //On insère les balises ouvrante et fermante avec rien dedans editable.insert(selectionStart, "<i></i>"); else { // On met la balise avant la sélection editable.insert(selectionStart, "<i>"); // On rajoute la balise après la sélection (et après les 3 caractères de la balise <b>) editable.insert(selectionEnd + 3, "</i>"); } } }); underline = (Button) findViewById(R.id.underline); underline.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View vue) { // On récupère la position du début de la sélection int selectionStart = editer.getSelectionStart(); // On récupère la position de la fin de la sélection int selectionEnd = editer.getSelectionEnd(); Editable editable = editer.getText(); // Si les deux positions sont identiques (pas de sélection de plusieurs caractères) if(selectionStart == selectionEnd) // On insère les balises ouvrante et fermante avec rien dedans editable.insert(selectionStart, "<u></u>"); else { // On met la balise avant la sélection editable.insert(selectionStart, "<u>"); // On rajoute la balise après la sélection (et après les 3 caractères de la balise <b>) editable.insert(selectionEnd + 3, "</u>"); } } }); } } |
Objectifs secondaires
Boutons à plusieurs états
En testant votre application, vous verrez qu'en cliquant sur un bouton, il conserve sa couleur et ne passe pas orange, comme les vrais boutons Android. Le problème est que l'utilisateur risque d'avoir l'impression que son clic ne fait rien, il faut donc lui fournir un moyen d'avoir un retour. On va faire en sorte que nos boutons changent de couleur quand on clique dessus. Pour cela, on va avoir besoin du 9-Patch visible à la figure suivante.
Comment faire pour que le bouton prenne ce fond quand on clique dessus ? On va utiliser un type de drawable que vous ne connaissez pas, les state lists. Voici ce qu'on peut obtenir à la fin :
1 2 3 4 5 6 | <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true" android:drawable="@drawable/pressed" /> <item android:drawable="@drawable/number" /> </selector> |
On a une racine <selector>
qui englobe des <item>
, et chaque <item>
correspond à un état. Le principe est qu'on va associer chaque état à une image différente. Ainsi, le premier état <item android:state_pressed="true" android:drawable="@drawable/pressed" />
indique que, quand le bouton est dans l'état « pressé », on utilise le drawable d'identifiant pressed
(qui correspond à une image qui s'appelle pressed.9.png
). Le second item, <item android:drawable="@drawable/number" />
, n'a pas d'état associé, c'est donc l'état par défaut. Si Android ne trouve pas d'état qui correspond à l'état actuel du bouton, alors il utilisera celui-là.
En parcourant le XML, Android s'arrêtera dès qu'il trouvera un attribut qui correspond à l'état actuel, et, comme je vous l'ai déjà dit, il n'existe que deux attributs qui peuvent correspondre à un état : soit l'attribut qui correspond à l'état, soit l'état par défaut, celui qui n'a pas d'attribut. Il faut donc que l'état par défaut soit le dernier de la liste, sinon Android s'arrêtera à chaque fois qu'il tombe dessus, et ne cherchera pas dans les <item>
suivants.
Internationalisation
Pour toucher le plus de gens possible, il vous est toujours possible de traduire votre application en anglais ! Même si, je l'avoue, il n'y a rien de bien compliqué à comprendre.
Gérer correctement le mode paysage
Et si vous tournez votre téléphone en mode paysage (Ctrl + F11 avec l'émulateur) ? Eh oui, ça ne passe pas très bien. Mais vous savez comment procéder, n'est-ce pas ?