Créer son propre objet graphique sous Android

ram-0000

Ce document a pour but de décrire la réalisation sous Android d'un nouvel objet graphique afin de répondre à un besoin spécifique.

Votre avis et vos suggestions sur ce tutoriel m'intéressent ! Alors après votre lecture, n'hésitez pas : Commentez

- Titre : Créer son propre objet graphique sous Android

- Auteur : ram-0000

- Parution : 6 décembre 2012

- Licence : Copyright ® 2008-2012 ram-0000. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Description du besoin fonctionnel

- 3. L'implémentation de base

- 3-1. Les fonctions et attributs « Work »

- 3-2. Les fonctions et attributs « View »

- 3-3. Le code complet de la classe « GuiIndicatorPosition »

- 3-4. La création d'un objet « GuiPositionIndicator »

- 3-5. Le code de l'activité

- 4. Exemple d'un projet complet

- 4-1. Fonctionnement de l'exemple

- 4-2. L'objet texte

- 4-2-1. La déclaration

- 4-2-2. L'initialisation de l'objet texte

- 4-2-3. Le gestionnaire d'événements

- 4-3. L'objet « GuiPositionIndicator »

- 4-3-1. La déclaration

- 4-3-2. La partie graphique

- 4-3-3. L'initialisation

- 4-3-4. La modification de la position

- 5. La parole est maintenant aux graphistes

- 6. Conclusions

1 - Introduction

Lors d'un développement pour Android, j'ai eu besoin d'un objet graphique et les objets graphiques proposés par le SDK Android ne me convenaient pas.

J'avais besoin d'un indicateur de position afin d'aider l'utilisateur à se repérer lors de sa navigation dans les différents écrans et les seuls objets tout faits et disponibles les plus proches étaient la « scroll bar » ou la « progress bar ». Outre le fait que le graphisme ne me plaisait pas, ces objets ne répondaient pas à mon besoin qui était, comme sur une carte plane de la terre, lorsque l'on va continuellement vers la droite, on revient par la gauche, « MAX + 1 = MIN » et « MIN -1 = MAX », cet indicateur de position devait pouvoir « tourner ».

J'ai donc commencé à regarder comment créer de toutes pièces un tel objet et ce tutoriel est le résultat de cette recherche.

Cet article est organisé comme suit :

- le paragraphe 2 spécifie mon besoin fonctionnel ;

- le paragraphe 3 décrit l'implémentation de base ;

- le paragraphe 4 décrit un projet de test qui utilise cet objet graphique ;

- le paragraphe 5 laisse libre cours aux graphistes afin qu'ils expriment leur créativité ;

- et enfin, le chapitre 6 se propose de conclure cet article.

2 - Description du besoin fonctionnel

Dans le programme e-verbe pour Android, il y a un champ texte qui affiche le temps conjugué.

Lorsque l'utilisateur effectue un balayage horizontal sur ce champ texte, cela permet de changer le temps de la conjugaison (présent, imparfait, passé simple, futur simple…).

Lorsque l'utilisateur effectue un balayage vertical sur ce champ texte, cela permet de changer le mode de la conjugaison (indicatif, subjonctif, impératif, conditionnel…).

De plus, lorsque l'utilisateur est au dernier temps (ou mode) et qu'il continue son balayage vers la droite (ou vers le bas), il doit revenir au premier temps (ou mode).

De même, lorsque l'utilisateur est au premier temps (ou mode) et qu'il continue son balayage vers la gauche (ou vers le haut), il doit passer au dernier temps (ou mode).

Le besoin est donc de créer un indicateur de position qui permette à l'utilisateur de se repérer dans ses balayages.

De plus, tous les modes de conjugaison n'ont pas le même nombre de temps (huit temps pour l'indicatif et deux temps pour l'impératif par exemple). Il faut donc un indicateur de position qui puisse se reconfigurer de manière dynamique.

Il y a bien les objets « scroll bar » ou « progress bar »qui existent dans le SDK Android mais ils ont plus une notion de « restant à parcourir » par rapport à « déjà parcouru » qu'une notion de « position courante » parmi un « ensemble de positions ».

