Introduction à JAVA

 Animation des applets

8.1 Les Threads

Avec un langage non multitâche, on réalise une animation en créant une boucle sans fin dans laquelle on met régulièrement à jour l'affichage. Cette méthode présente l'inconvénient majeur d'occuper 100% du temps du processeur.
JAVA peut  gérer plusieurs tâches simultanées : Lorsque la machine virtuelle exécute une applet classique elle gère en fait plusieurs processus dont l'exécution du code de l'applet et la gestion dynamique de la mémoire. Pour réaliser une animation, il faut ajouter à l'applet un processus ( Thread ) supplémentaire.

Un thread possède son propre flot d'instruction indépendant de celui de l'applet et dont le déroulement est le suivant. Une méthode start( ) démarre la méthode run( ) qui contient le code à exécuter par le processus. Une méthode stop( ) permet de terminer d'exécution de la méthode run( ). Pendant son exécution un thread peut être endormi pour une certaine durée [ sleep( ) ], mis en attente [ suspend( ) ], réactivé [ resume( ) ].
Rappel : Le navigateur lance la méthode start( ) à chaque fois que le cadre de l'applet devient visible et la méthode stop( ) quand ce cadre disparaît de l'affichage.

 8.2 Modèle d'animation simple

Pour la mise en place d'un thread, on commence par ajouter dans l'entête de l'applet " implements Runnable " qui signifie que l'on va implémenter la méthode abstraite run( ) de la classe Runnable (une interface de java.lang). On crée ensuite comme variable de classe de l'applet une instance d'un objet Thread . On surcharge les méthodes start( ) et stop( ) . Dans la méthode start( ), on teste l'existence du thread : on le crée s'il n'existe pas puis on active la méthode start( ) par [nom_thread.start( )] de ce thread [lancement de la méthode run( )] puis on implémente la méthode run( ) .
Pour un processus très simple, la méthode run( ) peut se résumer à une boucle sans fin qui appelle successivement la méthode repaint( ) de l'applet puis met le thread en sommeil. L'appel à la méthode sleep peut générer une exception : il faut impérativement traiter celle-ci au moyen de la séquence try, catch prévue pour le traitement des exceptions en JAVA. Par contre le bloc catch peut être vide.

 Dans l'animation proposée en exemple, on se contente de déplacer une ellipse.

import java.applet.*;
import java.awt.*; 

public class anima1 extends Applet implements Runnable
{  Thread runner = null;         //variables de classe
    double t;

  public void init()
   {  setBackground (Color.lightGray);}

  public void paint(Graphics g)
  {  int X=140+(int)(120*Math.sin(t));   //calcul de la position
     t+=0.05;              //déplacement lors de l'appel suivant
     g.fillOval(X,20,50,100);}

  public void start()       //surcharge de la méthode
   { if (runner == null)     //test d'existence
     {  runner = new Thread(this);   //création , this désigne l'applet
        runner.start();}}   // lancement de la méthode run( )

  public void stop()
  { if (runner != null)
    {  runner.stop();
       runner = null;}} //destruction

   public void run()
   { while (true)        // boucle sans fin
     { try               //action à réaliser
        { repaint();     // redessiner
          Thread.sleep(20);}   //pause de 20 ms
       catch (InterruptedException e)
       { stop();}}}   //traitement (facultatif) de l'exception
}

J'ai fait volontairement le choix de présenter une animation dont le résultat n'est pas très satisfaisant. L'image scintille et des traces grises traversent régulièrement l'ellipse. Pour comprendre l'origine de ce phénomène, il faut se souvenir que la méthode repaint( ) commence par appeler la méthode update( Graphics g) qui efface le cadre de l'applet. Comme cet effacement qui est à l'origine des défauts constatés la solution est de surcharger la méthode update( ) .

 8.3 La méthode du double tampon

Les auteurs de manuels sur JAVA proposent différentes méthodes pour remédier à ces défauts : effacer uniquement les parties mobiles, clipping, double tampon ... Après avoir expérimenté toutes ces techniques, je considère que seule la méthode du double tampon est efficace à 100 % et que les autres sont du bricolage souvent plus difficile à mettre en oeuvre que cette méthode.

