• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 1/13/20

Dessinez sur une interface graphique

Log in or subscribe for free to enjoy all this course has to offer!

Pour dessiner sur l'écran ou pour donner une apparence personnalisée à un objet graphique, le principe est simple :

  1. créer un objet graphique ;

  2. intégrer sa classe personnalisée dans l'interface graphique ;

  3. redéfinir la méthode d'affichage onDraw.

Dans ce chapitre je vous propose de commencer par créer un classe graphique, puis voir comment y afficher quelque chose et de faire un petit exemple.

En fin de chapitre, nous discuterons rapidement de la classe SurfaceView.

Création d’un objet graphique

Pour créer un objet graphique, il suffit d'hériter d'un objet graphique existant. Mais il est nécessaire de fournir des constructeurs adaptés.

Hériter d'un objet graphique

Si l'on veut partir d'un objet le plus neutre possible, le choix est souvent d'hériter de la classe View, qui est la classe mère de tout objet graphique. Mais rien ne vous empêche d'hériter d'autres classe que vous voudriez spécialiser, comme un Button  ou un SeekBar.

public class MaClasseGraphique extends View {
....
}

Fournir un constructeur adapté

Dans l'état actuel, Android Studio n'est pas content (Méchant IDE ! :pirate:), car notre classe ne fournit pas de constructeur Kivabien(Oui, le "Kivabien" est aussi une métrique qualitative des constructeurs :ange:).

Pour savoir quel constructeur fournir, on peut exploiter l'IDE AndroidStudio pour le générer automatiquement. Il vous proposera spontanément 4 choix :

  • public MaClasseGraphique(Context context)  : ce constructeur est utilisable pour instancier un objet depuis du code Java. Le contexte sera alors une référence vers l'activité contenant cet objet. Ce cas de figure est assez peu courant.

  • public MaClasseGraphique(Context context, AttributeSet attrs)  : ce constructeur est celui utilisé lors d'une instanciation depuis le XML. C'est le cas le plus courant. Le second paramètre contiendra les paramètres définis dans la balise XML.

  • public MaClasseGraphique(Context context, AttributeSet attrs, int defStyleAttr):  ce constructeur ajoute la possibilité de faire un choix de style parmi les variations d'un thème.

  • public MaClasseGraphique(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes): ce constructeur a été rajouté dans l'API 21. Il permet également de préciser une ressource d'un style à utiliser.

Suivant comment vous écrirez votre programme, l'un ou l'autre des ces constructeurs pourra être appelé. Je vous suggère donc de tous les redéfinir, sans n'oubliant d'y appeler systématiquement le constructeur parent. Si vous aviez des choses spécifiques à faire dans votre constructeur, alors je vous suggère de faire une méthode séparée (init ?) et de l'appeler depuis chacun des constructeurs :

// Code visant une API 17, donc avec seulement 3 constructeurs définis
public MaClasseGraphique(Context context) {
super(context);
init();
}
public MaClasseGraphique(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MaClasseGraphique(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
// code commun à tous les constructeurs
}

Intégrer sa classe dans l'interface graphique

Pour placer dans le XML un objet venu d'ailleurs que la bibliothèque standard Android, il suffit d'utiliser le nom complet du type de l'objet comme balise XML. Cela revient à noter le nom du package en préfixe du nom du type. Par exemple, voici l'ajout d'une instance d'un MaClasseGraphique, dans un XML, sachant que le package contenant cette classe est oc.demos.customui.

<oc.demos.customui.MaClasseGraphique
android:id="@+id/maclasseamoi"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

Ok, je sais créer un objet graphique et le mettre dans mon interface graphique. Et même que mon application compile et que ça marche :D !

Mais euh ... on ne voit rien ... je viens de personnaliser le néant :'(.

Comment fait-on pour dessiner finalement ? 

Personnalisez l'apparence de vos objets via onDraw

Le plus gros du travail de modification de l'apparence de nos objets graphiques personnels va se faire dans la méthode onDraw, héritée de View, que nous allons redéfinir :

@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
// ...
}

Cette méthode est celle appelée par le système à chaque fois qu'il souhaite rafraichir le visuel de cet objet.

La méthode  onDraw  reçoit en paramètre un Canvas  totalement vierge et ayant exactement la dimension de notre objet (la dimension est généralement gérée par les Layout  parents et les paramètres du XML). Charge à nous de dessiner dans ce Canvas.

Pour dessiner sur un Canvas, c'est très simple, puisque l'objet fournit les méthodes pour lui dessiner dessus. On pourra faire l'analogie avec les ardoises magiques de notre jeunesse, où l'objet lui-même fournit le moyen de lui dessiner dessus.

Ardoise magique (source : commons.wikimedia.org)
Ardoise magique (source : commons.wikimedia.org)

Ainsi on pourra faire un appel à canvas.drawRect(...)  pour dessiner un rectangle sur le canvas ou canvas.drawLine(...)pour y dessiner une ligne.

Ok ... mais comment faire un rectangle plein ou vide, une ligne pleine ou pointillée, ou activer ou non l'aliasing ?

On utilise un objet de type Paint qui sera assimilable à un crayon. C'est lui qui contiendra toutes ces préférences de dessin. Contrairement à Java, où le « crayon » fait partie du Graphics  modifié, en Android, on peut créer différents crayons. Il faut donc préciser à chaque dessin le crayon à utiliser. Ainsi, la commande complète pour dessiner un rectangle est :

canvas.drawRect(x1,y1, x2, y2, crayon) ;