Et en plus, ils répondaient difficilement à mon besoin de faire tourner ma position « MAX + 1 = MIN » et « MIN -1 = MAX ».

La copie d'écran ci-dessous montre le résultat obtenu :

Il y a deux approches possibles de cette problématique :

- l'indicateur de position est un objet actif sur lequel l'utilisateur peut agir. C'est donc l'indicateur de position qui est activé par l'utilisateur et c'est cet indicateur de position qui va demander à la vue de se rafraîchir en fonction de la position courante de l'indicateur de position ;

- l'indicateur de position est un objet passif sur lequel l'utilisateur ne peut pas agir. C'est la vue qui est activée par l'utilisateur et qui demande à l'indicateur de position de refléter la position courante.

C'est cette deuxième approche que j'ai choisi d'utiliser.

3 - L'implémentation de base

La classe de base de ce nouvel objet graphique est l'objet « GuiPositionIndicator » qui dérive de l'objet « View ».

Les fonctions et attributs implémentés par cette classe peuvent se classer en deux catégories (outre leur caractère privé, public ou protégé) :

- ceux nécessaires aux fonctionnalités à implémenter. Les noms de ces fonctions sont préfixés par les caractères « Work » ;

- ceux nécessaires et imposés par la classe « View ». Les noms de ces fonctions ne sont pas préfixés car leurs noms sont imposés par la classe « View » ;

- les attributs (variables membres) sont en plus préfixés par les caractères « m_ ».

3-1 - Les fonctions et attributs « Work »

Les fonctions et attributs « Work » à implémenter sont :

Nom

Rôle

private void WorkInternalInit()

Exécute l'initialisation des membres de la classe. Cette fonction est appelée par les constructeurs

public void WorkRolloverYes()

Positionne le mode rollover (MAX + 1 = MIN et MIN - 1 = MAX)

public void WorkRolloverNo()

Annule le mode rollover (MAX + 1 = MAX et MIN - 1 = MIN)

public final boolean WorkRollover()

Indique si le mode rollover est positionné ou non

private boolean m_WorkRollover

Variable mémorisant le statut du mode rollover

public void WorkSize(int Size)

Fonction permettant d'indiquer le nombre de positions possibles

public final int WorkSize()

Fonction retournant le nombre de positions possibles

private int m_WorkSize

Variable mémorisant le nombre de positions possibles

public void WorkPos(int Pos)

Fonction permettant d'indiquer la position courante

public final int WorkPos()

Fonction permettant de récupérer la position courante

private int m_WorkPos

Variable mémorisant la position courante

public void WorkPosNext()

Fonction permettant d'avancer d'une position la position courante

public void WorkPosPrevious()

Fonction permettant de reculer d'une position la position courante

3-2 - Les fonctions et attributs « View »

Les fonctions et attributs « View » à implémenter sont :

Nom

Rôle

public GuiPositionIndicator(Context context)

Constructeur simple pour créer l'objet à partir du code

public GuiPositionIndicator(Context context, AttributeSet attrs)

Constructeur utilisé pour créer l'objet à partir d'un fichier XML de ressources

public GuiPositionIndicator(Context context, AttributeSet attrs, int defStyle)

Constructeur utilisé pour créer l'objet à partir d'un fichier XML de ressources tout en appliquant un style spécifique

protected void onDraw(Canvas canvas)

Fonction appelée par Android pour redessiner l'objet graphique

3-3 - Le code complet de la classe « GuiIndicatorPosition »

Comme on peut le voir l'interface de la classe est très simple, il y a quelques fonctions nécessaires à la classe View. Les autres fonctions sont spécifiques à la fonctionnalité à implémenter.

Pour l'instant, le code de la fonction onDraw(), qui est en charge d'afficher l'objet graphique, est minimal. Il se contente de dessiner sa surface en vert et d'afficher la position courante par rapport au nombre de positions possibles. C'est dans cette fonction que les graphistes pourront s'exprimer.

