21.05.05 Animationen in Java ME

Um eine Animation zu zeichnen, gibt es mehrere Ansätze. Je nach Einsatzort und Komplexität müssen Sie sich für eine geeignete Methode entscheiden. Dieses Kapitel zeigt Ihnen gängige Ansätze und Techniken, wie Sie Animationen in der Java ME Welt realisieren können.

Einfache Schritt für Schritt Animation

Die einfachste Art der Animation stellt ein separater Thread dar, der nacheinander Attribute eines Canvas manipuliert, die Oberfläche neu zeichnen lässt, und zwischen den einzelnen Schritten jeweils ein paar Millisekunden wartet, damit die Animation für das menschliche Auge überhaupt sichtbar wird. Bspw. könnte man auf diese Weise ein einfaches Viereck animiert vergrößeren und verkleinern. Hierzu wird zuerst einmal ein Canvas benötigt, das nichts weiter macht, als ein simples Viereck auf die Mitte des Bildschirms zu zeichnen. Die Höhe und die Breite des Vierecks sind mit entsprechenden Getter- und Setter-Methoden manipulierbar.

package de.jbb.j2me.ani;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;

public class RectCanvas extends Canvas {

  private int width = 10;
  private int height = 10;

  protected void paint(Graphics g) {

    g.setColor(0, 0, 0);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(255, 255, 255);
    g.fillRect((getWidth() - width) / 2, (getHeight() - height) / 2, width, height);
  }
  
  public void setRectHeight(int height) {
    this.height = height;
  }

  public void setRectWidth(int width) {
    this.width = width;
  }

  public int getRectHeight() {
    return this.height;
  }

  public int getRectWidth() {
    return this.width;
  }
}

Im nächsten Schritt wird eine weitere Klasse angelegt – SimpleAnimator. Diese Klasse ist für die Animation des RectCanvas zuständig. Sie besitzt die Methode animate(RectCanvas canvas) von der aus die Animation angestoßen wird (sofern diese nicht bereits läuft). In dieser Methode wird ein neuer Thread gestartet, der die Größe des Rechtecks zuerst in einer Schleife (Animationsschleife) erhöht und anschließend in einer neuen Schleife (ebenfalls eine Animationsschleife) wieder verringert. Nach jeder Größenänderung des Rechtecks wird auf das RectCanvas zur Aktualisierung der Anzeige die Methode repaint() aufgerufen. Damit die Animation auch wahrgenommen werden kann, muss (wie bereits erwähnt) nach jeder Aktualisierung eine kurze Pause via Thread.sleep(long millisecondsToSleep) eingelegt werden.

package de.jbb.j2me.ani;

import javax.microedition.lcdui.Canvas;

public class SimpleAnimator {

  private boolean started = false;

  public void animate(final RectCanvas canvas) {

    if (started) {
      return;
    }
    started = true;
    new Thread(new Runnable() {
      public void run() {
        while (canvas.getRectWidth() < 100) {
          canvas.setRectWidth(canvas.getRectWidth() + 5);
          canvas.setRectHeight(canvas.getRectHeight() + 3);
          updateAndWait(canvas);
        }
        while (canvas.getRectWidth() > 10) {
          canvas.setRectWidth(canvas.getRectWidth() - 5);
          canvas.setRectHeight(canvas.getRectHeight() - 3);
          updateAndWait(canvas);
        }
        started = false;
      }
    }).start();
  }

  private void updateAndWait(Canvas c) {

    c.repaint();
    try {
      Thread.sleep(40);
    }
    catch (InterruptedException ie) {
      ie.printStackTrace();
    }
  }
}

Um die Animation zu begutachten, wird natürlich noch ein MIDlet benötigt:

package de.jbb.j2me.ani;

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class AnimatedMIDlet extends MIDlet implements CommandListener {

  private Display display;
  private RectCanvas canvas;
  private SimpleAnimator sanim;

  private Command animate;
  private Command exit;

  private boolean first = true;

  public void startApp() {
    
    if (first) {
      display = Display.getDisplay(this);
      canvas = new RectCanvas();
      sanim = new SimpleAnimator();

      animate = new Command("Animieren", Command.OK, 1);
      exit = new Command("Beenden", Command.EXIT, 2);

      canvas.setCommandListener(this);
      canvas.addCommand(animate);
      canvas.addCommand(exit);

      first = false;
    }
    display.setCurrent(canvas);
  }

  public void commandAction(Command c, Displayable d) {

    if (c.equals(animate)) {
      sanim.animate(canvas);
    }
    else if (c.equals(exit)) {
      destroyApp(false);
    }
  }

  public void destroyApp(boolean unconditional) {
    notifyDestroyed();
  }

  public void pauseApp() {}
}

Nach Betätigung des Animieren-Buttons im Emulator läuft die Animation durch.

Animation 1 Animation 2 Animation 3

Offscreen Image

Eine weitere, häufig eingesetzte Technik in Kombination mit einfachen Animationen ist das Offscreen Image bzw. Double-Buffering. Dieses stellt sicher, dass es bei komplexen und/oder schnell wiederholenden Zeichenoperationen nicht zum Flackern des Bildes kommt, indem die Anzeige vor dem Aufruf der paint-Methode auf ein Image gezeichnet wird, welches vom Canvas nur noch angezeigt werden muss.