Pour créer un crayon, il suffit de construire un Paint, puis de le configurer. Je vous laisserai regarder plus largement les possibilités offertes par cette classe en lisant sa Javadoc (https://developer.android.com/reference/android/graphics/Paint.html), mais voici quelques exemples classiques :

// Exemple de crayon si on voulait dessiner un trait jaune
crayonTrait = new Paint();
crayonTrait.setColor(Color.YELLOW);
crayonTrait.setAntiAlias(true); // Faire de l'anti-aliasing
// Exemple de crayon si on voulait dessiner un rectangle noir plein
crayonRect = new Paint();
crayonRect.setColor(Color.BLACK);
crayonRect.setStyle(Paint.Style.FILL);
// Exemple d'un crayon utilisé pour écrire un texte, de manière centrée
// par rapport au point d'ancrage, avec une police de 50pt.
crayonTexte = new Paint();
crayonTexte.setTextAlign(Paint.Align.CENTER);
crayonTexte.setTextSize(50);
crayonTexte.setColor(Color.BLACK);

Il est aussi possible de dessiner des images dans votre Canvas.  Si l'on considère une imagemonimage.png  que vous auriez placée dans le dossier drawable  de votre projet, alors vous pouvez y faire référence via l'identifiant R.drawable.monimage, tandis que les images fournies par la bibliothèque Android sont accessibles via android.R.drawable.btn_star_big_on  par exemple. Pour dessiner une image, il faut d'abord récupérer une référence vers un objet.

Pour récupérer un objet de type Drawable  à partir de l'identifiant de l'image, on va passer par l'appel système :

Drawable d = getResources().getDrawable(android.R.drawable.btn_star_big_on);

Ensuite, il faut définir la taille de l'objet :

d.setBounds(0,getHeight()-30,30,getHeight());

Et enfin, on peut dire au Drawable  de se dessiner sur le Canvas (c'est donc l'inverse des autres actions de dessin) :

d.draw(canvas);

Petit exemple concret

Voici un petit exemple complet d'un bouton qui, lorsqu'il s'affiche, dessine un petit cercle vert en haut à gauche et écrit un petit « Beta » en bas à droite, en plus de son texte normal.

Exemple de rendu visé
Exemple de rendu visé

Comme décrit, la classe BoutonPerso doit hériter de  Button, fournir au moins le constructeur à 2 paramètres et redéfinir la méthode onDraw.

// Aurait du être :
// public class BoutonPerso extends Button {
//
// Mais pour des questions de compatibilité :
import android.support.v7.widget.AppCompatButton;
public class BoutonPerso extends AppCompatButton {
public BoutonPerso(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){ /* ... */ }
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
}
}

On voudrait que la méthode onDraw  fasse les 2 tracés décrits. On fera donc appel à drawCirclepour faire le disque et à drawTextpour écrire "Beta".

@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
// Tracé du disque en haut à gauche
c.drawCircle(8, 8, 4, pCercle);
// Écriture du texte "Beta" en bas à droite
c.drawText("Beta", getWidth(), getHeight()-pBeta.getTextSize(), pBeta);
}

Il est donc nécessaire de créer les "crayons"  adaptés. Ils seront idéalement mis en attributs et initialisés dans la méthode init.

private Paint pCercle = new Paint();
private Paint pBeta = new Paint();
private void init(){
// Configuration d'un crayon pour un disque Vert
pCercle.setColor(Color.GREEN);
pCercle.setStyle(Paint.Style.FILL);
pCercle.setAntiAlias(true);
// Configuration d'un crayon pour un texte rouge
pBeta.setTextAlign(Paint.Align.RIGHT);
pBeta.setTextSize(12);
pBeta.setColor(Color.RED);
}

Une fois cette classe faite, il suffit d'en intégrer une instance dans le XML et d'exécuter l'application ! :)

<oc.demos.customui.BoutonPerso
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ceci est un bouton perso"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="parent"
/>

Besoin d'optimisations ? Utilisez SurfaceView.

 Le type SurfaceView  est dédié aux usages où le rafraichissement de l'interface graphique est calculé par un processus séparé. Il a pour but de limiter le scintillement et de faciliter l'interaction entre un processus séparé et l'interface graphique (comme cela sera discuté en Partie 3, l'interaction directe entre l'un et l'autre mène immédiatement à une exception).

Dans les grandes lignes, son usage est assez similaire à ce qui a été précédemment décrit, puisqu'il suffit d'hériter de SurfaceView  et qu'il faut redéfinir public void draw(Canvas canvas)  (et non pas onDraw). Il faut ensuite intégrer l'objet dans son interface graphique (via le XML par exemple) et on a l'impression que c'est finalement exactement pareil.

Seulement, comme le rôle de cet objet est de permettre son accès à plusieurs processus, ce qui change est que la méthode draw  n'est pas appelée par le système. C'est à nous de gérer les verrous permettant de garantir l'accès atomique à la partie partagée. Ces verrous sont gérés via unSurfaceHolder.

Par ailleurs, si l'affichage doit être mis à jour par un processus, alors celui-ci doit probablement être notifié des événements liés à la surface, comme sa création, sa destruction ou sa mise à jour structurelle (changement de taille). Il faut donc créer un objet répondant au contrat SurfaceHolder.Callbacket l'enregistrer en tant qu'écouteur auprès du SurfaceHolder.

Ainsi, en plus des actions dans le cas général, il faut :

  1. récupérer une référence vers le SurfaceHolder ;

  2. créer un écouteur de type SurfaceHolder.Callback, l'enregistrer auprès duSurfaceHolder (au moins pour le lancement et l'arrêt du processus d'affichage) ;

  3. lancer manuellement le rafraichissement de l'affichage en verrouillant son accès via quelque chose du type :

Canvas drawingCanvas = myHolder.lockCanvas();
mySurfaceView.draw(drawingCanvas);
myHolder.unlockCanvasAndPost(drawingCanvas);

Example of certificate of achievement
Example of certificate of achievement