Vous remarquerez aussi que l'on ne redessine pas l'objet graphique dans les fonctions « public void WorkSize(int Size) » et « public void WorkPos(int Pos) ». La vue est simplement invalidée par un appel à la fonction « invalidate') ». C'est le framework qui se chargera d'appeler la fonction « protected void onDraw(Canvas canvas) » dès que possible.

Le code de la classe « GuiIndicatorPosition » est le suivant :

1. package fr.circitor.test;

2. import android.content.Context;

3. import android.graphics.Canvas;

4. import android.graphics.Color;

5. import android.graphics.Paint;

6. import android.util.AttributeSet;

7. import android.view.View;

9. public class GuiPositionIndicator extends View

10. {

11. // Constructeur simple pour créer l'objet à partir du code

12. public GuiPositionIndicator(Context context)

13. {

14. super(context);

15. WorkInternalInit();

16. }

18. // Constructeur utilisé pour créer l'objet à partir d'un fichier XML de ressources

19. public GuiPositionIndicator(Context context, AttributeSet attrs)

20. {

21. super(context, attrs);

22. WorkInternalInit();

23. }

25. // Constructeur utilisé pour créer l'objet à partir d'un fichier XML de ressources tout en appliquant un style spécifique

26. public GuiPositionIndicator(Context context, AttributeSet attrs, int defStyle)

27. {

28. super(context, attrs, defStyle);

29. WorkInternalInit();

30. }

32. // Fonction appelée par Android pour redessiner l'objet graphique

33. protected void onDraw(Canvas canvas)

34. {

35. // crée une couleur verte

36. Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

37. paint.setColor(Color.GREEN);

38. paint.setStyle(Paint.Style.FILL_AND_STROKE);

40. // dessine un rectangle

41. canvas.drawRect(0, 0, getWidth(), getHeight(), paint);

43. // le texte à afficher

44. String text = "Position=" + Integer.toString(WorkPos()) + ", Taille=" + Integer.toString(WorkSize());

46. // la couleur du text

47. paint.setColor(Color.BLACK);

49. // affichage du texte

50. canvas.drawText(text, 10, getHeight() / 2, paint);

51. }

53. // Execute l'initialisation des membres de la classe, appelée par le constructeur

54. private void WorkInternalInit()

55. {

56. WorkSize(0);

57. WorkPos(0);

58. WorkRolloverYes();

59. }

61. // Positionne le mode rollover (MAX + 1 = MIN et MIN ? 1 = MAX)

62. public void WorkRolloverYes() { m_WorkRollover = true; }

64. // Annule le mode rollover (MAX + 1 = MAX et MIN ? 1 = MIN)

65. public void WorkRolloverNo() { m_WorkRollover = false; }

67. // Indique si le mode rollover est positionné ou non

68. public final boolean WorkRollover() { return m_WorkRollover; }

70. // Variable mémorisant le statut du mode rollover

71. private boolean m_WorkRollover;

73. // Fonction permettant d'indiquer le nombre de positions possibles

74. public void WorkSize(int Size)

75. {

76. m_WorkSize = Size;

77. invalidate();

78. }

80. // Fonction retournant le nombre de positions possibles

81. public final int WorkSize() { return m_WorkSize; }

83. // Variable mémorisant le nombre de positions possibles

84. private int m_WorkSize;

86. // Fonction permettant d'indiquer la position courante

87. public void WorkPos(int Pos)

88. {

89. m_WorkPos = Pos;

90. invalidate();

91. }

93. // Fonction permettant de récupérer la position courante

94. public final int WorkPos() { return m_WorkPos; }

96. // Variable mémorisant la position courante

97. private int m_WorkPos;

99. // Fonction permettant d'avancer d'une position la position courante

100. public void WorkPosNext()

101. {

102. int current = WorkPos() + 1;

103. if(current == WorkSize())

104. {

105. if(WorkRollover() == false) current--;

106. else current = 0;

107. }

108. WorkPos(current);

109. }

111. // Fonction permettant de reculer d'une position la position courante

112. public void WorkPosPrevious()

113. {

114. int current = WorkPos() - 1;

115. if(current == -1)

116. {

117. if(WorkRollover() == false) current++;

118. else current = WorkSize() - 1;

119. }

120. WorkPos(current);

121. }

122. }