Principe :  Le principe de cette technique d'affichage consiste à préparer la nouvelle image à visualiser dans un tampon (zone de la mémoire) puis à l'afficher à l'écran quand elle est entièrement terminée. L'affichage correspond à la copie d'une zone mémoire dans une autre. Tous les processeurs modernes sont dotés d'instructions spécifiques pour effectuer ces transferts et leur durée est très brève.
Méthode à utiliser :
Il faut déclarer une image et le contexte graphique associé. Ces deux objets sont ensuite crées dans init( ) car c'est à ce moment seulement que la taille de la fenêtre de l'applet est connue. Le dessin est réalisé dans le nouveau contexte graphique par la méthode paint( ). Pour terminer celle-ci, on transfère l'image préparée dans le contexte graphique de l'applet avec la méthode drawImage( ). Les arguments de cette méthode sont le nom de la zone mémoire, les coordonnées du coin supérieur gauche, la couleur du fond et un objet observateur d'image . Comme ici on a la certitude que la totalité de l'image réside en mémoire, on peut se contenter de celui de l'applet (utilisation de this). Il faut ensuite surcharger la méthode update( ).

Si la dimension de la fenêtre de votre navigateur est suffisante, vous pouvez comparer les deux applets.

import java.applet.*;
import java.awt.*;

public class anima2 extends Applet implements Runnable
{   private Thread runner = null;
     Image ima;      Graphics h;     //déclarations
     int la,ha;
     double t;

    public void init()
     {  setBackground (Color.lightGray);
        la=size().width;        ha=size().height; //taille de l'applet
        ima=createImage(la,ha); //creation d'une zone la*ha pixels
        h=ima.getGraphics();}    //contexte graphique associé à l'image

     public void update(Graphics g)   //surcharge indispensable
      {  paint(g);}

     public void paint(Graphics g)
      {  h.clearRect(0,0,la,ha);   //effacement de la zone de dessin
         int X=140+(int)(120*Math.sin(t));
         t+=0.05;
         h.fillOval(X,20,50,100);
         g.drawImage(ima,0,0,Color.white,this);} //transfert de ima vers l'écran

 //les méthodes start, stop et run sont identiques à celles de l'exemple précédent

    public void destroy()   //voir la remarque ci-dessous
     {  h.dispose();    ima.flush();}
}

Cette technique peut également être utilisée (même en dehors des animations) à chaque fois que l'on oblige l'applet à se redessiner avec une fréquence élevée.

 Remarques :
1. Le gestionnaire de mémoire (ramasse-miettes) ne gère pas automatiquement la destruction des objets Images ni des contextes graphiques. L'utilisation de la méthode destroy( ) permet au programmeur de terminer proprement son programme en assurant une libération correcte de la mémoire avec les méthodes dispose( )  et flush( ) .
2. J'ai suivi l'usage de nommer cette technique "double tampon" alors que l'on utilise en fait un seul tampon mémoire.

8.4 Animation avec des images.

Il est aussi possible de réaliser des animations en utilisant l'affichage successif d'images. On obtient un effet analogue à celui des images gif animées. La difficulté de cette méthode provient de ce qu'il faut s'assurer que les images ont bien été chargées à partir du serveur avant de lancer leur affichage sur le client.
Dans le listing ci-dessous on retrouve les techniques examinées plus haut. On pourra noter que les initialisations sont réalisées ici au début de la méthode run( ) et pas dans une méthode init( ). Le boolean ok est mis à "true" quand le chargement de la totalité des images à partir du serveur est terminé. Le contrôle du chargement est effectué par un objet de la classe MediaTracker du package java.awt. Dans notre exemple, les images sont stockées (sur le serveur) dans le sous-répertoire "image" du répertoire de la page html de chargement de l'applet. Pour avoir un affichage correct, il est essentiel que toutes les images aient les mêmes dimensions. Chaque image recouvrant exactement la précédente, il est en principe inutile d'effacer.
Les méthodes sleep( ) du Thread  et waitForAll du MediaTracker pouvant être à l'origine d'une exception, il faut utiliser des blocs try et catch.

import java.applet.*;
import java.awt.*;

public class anima3 extends Applet implements Runnable
{ Thread runner = null;
   Graphics h;     Image imag[];
   int index,large = 0,haut = 0;
   boolean ok = false;
   final int nbima = 10;

 private void displayImage(Graphics g)
  { if (!ok) return;
    g.drawImage(imag[index],(size().width - large)/2,(size().height - haut)/2, this);} //centrage de l'affichage

 public void paint(Graphics g)
  { if (ok){         //chargement terminé
//g.clearRect(0, 0, size().width, size().height); effacement si surcharge de update()
     displayImage(g);}
     else g.drawString("Chargement",10,20);}

  public void start()
   { if (runner == null){
       runner = new Thread(this);
       runner.start();}}                

  public void stop()
   { if (runner != null){
      runner.stop();
      runner = null;}}