Ein solches Canvas besteht nur aus einigen, wenigen Zeilen Code:

public class ImageCanvas extends Canvas {

  private Image drawImage;

  protected void paint(Graphics g) {

    if (drawImage != null) {
      g.drawImage(drawImage, 0, 0, Graphics.TOP|Graphics.LEFT);
    }
  }

  public void setImage(Image img) {
    drawImage = img;
  }
}

In der Animationsschleife wird dann vor dem Neuzeichnen des Canvas das Image aktualisiert.

// Initialisierung des Offscreen-Images
Image off = Image.createImage(canvas.getWidth(), canvas.getHeight());
// ...
// Wir befinden uns in der Animationsschleife
Graphics g = off.getGraphics();
g.setColor(0);
g.fillRect(canvas.getWidth(), canvas.getHeight());
// weitere Zeichenoperationen
canvas.setImage(off);
// warten
canvas.repaint();

Komplexe Animationen

In manchen Fällen reicht eine Schritt für Schritt Animation jedoch nicht mehr aus bzw. wäre sehr kompliziert. Dies ist bspw. dann der Fall, wenn viele Bewegungen parallel und unabhängig voneinander ausgeführt werden müssen. Selbstverständlich könnte dann nach jeder duchgeführten Bewegung jeder Thread eine Neuzeichnung des Canvas veranlassen. In ungünstigen Fällen wird dann aber die repaint Methode sehr oft und/oder (beinahe) gleichzeitig aufgerufen, was wiederum zu Geschwindigkeitsverlusten und unschönen Effekten wie bspw. Flimmern führen kann. Um dieses Problem zu umgehen, rufen diese einzelnen Threads nicht selbstständig repaint auf. Stattdessen läuft ein weiterer Thread, der in regelmäßigen Abständen repaint aufruft.

Als Beispiel wird hier unsere Klasse erweitert. Das Rechteck soll nicht nur seine Größe verändern, sondern auch seine Farbe.

Zuerst muss die RectCanvas-Klasse um die entsprechenden Attribute und Methoden ergänzt werden:

package de.jbb.j2me.ani;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;

public class RectCanvas extends Canvas {

  // alte Attribute
  
  private int red = 255;
  private int green = 255;
  private int blue = 255;

  protected void paint(Graphics g) {

    g.setColor(0, 0, 0);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(red, green, blue);
    g.fillRect((getWidth() - width) / 2, (getHeight() - height) / 2, width, height);
  }
  
  // alte Getter und Setter
  
  public int getBlue() {
    return this.blue;
  }

  public void setBlue(int blue) {
    this.blue = blue;
  }

  public int getGreen() {
    return this.green;
  }

  public void setGreen(int green) {
    this.green = green;
  }

  public int getRed() {
    return this.red;
  }

  public void setRed(int red) {
    this.red = red;
  }
}

Die Animationen werden nun auch ausgelagert. Sie finden nicht mehr in der Klasse SimpleAnimator statt, sondern in separaten Klassen, die von der Basisklasse Animaton erben. Eine Animation implementiert hier eine Befehlsfolge, die die Darstellung eines Canvas über dessen Attribute verändert (bspw. Änderung der x/y Koordinaten und Größe unseres Rechtecks), die Änderungen aber nicht sofort neuzeichnet.

package de.jbb.j2me.ani;

public abstract class Animation implements Runnable {

  private boolean run = false;

  public void run() {
    run = true;
    while (run) {
      doInLoop();
    }
  }

  protected abstract void doInLoop();

  public void stop() {
    this.run = false;
  }
}

Die konkreten Implementierungen dieser abstrakten Klasse werden später in jeweils einen separaten Thread gepackt und ausgeführt. Eine Konkretisierung muss nur die Methode doInLoop implementieren. Dort müssen alle Aktualisierungen durchgeführt werden, die für den aktuellen Animationsschritt notwendig sind. Anschließend muss in dieser Methode gewartet werden, bis der nächste Animationsschritt durchgeführt werden soll.

Die Implementierung der bereits bekannten Vergrößer- und Verkleinerungsfunktion aus dem SimpleAnimator kann bspw. so umgesetzt werden:

package de.jbb.j2me.ani;

public class RectResizer extends Animation {

  private RectCanvas canvas;
  private boolean maxi = true;

  public RectResizer(RectCanvas canvas) {
    this.canvas = canvas;
  }

  protected void doInLoop() {

    if (canvas.getRectWidth() < 100 && maxi) {
      canvas.setRectWidth(canvas.getRectWidth() + 5);
      canvas.setRectHeight(canvas.getRectHeight() + 3);
      pause();
    }
    else if (canvas.getRectWidth() > 10) {
      canvas.setRectWidth(canvas.getRectWidth() - 5);
      canvas.setRectHeight(canvas.getRectHeight() - 3);
      maxi = false;
      pause();
    }
    else {
      maxi = true;
    }
  }

  private void pause() {

    try {
      Thread.sleep(100);
    }
    catch (InterruptedException ie) {
      ie.printStackTrace();
    }
  }
}

Da nun auf die eigentliche Animationsschleife kein direkter Zugriff mehr besteht, wird eine zusätzliche Variable maxi benötigt, die spezifiziert, ob das Rechteck momentan vergrößert oder verkleinert wird.

Lesen Sie auf der zweiten Seite weiter.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.