3-4 - La création d'un objet « GuiPositionIndicator »

L'objet « GuiPositionIndicator » est créé directement à partir du fichier XML de ressources comme ceci :

1. <view

2. class="fr.circitor.test.GuiPositionIndicator"

3. android:id="@+id/IdGuiPositionIndicator"

4. android:layout_width="fill_parent"

5. android:layout_height="40sp"

6. />

C'est le nom de l'attribut « Class » qui fait le lien avec notre classe « GuiIndicatorPosition ».

3-5 - Le code de l'activité

L'initialisation de la classe GuiPositionIndicator peut être faite dans le constructeur de l'activité :

1. package fr.circitor.test;

2. import android.app.Activity;

3. import android.os.Bundle;

4. import android.view.View;

5. import android.widget.TextView;

7. public class TestActivity extends Activity

8. {

9. public void onCreate(Bundle savedInstanceState)

10. {

11. super.onCreate(savedInstanceState);

12. setContentView(R.layout.main);

14. // ajoute un texte dans le TextView

15. TextView tv = (TextView)findViewById(R.id.IdTextView);

16. tv.setText("Cliquez-moi !!");

18. // ajoute un handler de clic sur le TextView

19. tv.setOnClickListener(m_handler);

21. // récupère l'indicateur de position

22. m_ctrl = (GuiPositionIndicator)findViewById(R.id.IdGuiPositionIndicator);

24. // initialise sa taille et sa position

25. m_ctrl.WorkSize(10);

26. m_ctrl.WorkPos(4);

27. }

29. private GuiPositionIndicator m_ctrl;

30. private View.OnClickListener m_handler = new View.OnClickListener()

31. {

32. public void onClick(View v)

33. {

34. // sur un clic dans le text view, on fait avancer l'indicateur de position

35. m_ctrl.WorkPosNext();

36. }

37. };

38. }

Vous remarquerez que :

- l'initialisation des valeurs du contrôle « GuiPositionIndicator » est faite de manière très simple dans le constructeur de l'activité ;

- il y a un EventListener qui écoute les clics de l'utilisateur. À chaque clic sur le texte, la position courante est avancée.

4 - Exemple d'un projet complet

Un projet complet utilisant l'objet graphique « GuiPositionIndicateur peut être téléchargé ici. Vous remarquerez qu'il ne contient aucune fioriture, c'est le projet le plus simple qui soit.

4-1 - Fonctionnement de l'exemple

Le fonctionnement de l'exemple est assez simple.

Il y a deux objets graphiques :

- une fenêtre de texte (un objet Android « textView ») en gris qui affiche la position courante ainsi que la taille de l'indicateur de position ;

- l'indicateur de position (un objet « GuiPositionIndicator ») juste en dessous qui affiche sous forme graphique la position courante et la taille.

4-2 - L'objet texte

4-2-1 - La déclaration

L'objet texte est déclaré directement dans les ressources du projet :

1. <TextView

2. android:layout_width="fill_parent"

3. android:layout_height="140sp"

4. android:background="#FF555555"

5. android:id="@+id/IdTextView"

6. />

4-2-2 - L'initialisation de l'objet texte

L'initialisation de l'objet texte dans la fonction onCreate() de l'activité est basique :

1. // ajoute un texte dans le TextView

2. m_tv = (TextView)findViewById(R.id.IdTextView);

4. // ajoute un handler sur le TextView

5. m_tv.setOnTouchListener(m_handler);

Une fonction dédiée, la fonction RefreshText(), permet d'afficher le texte de la fenêtre ainsi que sa position courante :

1. private void RefreshText()

2. {

3. // affiche un texte ainsi que la position courante

4. m_tv.setText("Faites un balayage droite/gauche !!\n\nPosition=" + m_ctrl.WorkPos() + "/" + m_ctrl.WorkSize());

5. }

