Moviendo una figura por el área de trabajo de una ventana

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1232 bytes)next.gif (1211 bytes)

Subprocesos (threads)

La primera aproximación al problema de la animación

Eliminando el parpadeo

Insertando un retardo en el bucle sin fin


La primera aproximación al problema de la animación

disco.gif (1035 bytes)anima1: AnimaApplet1.java

La clase que describe el applet AnimaApplet1 deriva de Applet y ha de implementar el interface Runnable y definir la función run, además de otras funciones cuyas tareas se explicarán a lo largo de esta página.

Además, es necesario crear un subproceso, es decir, un objeto de la clase Thread. Creamos este objeto en la redefinición de la función start miembro de la clase que describe el applet. La función miembro start se llama después de init cuando la página que contiene el applet aparece en el navegador. Se crea el objeto anima de la clase Thread, desde dicho objeto se llama a su función miembro start para poner en marcha el subproceso.

public class AnimaApplet1 extends Applet implements Runnable {
    Thread anima;
//...
  public void start(){
     if(anima ==null){
        anima=new Thread(this);
        anima.start();
     }
  }
}

A continuación, se ejecuta la función miembro run, que consta de un bucle indefinido while, que llama a la función mover.

  public void run() {
    while (true) {
       mover();
    }
  }

La función mover actualiza la posición de la figura que se mueve y comprueba que no supera los límites de su confinamiento. Por último, llama a paint para dibujar la figura en su nueva posición. Como vemos en el código, cuando la pelota alcanza los contornos del applet, rebota.

  void mover() {
    x += dx;
    y += dy;
    if (x >= (anchoApplet-radio) || x <= radio) dx*= -1;
    if (y >= (altoApplet-radio) ||y <= radio) dy*= -1;
    repaint();      //llama a paint
  }

Dibujamos la pelota de color rojo en su nueva posición

  public void paint (Graphics g) {
    g.setColor(Color.red);
    g.fillOval(x-radio, y-radio, 2*radio, 2*radio);
  }

El movimiento de la pelota lo paramos cuando abandonamos la página que contiene el applet, se llama entonces a la función stop miembro de la clase que describe el applet. En dicha función miembro, se para el subproceso anima, desde este objeto se llama a la función stop miembro de la clase Thread. Por último, se asigna a anima el valor null.

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

El código completo de este ejemplo, es el siguiente

package anima1;

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

public class AnimaApplet1 extends Applet implements Runnable {
    Thread anima;
    int radio=10;     	//radio de la pelota
    int x, y;       	//posición del centro de la pelota
    int dx = 1;     	//desplazamientos
    int dy = 1;
    int anchoApplet;
    int altoApplet;

  public void init () {
    anchoApplet=getSize().width; 	//dimensiones del applet
    altoApplet=getSize().height;	
    x=anchoApplet/4;			//posición inicial de partida
    y=altoApplet/2;
  }
  public void start(){
     if(anima ==null){
        anima=new Thread(this);
        anima.start();
     }
  }
  public void stop(){
     if(anima!=null){
        anima.stop();
        anima=null;
     }
  }
  public void run() {
    while (true) {
        mover();
    }
  }
  void mover() {
    x += dx;
    y += dy;
    if (x >= (anchoApplet-radio) || x <= radio) dx*= -1;
    if (y >= (altoApplet-radio) || y <= radio) dy*= -1;
    repaint();      //llama a update
  }
  public void paint (Graphics g) {
    g.setColor(Color.red);
    g.fillOval(x-radio, y-radio, 2*radio, 2*radio);
  }
}

Nuestra primera aproximación a la animación no es nada brillante. Existen dos probemas: la pelota se mueve muy rápidamente y en segundo lugar, se produce un molesto parpadeo causado, por el borrado de la ventana del applet y vuelta a pintarlo de nuevo con la pelota en una nueva posición. Como hemos explicado, repaint no llama directamente a paint sino a  update, que borra la ventana y a continuación llama a paint.

 

Eliminando el parpadeo

disco.gif (1035 bytes)anima2: AnimaApplet2.java

La solución más simple para eliminar el parpadeo en la animación es la de redefinir la función update en la clase que describe el applet. Lo primero que hace update es borrar la ventana y luego, llama al método paint. La parte del código que borra la ventana es la que causa el parpadeo.  Por lo tanto, hemos de redefinir update en la clase que describe el applet. Si dibujamos en la ventana sin borrarla previamente eliminamos el parpadeo.

    public void update(Graphics g) {
	paint(g);
    }