  public void run()
   { index = 0;
     if (!ok)     // chargement des images
     {  repaint();
        h = getGraphics();
        imag = new Image[nbima];   //tableau d'images
        MediaTracker tracker = new MediaTracker(this); //
        String strImage;
        for (int i = 0; i < nbima; i++){ //boucle de chargement
           strImage = "image/ima" + i + ".gif"; //nom du fichier
           imag[i] = getImage(getDocumentBase(), strImage); //détermination de son adresse
           tracker.addImage(imag[i], 0);} //chargement
     try{
        tracker.waitForAll(); //attente de réalisation de la totalité de la boucle
        ok = !tracker.isErrorAny();}
     catch (InterruptedException e){ }
     large  = imag[0].getWidth(this);   //dimensions des images
     haut = imag[0].getHeight(this);}
     repaint();
     while (true){   //boucle d'animation
        try{
          displayImage(h);
          index++;
          if (index == nbima)     index = 0;
          Thread.sleep(100);}   //100 ms entre chaque image
       catch (InterruptedException e){stop();}}}

public void destroy() //pour quitter proprement
   {  h.dispose();
      for (int i = 0; i < nbima; i++) imag[i].flush();}
}

8.5 Activités indépendantes

 Il est possible de gérer de manière simultanée plusieurs processus qui peuvent être indépendants en étendant la classe Thread. Afin de ne pas trop allonger  le listing de l'exemple, les deux processus mis en place utilisent la même classe (process) mais les deux activités sont totalement indépendantes et asynchrones. Dans le premier processus, on déplace un rectangle noir de droite à gauche; dans le second on déplace un rectangle rouge de haut en bas. Dans un cas concret, on pourra bien sur utiliser des classes différentes pouvant agir sur des objets différents.

La méthode run( ) de l'applet crée deux instances p1 et p2 de la classe process puis lance leur exécution avec p1.start( ) et p2.start( ). La classe process modifie dans sa méthode run( ) la position de l'origine des rectangles qui lui sont passés en argument. Cette méthode exécute ensuite une boucle infinie qui redessine l'applet. La méthode sleep( ) d'un Thread pouvant être à l'origine d'une exception, il faut utiliser des blocs try et catch. L'usage d'un double tampon permettrait d'améliorer cette animation.

import java.applet.*;
import java.awt.*;

class process extends Thread
{   Rectangle rp;   //objet à modifier
     boolean ver;

 process(Rectangle r,boolean v) //constructeur
  {   rp=r;   ver=v;}

 public void run()
  { int del =(ver) ? 20 : 40;   //pour avoir des processus asynchrones
    while(true){
    for (int i=0; i<100; i++){
      if (ver) rp.x=2*i; else rp.y=i; //déplacement dans un sens
    try{Thread.sleep(del);}
    catch (InterruptedException e){ }}
    for (int i=100; i>0; i--){        //puis dans l'autre
      if (ver) rp.x=2*i; else rp.y=i;
    try{Thread.sleep(del);}
    catch (InterruptedException e){ }}}}}

public class anima4 extends Applet implements Runnable
{  Thread runner = null;
    Rectangle r=new Rectangle(0,0,40,20);
    Rectangle r2=new Rectangle(0,0,20,40);
    process p1,p2;

   public void init()
    { setBackground(Color.lightGray);}

  public void paint(Graphics g)
   {  g.setColor(Color.black);
      g.fillRect(10+r.x,60+r.y,r.width,r.height);
      g.setColor(Color.red);
      g.fillRect(100+r2.x,10+r2.y,r2.width,r2.height);}

  public void start()
   { if (runner == null)
     {  runner = new Thread(this);
        runner.start();}}

  public void stop()
   { if (runner != null)
     { runner.stop();
      runner = null;}}

  public void run()
   { p1=new process(r,true);
     p2=new process(r2,false);
     p1.start();
     p2.start();
     while (true){
       try{
         repaint();
         Thread.sleep(10);}
       catch (InterruptedException e){ }}}
}

8.6 Modifications à partir de JAVA 1.1

Les concepteurs de JAVA se sont rendu compte que la méthode Thread.stop( ) utilisée pour arrêter un Thread et la méthode Thread.suspend( ) utilisée pour le mettre en attente étaient peu sûres.
Elles ont été déclarée "deprecated"  et ne doivent en principe plus être utilisées.
Les méthodes stop( ) et run( ) doivent être modifiées ainsi :

  public void stop()
  { if (runner != null) runner = null;} //destruction

   public void run()
   {  Thread bidon=Thread.currentThread( ); //bidon est le Thread en action (runner)
     while ( runner==bidon )    / / boucle qui cesse quand runner est détruit par stop( )
     { try                   //action à réaliser
        { repaint();     // redessiner
          Thread.sleep(20);}   //pause de 20 ms
       catch (InterruptedException e)
       { stop();}}}   //traitement (facultatif) de l'exception
}

De même pour provoquer une pause, il faut remplacer runner.suspend( ) par stop( ) et runner.resume( ) par start( ).