4-2-3 - Le gestionnaire d'événements

La partie la plus intéressante de l'objet texte est probablement le gestionnaire d'événements qui permet la gestion du balayage :

1. // le handler d'événements de la fenêtre texte

2. private View.OnTouchListener m_handler = new View.OnTouchListener()

3. {

4. public boolean onTouch(View v, MotionEvent event)

5. {

6. Log.i("onTouchEvent", "event=" + event.toString());

7. if(event.getX() > v.getWidth())

8. return true;

9. if(event.getY() > v.getHeight())

10. return true;

12. if(event.getAction() == MotionEvent.ACTION_DOWN)

13. {

14. Log.i("onTouchEvent", "DOWN");

15. if(m_flag_swipe_is_on == true)

16. return true;

17. m_flag_swipe_is_on = true;

18. m_x_swipe = event.getX();

19. return true;

20. }

22. if(event.getAction() == MotionEvent.ACTION_MOVE)

23. {

24. Log.i("onTouchEvent", "MOVE");

25. if(m_flag_swipe_is_on == false)

26. return true;

28. float delta_x = event.getX() - m_x_swipe;

29. Log.i("onTouch", "Move, dx=" + delta_x);

31. // seul un déplacement plus grand que 10 est pris en compte

32. if(StrictMath.abs(delta_x) < m_move_sensibility_x)

33. return true;

34. if(delta_x > 0)

35. m_ctrl.WorkPosNext();

36. else

37. m_ctrl.WorkPosPrevious();

39. m_x_swipe = event.getX();

40. RefreshText();

41. return true;

42. }

44. if(event.getAction() == MotionEvent.ACTION_UP)

45. {

46. Log.i("onTouchEvent", "UP");

47. if(m_flag_swipe_is_on == false)

48. return true;

49. m_flag_swipe_is_on = false;

50. return true;

51. }

53. if(event.getAction() == MotionEvent.ACTION_CANCEL)

54. {

55. Log.i("onTouchEvent", "CANCEL");

56. if(m_flag_swipe_is_on == false)

57. return true;

58. m_flag_swipe_is_on = false;

59. return true;

60. }

62. Log.i("onTouchEvent", "UNKNOWN");

63. return true;

64. }

65. };

67. // variable qui enregistre si un balayage est en cours

68. private boolean m_flag_swipe_is_on;

70. // variable qui enregistre la dernière position du balayage

71. private float m_x_swipe;

73. // variable qui permet de gérer la sensibilité du balayage. Plus cette valeur est petite, plus le balayage est sensible

74. private final int m_move_sensibility_x = 25;

4-3 - L'objet « GuiPositionIndicator »

4-3-1 - La déclaration

L'objet « GuiPositionIndicator est déclaré directement dans les ressources du projet :

1. <view

2. class="fr.circitor.test.GuiPositionIndicator"

3. android:id="@+id/IdGuiPositionIndicator"

4. android:layout_width="fill_parent"

5. android:layout_height="40sp"

6. />

Il n'y a rien de particulier à signaler dans cette déclaration mise à part la ligne « class= » qui fait le lien entre cet objet graphique et la classe « GuiPositionIndicator ».

4-3-2 - La partie graphique

La partie « artistique » de l'objet GuiPositionIndicator, se trouve sans surprise dans la fonction « onDraw() » :

1. // Fonction appelée par Android pour redessiner l'objet graphique

2. protected void onDraw(Canvas canvas)

