Java para programadores (6.4):Hebras y animacion

DEBE UN APPLET SER COMPLETAMENTE DEPENDIENTE de los eventos que pueda recibir y no hacer nada sin ellos?. ¿No pueden hacer algo por su cuenta y sin que nadie se lo pida?¿ Podemos conseguir que se parezcan mas a los programas tradicionales, que tienen una secuencia de instrucciones y que se ejecutan de principio a fin?

La respuesta es si. El applet puede crear una hebra. Una hebra representa un hilo conductor que permite ejecutar de forma independiente un conjunto de instrucciones desde principio a fin. En un momento determinado, pueden existir y funcionar varias hebras al mismo tiempo. Un applet, y por supuesto un programa Java, puede crear varias hebras y ponerlas en marcha. Cada una de estas hebras son, realmente, un pequeño e independiente programa.

Un uso normal de estas hebras es la realización de animaciones. Una hebra funciona continuamente mientras el applet este en pantalla. La hebra cambia la presentación del applet varias veces por segundo. Si esos cambios son suficientemente frecuentes y  el cambio es suficientemente pequeño, el espectador percibe la sensación de movimiento continuo.

La programación con hebras se llama programación paralela porque varias hebras pueden funcionar en paralelo. La programación paralela puede resultar problemática cuando varias hebras quieren emplear un recurso compartido. Un ejemplo de recurso compartido puede ser la pantalla. Si varias hebras intentan dibujar  al mismo tiempo, es posible que el contenido de la pantalla se estropee. Las variables instanciables también son recursos compartidos.Para evitar problemas, el acceso a los recursos compartidos, se debe sincronizar de alguna manera. Java define una manera eficiente y fácil de realizar esa sincronización. Lo veremos mas adelante.

Aun cuando el applet cree únicamente una hebra, habrá dos hebras funcionando en paralelo, dado que que siempre esta presente una hebra que controla la interface del usuario monitorizando los eventos.¿ Que pasa si las hebras intentan dibujar algo al mismo tiempo que el usuario cambia el tamaño del applet?. Es un problema de sincronización.Esto nos obliga a utilizar la sincronización incluso en los casos que el applet solo cree una hebra.


En Java, una hebra es concretamente un objeto de tipo Thread. Este tipo de objetos, deben tener una subrutina que ejecutar. Hay dos maneras sencillas de crear estos objetos :Puede definir una subclase de Thread y sobregrabar el método run() en su subclase, en este caso el método run() de su subclase es la rutina que ejecutara Thread. O puede crear una clase que implemente la interface Runnable y le pase un objeto de esta clase al constructor Thread. La interface Runnable consiste en único método, run(). Para implementar esta interface en una clase, debe definir el método run()en la clase.Si construye un objeto Thread con un objeto tipo Runnable, entonces el método run() del objeto, será la subrutina que se ejecute en esa hebra.

El segundo método de crear las hebras es mucho mas común, incluso aunque sea un poco mas retorcido.Típicamente, puede declarar una clase applet que implemente la interface Runnable. Puede crear una hebra basada en al applet. Lo mas normal es que esto lo haga en el método init() o en el método start(). El applet tendrá un aspecto semejante a este:

public class AppletConHebra extends Applet
                implements Runnable {
    Thread runner;    // la hebra se ejecuta de forma independiente
    .
    . // mas cosas
    .
    public void init() {
       runner = new Thread(this); // crea una hebra basada en
                // este applet; la hebra ejecutara
                // el método run del applet
       .   .   .   // otras inicializaciones
    }
    public void run() {
       .  .  . // comandos a ejecutar en la hebra
    }
    .
    . // mas cosas
} //fin de la clase AppletConHebra

De esta forma, el método run puede acceder a todas las instancias de las variables del applet. Todas esas variables, así como la pantalla, son recursos compartidos en potencia, dado que la hebra ejecuta el método run() al mismo tiempo que la hebra que controla la interface gráfica puede ir llamando a otros métodos del mismo applet.

(Applet no tiene nada de especial, es mas, puede implementar la interface Runnable en cualquier clase que cree. Una  muy útil es la subclase runnable que Canvas.)

