Licence CC BY-NC-SA

TP : un bloc-notes

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

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

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.

Voici à quoi va ressembler l'application

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 les TextView.

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).

Le bouton « Afficher »

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.

Le texte est mis en forme en temps téel dans la zone de prévisualisation

Spécifications techniques

Fichiers à utiliser

On va d'abord utiliser les smileys du Site du Zéro : :) :D ;).

Pour les boutons, j'ai utilisé les 9-patches visibles à la figure suivante.

Carré bleu Carré vert

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

<b>Le texte</b>

Écrire en italique

<i>Le texte</i>

Souligner du texte

<u>Le texte</u>

Insérer une image

<img src="Nom de l'image">

Changer la couleur de la police

<font color="Code couleur">Le texte</font>

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.

Le Logcast est ouvert

La première chose à faire, c'est de cliquer sur le troisième bouton en haut à droite (voir figure suivante).

Cliquez sur le troisième bouton

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.

Cette liste déroulante permet d'afficher dans le Logcat les messages que vous souhaitez

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.

Une liste d'erreurs s'affiche

À la figure suivante, on peut voir le message que j'avais inséré.

le message que j'avais inséré s'affiche bien

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.

L'application a planté, il suffit de regarder le message pour savoir où

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>");
        }
      }
    });
  }
}

Télécharger le projet

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.

Ce bouton va nous permettre de modifier la couleur d'un bouton appuyé

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 ? :)