3. {

4. super.onDraw(canvas);

6. // verifie s'il y a quelque chose

7. if(canvas == null) return;

8. if(WorkSize() == 0) return;

9. //Log.d("GuiPositionIndicator", "onDraw()");

11. // paint color

12. Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

13. paint.setColor(Color.RED);

14. paint.setStyle(Paint.Style.STROKE);

16. // get sizes of the object

17. float width = getWidth();

18. //Log.d("GuiPositionIndicator", "width=" + width);

19. float height = getHeight();

20. //Log.d("GuiPositionIndicator", "height=" + height);

22. // check size to determine if horizontal or vertical view

23. boolean flag_horizontal = true;

24. if(width < height)

25. flag_horizontal = false;

27. // compute the radius

28. float radius = width * radiusRatio();

29. if(flag_horizontal == true)

30. radius = height * radiusRatio();

32. // compute the total size

33. // 2 radius per circle to draw

34. float size = WorkSize() * 2;

36. // add space between circles

37. if(WorkSize() > 1)

38. size += (WorkSize() - 1) * nbRadiusBetweenCircle();

40. // multiply by the size of the radius

41. size *= radius;

43. // and divide it by 2

44. size /= 2;

46. float x = width / 2;

47. float y = height / 2;

48. if(flag_horizontal == true)

49. x -= size;

50. else

51. y -= size;

53. // compute offset between circles

54. float offset = (nbRadiusBetweenCircle() + 2) * radius;

56. // draw circles

57. for(int boucle = 0; boucle != WorkSize(); boucle++)

58. {

59. boolean fill_it = false;

60. if(WorkPos() == boucle)

61. fill_it = true;

62. circle(canvas, x, y, radius, fill_it);

64. if(flag_horizontal == true)

65. x += offset;

66. else

67. y += offset;

68. }

69. }

71. private final float radiusRatio() { return (float)0.20; }

72. private final float nbRadiusBetweenCircle() { return (float)0.7; }

73. private final int circleEmptyColor() { return Color.GRAY; }

74. private final int circleFullColor() { return Color.WHITE; }

76. private void circle(Canvas canvas, float X, float Y, float Radius, boolean Fill)

77. {

78. // paint color

79. Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

80. if(Fill == false)

81. {

82. paint.setStyle(Paint.Style.STROKE);

83. paint.setColor(circleEmptyColor());

84. }

85. else

86. {

87. paint.setStyle(Paint.Style.FILL_AND_STROKE);

88. paint.setColor(circleFullColor());

89. }

91. // draw circle

92. canvas.drawCircle(X, Y, Radius, paint);

93. }

4-3-3 - L'initialisation

L'initialisation de l'objet « GuiPositionIndicator » se fait dans la fonction onCreate() de l'activité :

1. public class TestActivity extends Activity

2. {

3. // l'objet GuiPositionIndicator

4. private GuiPositionIndicator m_ctrl;

6. public void onCreate(Bundle savedInstanceState)

7. {

8. ...

9. // récupère l'indicateur de position

10. m_ctrl = (GuiPositionIndicator)findViewById(R.id.IdGuiPositionIndicator);

12. // initialise la taille et la position de l'indicateur (pour cet exemple, ces valeurs sont arbitraires)

13. m_ctrl.WorkSize(10);

14. m_ctrl.WorkPos(4);

15. ...

4-3-4 - La modification de la position

La modification de la position de l'objet « GuiPositionIndicator » se fait dans le gestionnaire d'événements de la fenêtre de texte :

1. ...

2. if(delta_x > 0)

3. m_ctrl.WorkPosNext();

4. else

5. m_ctrl.WorkPosPrevious();

6. ...

5 - La parole est maintenant aux graphistes

Voilà, la « tambouille » Android est maintenant terminée, c'est aux graphistes d'exprimer leur créativité en remplaçant le code de la fonction onDraw(), cette tâche est largement au-dessus de mes possibilités.

À titre d'exemple, voici quelques compteurs qui pourraient être dessinés :

6 - Conclusions

Cet article est maintenant terminé et j'espère qu'il vous a montré qu'il est très facile d'implémenter un nouveau type d'objet.

Je suis complètement débutant avec cet environnement de développement et j'avoue que j'ai été agréablement surpris des possibilités graphiques qui sont offertes et des extensions possibles et faciles sous Android malgré un aspect un peu « rustique » au premier abord.

Votre avis et vos suggestions sur ce tutoriel m'intéressent ! Alors après votre lecture, n'hésitez pas : Commentez

Je tiens à remercier ClaudeLELOUP pour ses conseils avisés lors de la relecture de ce tutoriel.