Ahora bien, cuando se crea por primera vez un objeto Thread, no empieza a funcionar de forma automática. La clase Thread incluye métodos instanciables para arrancar y parar la hebra. ( Una hebra se para automáticamente si ejecuta la instrucción “return” del método run(). Cuando una hebra se ha parado, no puede ser rearrancada. Sin embargo, puede suspender la hebra temporalmente y continuarla mas tarde. También puede hacer que quede dormida por un periodo de tiempo determinado, y puede hacer que se quede esperando la notificación de algún evento. Si el objeto Thread se llama programa aquí tiene alguno de los métodos que puede llamar:

  • programa.start();— Provoca que se inicie la ejecución de la hebra y de su método run()asociado. (Observe que no llama directamente al método run())
  • programa.stop(); — Finaliza la ejecución de la hebra. Una hebra que se detenga por este método o porque finalice la ejecución del método run() se dice que esta muerta.
  • programa.suspend();— Provoca la detención de la hebra. La hebra se detiene en mitad de la ejecución.
  • programa.resume();–Provoca la reanudación de la hebra suspendida anteriormente.
  • programa.isAlive();–Devuelve un valor boleano que indica si la hebra esta viva o no. Devuelve false si la hebra esta muerta. (Una hebra que esta suspendida, se considera viva).

Si en un applet utiliza una hebra, puede decidir donde quiere llamar a cualquiera de estos procedimientos. Una forma de organizar todo esto es crear e iniciar la hebra la primera vez que el applet llama al método start(), y pararla en el método destroy() del applet, si es que todavía esta activa. Dado que probablemente no quiera que la hebra siga funcionando cuando el applet no este activo, puede suspenderla en el método stop() y continuarla en el método start().( Una alternativa que puede elegir es la de detener la hebra en el método stop() y crear una nueva la siguiente vez que sea llamado el método start(). Con todo esto en mente, aquí tiene las líneas generales de un applet mas completo que utiliza una única hebra:

      public class AppletWithThread extends Applet
                                    implements Runnable {

         Thread programa = null;

         .
         .  // mas cosas
         .

         public void start() {
            if (programa == null) {  // crea y arranca la hebra
               programa = new Thread(this);
               programa.start();
            }
            else if (programa.isAlive()) {  // continua la hebra,
               programa.resume();           //   a menos que este muerta
            }
         }

         public void stop() {
            if (programa.isAlive())  // suspender la hebra, 
               programa.suspend();   //      si esta viva
         }

         public void destroy() {
            if (programa.isAlive())  // Si la hebra esta viva,
                programa.stop();     //    pararla
            programa = null;
         }

         public void run() {
            . . . // subrutina a ejecutar por la hebra
         }

      }  // fin de la clase AppletWithThread

Ahora, ya tiene que decidir que es lo que pondrá en el método run(). Si esta utilizando la hebra para animación, puede emplear un bucle infinito que se dedique a dibujar imagen de la animación tras otra. También puede desear una parada entre las imágenes. Estas pausas crean espacios entre las imágenes por lo que la animación va mas lenta, pero a cambio, permiten que otras hebras funcionen. Puede insertar una pausa llamando al método static Thread.sleep(int). El parámetro de este método indica el tiempo de pausa en milisegundos. (1000 milisegundos igual a 1 segundo). Desgraciadamente, para usar ese método, tiene que tener en cuenta el hecho de que puede perder una excepción, y las excepciones son algo que no trataremos hasta el Capítulo 8. Por ahora acepte que la sintaxis para llamar a este método es:

try {Thread.sleep(sleepTime);}
catch (InterruptedException e) {}

donde sllepTime es el numero de milisegundos que quiere que quede dormida la hebra.

Suponga, por ejemplo, que quiere que su applet desplace el mensaje “Hello World” desde la derecha a la izquierda cruzando el applet. Puede usar unas variables instanciables x e y, para controlar la posición del mensaje y dejar que el método paint() dibuje el mensaje en la pantalla. El método run para animar el mensaje debe cambiar los valores de x e y en cada fotograma y llamar a repaint() para ver el fotograma en pantalla, luego el applet ha de contener lo siguiente:

      int x;  // coordenadas x e y de inicio de la string 
      int y = -1;  // este valor indica que  y
                   //       todavía no ha sido calculado por run() 
      String message = "Hola Mundo";
      int sleepTime = 100;  //Milisegundos entre imágenes

      public void paint(Graphics g) {
         if (y > 0)
            g.drawString(message,x,y);
      }

      public void run() {

         FontMetrics fm = getFontMetrics(getFont());
                 // toma un objeto FontMetrics para las fuentes del applett
         int stringWidth = fm.stringWidth(message);
                 // usa  fm para determinar el tamaño del mensaje
                 //  que se presentara

         int min_x = -stringWidth;  // minimo valor aceptable para x,
                                    //    cuando el mensaje se desplace
                                    //    totalmente fuera por la izquierda
                                    //   del applet.
         int max_x = size().width;  // mayor valor aceptable para x,
                                    //    cuando el mensaje esta apunto 
                                    //    de aparecer por la derecha.
         int dx = 5;  // numero de pixels que se moverá el mensaje
                      //    entre dos imágenes seguidas

         x = max_x;  // establecer el inicio del mensaje
         y = fm.getHeight();

         while (true) {  // bucle infinito
            x = x - dx;  // mover el mensaje
            if (x < min_x)  // si esta en el final izquierdo,
               x = max_x;      //     moverlo al principio
            repaint();   // indicar al sistema que redibuje
            try { Thread.sleep(sleepTime); }  // pausa entre imágenes
            catch (InterruptedException e) { }
         }

      }  // fin de run()

Desgraciadamente, mientras el mensaje se pasea por la pantalla, esta vibra de forma terrible ya que el mensaje se borra y redibuja continuamente. La mejor solución es usar una imagen off-screen. El método run() puede dibujar la imagen en el off-screen y el método paint() puede copiarla a la pantalla. Sin embargo, esto produce un nuevo problema. : El área off-screen es un recurso compartido.¿Que pasa si paint() esta copiando la imagen a la pantalla mientras el método run() esta en el medio del dibujo de una nueva imagen?. La pantalla puede quedar destrozada. De algún modo, el acceso al off-screen debe ser controlado, de tal forma que solo una hebra pueda acceder al mismo tiempo.


En Java, el acceso a los recurso compartidos puede ser controlado usando métodos sincronizados. Un método instanciable se define como sincronizado añadiéndole el modificador sincronized a su declaración. Por ejemplo:

 

syncronized void drawOneFrame() { ... }

 

Puede añadir este modificador a cualquier método standard, como el paint. Esto significa que en un objeto solo puede estar funcionando en un momento un método sincronizado, y si este método o algún otro método sincronizado esta funcionando en el mismo objeto por una hebra diferente, la nueve hebra deberá esperar hasta que la otra finalice la ejecución del método. Todo esto es manejado automáticamente por el sistema. Todo lo que debe hacer, es declarar que métodos son sincronizados.

Si todos los accesos a los recursos compartidos se realiza por medio de métodos sincronizados, puede estar seguro que en un momento determinado solo una de las hebras podrá tener acceso a esos  recursos. Puede están convencido que una hebra habrá podido finalizar su tratamiento antes de que cualquier otra tenga la posibilidad de acceder al recurso.

Para aplicar sincronización con off-screen, deberá definir como sincronizado el método paint que copia el off-screen a la pantalla:

       Image OSC = null;   // la imagen off-screen 

       synchronized public void paint(Graphics g) {
          if (OSC != null)  //si off-screen existe, lo copia a pantalla
              g.drawImage(OSC,0,0,this);
          else {  // si no, rellena el applet con el color de fondo
              g.setColor(getBackground());
              g.fillRect(0,0,size().width,size().height);
          }
       }

       public void update(Graphics g) {
             // redefine update() para que no borre el applet;
             // esto evita la vibración
          paint(g);
       }

La imagen off-screen debe ser creada antes de cualquier imagen pueda ser presentada. El sitio para hacerlo es el el método start() del applet, justo antes de crear la hebra:

         public void start() {
            if (runner == null) {  // crea el area off-screen i la hebra
               OSC = createImage(size().width, size().height);
               runner = new Thread(this);
               runner.start();
            }
            else if (runner.isAlive()) {  // continua con la hebra,
               runner.resume();           // A menos que este muerta
        }
         }

Adicionalmente a esto, debe crear un método sincronizado para dibujar los fotogramas de la animación en el área off-screen. El método será llamado por el run():

       synchronized void drawNextFrame() {
          . . . // dibuja el nuevo fotograma
          // Nota: este método necesita parámetros o acceso
          // a las variables que describen la imagen
       }

       public void run() {
          . . . // inicializar
          while (true) {
             . . . // hacer cualquier actualización necesaria
                   // (Esto también se puede hacer en drawNextFrame().)
             drawNextFrame();
             repaint();
             try { Thread.sleep(sleepTime); }
             catch (InterruptedException e) { }
          }
       }

Usando este método, puede escribir un applet que desplace un mensaje suavemente por la pantalla sin parpadeos. Y también le puede hechar algo de fantasía

Acerca de Miguel Garcia

Programador, Desarrollador web, Formador en distintas areas de informatica y director de equipos multidisciplinares.
Esta entrada fue publicada en Formacion, Java y etiquetada , , , . Guarda el enlace permanente.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.