La solución a este problema es parcial, ya que no se borra el fondo y en la ventana del applet la pelota aparece en todas las posiciones por las que ha pasado a lo largo de su movimiento. Esta situación es conveniente, cuando queremos que se muestren las sucesivas posiciones por las que ha pasado el móvil, para darnos una idea acerca de su trayectoria.

El double-buffer es la solución a muchos de los problemas asociados con la animación. En vez de dibujar directamente en la ventana del applet dibujamos en un buffer intermedio (contexto gráfico en memoria). Cuando es el momento de actualizar la animación lo que hacemos es volcar lo dibujado desde el contexto en memoria a la ventana del applet en una simple y muy rápida operación de transferencia. Luego, volvemos a dibujar en el contexto gráfico en memoria, lo volcamos a la ventana, y así sucesivamente.

  public void update(Graphics g){
     if(gBuffer==null){
          imag=createImage(anchoApplet, altoApplet);
          gBuffer=imag.getGraphics();
     }
     gBuffer.setColor(getBackground());
     gBuffer.fillRect(0,0, anchoApplet, altoApplet);
//dibuja la pelota
     gBuffer.setColor(Color.red);
     gBuffer.fillOval(x-radio, y-radio, 2*radio, 2*radio);
//transfiere la imagen al contexto gráfico del applet
     g.drawImage(imag, 0, 0, null);
 }

Como vemos en el código, es muy importante que el contexto gráfico en memoria tenga las dimensiones de la ventana del applet. Como estamos trabajando en un contexto en memoria  no hay que preocuparse por los efectos de borrarlo antes de dibujar sobre dicho contexto. De hecho, es el primer paso que hay que hacer cuando empleamos esta técnica conocida por el nombre de double-buffer.

Una vez borrado gBuffer, se dibuja sobre dicho contexto la pelota en la nueva posición. Finalmente, se transfiere la imagen creada imag desde la memoria al contexto gráfico g de la ventana del applet, mediante la función drawImage. Fijarse que el método paint no se llama ahora desde update.

 

Insertando un retardo en el bucle sin fin

El segundo problema que observamos al ejecutar el applet AnimaApplet1, es que las posiciones de la pelota parecen aleatorias en la ventana del applet. Precisamos introducir un retardo en el bucle sin fin while.

La clase Thread dispone de una función miembro sleep que hace que el subproceso detenga su ejecución durante un número de milisegundos indicado en su argumento. Dicha función, solamente se puede llamar dentro de un bloque try...catch, ya que puede producirse una excepción del tipo InterruptedException.

  public void run() {
    while (true) {
//...
        try{
            Thread.sleep(200);
        }catch(InterruptedException ex){
            break;
        }
    }
  }

Se puede mejorar el código escribiendo la función run de la forma en la que se indica en el siguiente listado.

package anima2;

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

public class AnimaApplet2 extends Applet implements Runnable {
    Thread anima;
    int radio=10;       //radio de la pelota
    int x, y;     	//posición del centro de la pelota
    int dx = 1;         //desplazamientos
    int dy = 1;
    int anchoApplet;
    int altoApplet;
    int retardo=80;
//Doble buffer
     Image imag;
     Graphics gBuffer;

  public void init () {
    setBackground(Color.white);
    anchoApplet=getSize().width;	//dimensiones del applet
    altoApplet=getSize().height;
    x=anchoApplet/4;			//posición inicial de partida
    y=altoApplet/2;  
}
  public void start(){
     if(anima ==null){
        anima=new Thread(this);
        anima.start();
     }
  }

  public void stop(){
     if(anima!=null){
        anima.stop();
        anima=null;
     }
  }
  public void run() {
    long t=System.currentTimeMillis();
    while (true) {
        mover();
        try{
            t+=retardo;
            Thread.sleep(Math.max(0, t-System.currentTimeMillis()));
        }catch(InterruptedException ex){
            break;
        }
    }
  }
  void mover(){
     x += dx;
     y += dy;
     if (x >= (anchoApplet-radio) || x <= radio) dx*= -1;
     if (y >= (altoApplet-radio)  || y <= radio) dy*= -1;
     repaint();		//llama a update
  }
  public void update(Graphics g){
     if(gBuffer==null){
          imag=createImage(anchoApplet, altoApplet);
          gBuffer=imag.getGraphics();
     }
     gBuffer.setColor(getBackground());
     gBuffer.fillRect(0,0, anchoApplet, altoApplet);
//dibuja la pelota
     gBuffer.setColor(Color.red);
     gBuffer.fillOval(x-radio, y-radio, 2*radio, 2*radio);
//transfiere la imagen al contexto gráfico del applet
     g.drawImage(imag, 0, 0, null);
 }

  public void paint (Graphics g) {
//se llama la primera vez que aparece el applet
  }
}