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.
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.