21.05.05 Animationen in Java ME

Ähnlich funktioniert auch die Animation um die Farbe zu ändern. Dort wird in der doInLoop-Methode nur nicht die Größe des RectCanvas verändert, sondern die einzelnen Farbwerte des Rechtecks. Natürlich muss auch hier wieder überprüft werden, ob die Farbwerte momentan erhöht, oder verringert werden.

package de.jbb.j2me.ani;

public class RectFader extends Animation {

  private RectCanvas canvas;
  private boolean redMaxi = false;
  private boolean greenMaxi = false;
  private boolean blueMaxi = false;

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

  protected void doInLoop() {

    int red = canvas.getRed();
    int green = canvas.getGreen();
    int blue = canvas.getBlue();

    if (redMaxi) {
      red += 5;
    }
    else {
      red -= 5;
    }
    if (greenMaxi) {
      green += 3;
    }
    else {
      green -= 3;
    }
    if (blueMaxi) {
      blue += 1;
    }
    else {
      blue -= 1;
    }

    if (red >= 255) {
      red = 255;
      redMaxi = false;
    }
    else if (red <= 0) {
      red = 0;
      redMaxi = true;
    }
    if (green >= 255) {
      green = 255;
      greenMaxi = false;
    }
    else if (green <= 0) {
      green = 0;
      greenMaxi = true;
    }
    if (blue >= 255) {
      blue = 255;
      blueMaxi = false;
    }
    else if (blue <= 0) {
      blue = 0;
      blueMaxi = true;
    }

    canvas.setRed(red);
    canvas.setGreen(green);
    canvas.setBlue(blue);

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

Der SimpleAnimator ist mit dieser neuen Struktur leider nicht mehr verwendbar. Stattdessen wird er durch einen ComplexAnimator ausgetauscht. Diesem werden im Konstruktor beliebig viele Objekte der Klasse Animation übergeben, welche wiederrum alle während der Animation ausgeführt werden. Hierdurch können auch auf sehr einfachem Weg beliebig viele und/oder neue Animationen auf das Rechteck bzw. die gesamte Zeichenoberfläche angewandt werden.

Die Animation selbst findet in der Methode animate statt. Ihr wird ein Canvas übergeben, das in regelmäßigen Abständen neu gezeichnet werden soll. Zusätzlich gibt es noch Parameter, die angeben, wie oft das Canvas in der Sekunde neu gezeichnet werden soll (Frames per Second, fps) und wie lang die Animation insgesamt laufen soll (duration).

In einem separatem Thread werden dann anhand der fps die Anzahl der Millisekunden errechnet, die nach jeder Aktualisierung pausiert werden müssen (millis). Aus der Laufzeit und den zu pausierenden Millisekunden kann wiederum die Gesamtzahl (loops) der Schleifendurchläufe (repaint()-Aufrufe) ermittelt werden. Diese Schleife wird nach den einzelnen Animationen gestartet. Zum Abschluss müssen die Animationen noch gestoppt werden.

package de.jbb.j2me.ani;

import javax.microedition.lcdui.Canvas;

public class ComplexAnimator {

  private boolean started = false;
  private Animation[] animations;

  public ComplexAnimator(Animation[] animations) {
    this.animations = animations;
  }

  public void animate(final Canvas canvas, final int fps, final int duration) {

    if (started) {
      return;
    }
    started = true;
    new Thread(new Runnable() {

      public void run() {
        int millis = 1000 / fps;
        int loops = duration * 1000 / millis;
        for (int i = 0; i < animations.length; i++) {
          new Thread(animations[i]).start();
        }
        for (int i = 0; i < loops; i++) {
          canvas.repaint();
          try {
            Thread.sleep(millis);
          }
          catch (InterruptedException ie) {
            ie.printStackTrace();
          }
        }
        for (int i = 0; i < animations.length; i++) {
          animations[i].stop();
        }
        started = false;
      }
    }).start();
  }
}

Um den ComplexAnimator zu verwenden, muss noch das MIDlet angepasst werden:

package de.jbb.j2me.ani;

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

public class AnimatedMIDlet extends MIDlet implements CommandListener {

  // ...
  // ComplexAnimator anstatt SimpleAnimator
  private ComplexAnimator canim;

  public void startApp() {

    if (first) {
      // ...
      // Animationen erzeugen
      Animation[] anis = new Animation[2];
      anis[0] = new RectResizer(canvas);
      anis[1] = new RectFader(canvas);
      // ComplexAnimator anstatt SimpleAnimator initialisieren
      canim = new ComplexAnimator(anis);
      // ...
    }
    display.setCurrent(canvas);
  }

  public void commandAction(Command c, Displayable d) {

    if (c.equals(animate)) {
      // Der Animate-Aufruf wird um die zusätzlichen Parameter ergänzt
      canim.animate(canvas, 25, 15);
    }
    // ...
  }
  // ...
}

Mit diesem Gerüst kann eine Zeichenoberfläche auf sehr einfachem Weg vielseitig animiert werden.

Animation 1 Animation 2 Animation 3

Nachteil dieser Methode ist jedoch, dass mit hoher Wahrscheinlichkeit gelegentlich Animationsschritte der einzelnen Animationen übersprungen werden, was die Animationen ruckelig erscheinen lassen kann.

Berechnungen in der paint-Methode

Oftmals werden bei Animationen in Java ME aus Bequemlichkeit viele Berechnungen für die Darstellung einer Canvas-Oberfläche direkt in der paint-Methode durchgeführt. Dies kann bei komplexen Berechnungen in Kombination mit Endgeräten, die diese Berechnungen nicht schnell durchführen können, jedoch ins Auge gehen. Denn beim Aufruf von repaint() wird die paint-Methode asynchron aufgerufen. Das bedeutet konkret, dass unter Umständen die paint-Methode mehrmals gleichzeitig durchläuft, und es so zu Darstellungsfehlern kommen kann. Ein abstraktes Beispiel:

// Animationsschleife
while (y < z) {
  // Alle Berechnungen und Aktualisierungen werden in der paint-Methode
  // des Canvas durchgeführt. Hierdurch benötigt ein Durchlauf der paint-
  // Methode in diesem Fall beispielhaft ca. 65 Millisekunden.
  canvas.repaint(); 
  try {
    // Gewartet wird aber immer nur 30 Millisekunden lang. Da die paint-
    // Methode asynchron ist und 65 Millisekunden benötigt, wird durch 
    // diese Schleife die paint-Methode bei jedem Durchlauf weitere zwei 
    // Mal aufgerufen, obwohl der "erste" Aufruf der paint-Methode noch
    // nicht abgeschlossen ist.
    Thread.sleep(30);
  }
  catch (InterruptedException ie) {
    ie.printStackTrace();
  }
}

Um dieses Problem zu umgehen, können Sie alle Berechnungen, die nicht zwingend in der paint-Methode benötigt werden, auslagern, oder ein Offscreen-Image verwenden. Hilft dies auch nicht, können Sie Ihr Programm explizit anweisen, so lange zu warten, bis alle noch anstehenden paint-Aufrufe vollständig abgearbeitet wurden. Dies funktioniert mit der Methode canvas.serviceRepaints(). Beachten Sie aber, dass es hier zu Deadlocks kommen kann, wenn von einer anderen Stelle in Ihrem MIDlet ständig neue repaint()-Aufrufe die Warteschlange der paint-Aufrufe neu „auffüllen“. Alternativ (und je nach Anwendungsfall) können Sie auch die Methode callSerially(Runnable r) der Klasse Display verwenden. Der Code des Runnable-Übergabeparameters wird erst ausgeführt, wenn alle noch ausstehenden paint-Aufrufe abgearbeitet wurden.

Selbstverständlich verzögert sich durch solche Maßnahmen auch die Ausführungszeit eines Animationsschrittes. Diese Technik sollte deshalb nur in Kombination mit der Technik, die im nachfolgenden Punkt Gleichmäßige Animationen vorgestellt wird, verwendet werden. Gleichzeitig sollte über eine Optimierung/Vereinfachung des Programms an dieser Stelle nachgedacht werden. Denn trotz Umsetzung der oben genannten Punkte bleibt meist das ursprüngliche Problem: Die Berechnung und Zeichnung der Oberfläche benötigt noch immer länger, als zwischen den einzelnen Animationsschritten gewartet werden kann.

Gleichmäßige Animationen

Zwischen jedem Animationsschritt wird zwar immer die gleiche Anzahl an Millisekunden geschlafen, doch dürfen die Rechen- und/oder Zeichenoperationen, die für die Animation notwendig sind, nicht vernachlässigt werden. Je nach Komplexität benötigen diese auch noch einmal einige Millisekunden, die bei einer gleichmäßigen Animation berücksichtigt werden müssen.

Um diese Differenz auszugleichen, merkt man sich beim Start der Animation die aktuelle Zeit. Nach dem Ende der Animation zieht man dann die benötigte Zeit von der aktuellen Zeit ab. Somit erhält man die Zeit, die dieser Animationsschritt gebraucht hat. Von der eigentlich zu schlafenden Zeit wird dann die Animationszeit abgezogen.

Im ComplexAnimator könnte dieser Ansatz so umgesetzt werden:

// ...
long repaintTime = 0;
for (int i = 0; i < loops; i++) {
  repaintTime = System.currentTimeMillis();
  canvas.repaint();
  canvas.serviceRepaints();
  repaintTime = System.currentTimeMillis() - repaintTime;
  try {
      Thread.sleep(millis - repaintTime);
  }
  catch (InterruptedException ie) {
      ie.printStackTrace();
  }
  catch (IllegalArgumentException iae) {
      // repaintTime war größer als millis
  }
}
// ...

Selbstverständlich muss das dann auch noch in den einzelnen Animationen umgesetzt werden. Damit man sich nicht bei jeder Animation selbst darum kümmern muss, gibt es geringfügige Änderungen an der Basisklasse Animation. Diese kümmert sich fortan selbst um die Pausierung nach einem Animationsschritt. Die konkrete Implementierung muss lediglich über eine Methode die Anzahl der Millisekunden bereitstellen, die nach einem Animationsschritt gewartet werden sollen (getMillisToWait).

package de.jbb.j2me.ani;

public abstract class Animation implements Runnable {

  private boolean run = false;

  public void run() {
    run = true;
    long time = 0;
    while (run) {
      time = System.currentTimeMillis();
      doInLoop();
      try {
        Thread.sleep(getMillisToWait() - System.currentTimeMillis() + time);
      }
      catch (InterruptedException ie) {
        ie.printStackTrace();;
      }
      catch (IllegalArgumentException iae) {
        // getMillisToWait() kleiner als die Zeitdifferenz
      }
    }
  }

  protected abstract void doInLoop();
  protected abstract long getMillisToWait();

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

Die RectResizer Animation müsste dann wie folgt angepasst 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);
    }
    else if (canvas.getRectWidth() > 10) {
      canvas.setRectWidth(canvas.getRectWidth() - 5);
      canvas.setRectHeight(canvas.getRectHeight() - 3);
      maxi = false;
    }
    else {
      maxi = true;
      doInLoop();
    }
  }

  protected long getMillisToWait() {
    return 100;
  }
}

Berücksichtigen Sie jedoch, dass ein Thread.sleep niemals wirklich auf die Nanosekunde genau arbeitet. Vor allem wenn ein Thread weniger als 15-20 Millisekunden pausieren soll, wird der Thread dieses länger als angegeben machen. Benötigen Sie aber zwingend wirklich einen sehr gleichmäßigen Animationsverlauf bei Wartezeiten von weniger als 15-20 Millisekunden, können Sie Ihre Thread.sleep Aufrufe durch den Aufruf dieser Methode ersetzen:

public void sleep(long millis) {
  long aim = System.currentTimeMillis() + millis;
  while (System.currentTimeMillis() < aim);
}

Hierdurch wird das Endgerät jedoch ständig maximal belastet und es kann zu Problemen mit parallelen Threads kommen.

Selbstverständlich lassen sich auch die unterschiedlichen Techniken miteinander verknüpfen. Es hängt jedoch von der Anforderung an, welche davon zum Einsatz kommen sollten. Oftmals reicht eine einfache Animation voll und ganz aus.

Previous Article
Next Article

Schreibe einen Kommentar

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