Einfache Animationen und Threads
von Laura Lemay
Als ich Java das erste Mal in Aktion sah, handelte es sich um eine Animation: ein großes rotes »Hi there!«, das von rechts nach links über den Bildschirm lief. Diese einfache Form der Animation genügte, um meine Aufmerksamkeit zu erregen. »Das ist ja echt cool«, dachte ich damals.
Diese Art einer einfachen Animation erfordert nur ein paar Methoden in Java, jedoch bilden diese wenigen Methoden die Grundlage für jedes Java-Applet, das Sie am Bildschirm dynamisch aktualisieren wollen. Der Beginn mit einfachen Animationen ist der richtige Ausgangspunkt, um komplexere Applets zu entwickeln. Heute lernen Sie die Grundlagen von Animation in Java: Wie die verschiedenen Systemteile zusammen funktionieren, so daß sich Figuren bewegen und Applets dynamisch aktualisieren. Insbesondere erforschen Sie heute folgendes:
In der heutigen Lektion arbeiten Sie auch mit zahlreichen Beispielen echter Applets, die Animationen erstellen oder dynamische Bewegungen ausführen.
Eine Animation umfaßt in Java zwei Schritte: Erstens Aufbau und Ausgabe eines Animationsrahmens und zweitens entsprechend häufige Wiederholung der Zeichnung, um den Eindruck von Bewegung zu vermitteln. Aus den einfachen statischen Applets, die Sie gestern geschrieben haben, haben Sie gelernt, wie der erste Teil bewältigt wird. Heute lernen Sie den zweiten Teil.
Die paint()-Methode, die Sie gestern gelernt haben, wird von Java immer aufgerufen, wenn ein Applet gezeichnet werden muß - beim erstmaligen Zeichnen des Applets, wenn das Applet-Fenster verschoben oder es durch ein anderes Fenster überlagert wird. Sie können Java aber auch auffordern, ein Applet zu einem bestimmten Zeitpunkt nachzuzeichnen. Um die Darstellung am Bildschirm zu ändern, erstellen Sie das Bild - einen »Rahmen« - das Sie zeichnen wollen, dann fordern Sie Java auf, diesen Rahmen zu zeichnen. Tun Sie dies wiederholt und schnell genug, erhalten Sie in Ihrem Java-Applet eine Animation. Mehr ist dazu nicht nötig.
Wo findet all dies statt? Nicht in der paint()-Methode. paint() gibt nur Punkte am Bildschirm aus. Mit anderen Worten, paint() ist nur für den aktiven Rahmen der Animation zuständig. Die wirkliche Arbeit dessen, was paint() wirklich bewirkt, die Änderung des Rahmens für eine Animation, findet irgendwo anders in der Definition Ihres Applets statt.
In diesem »irgendwo anders« erstellen Sie den Rahmen (setzen Variablen für paint(), definieren Farben, Fonts oder andere Objekte, die paint() benötigt), dann rufen Sie die repaint()-Methode auf. repaint() ist der Auslöser, der Java veranlaßt, paint() aufzurufen und Ihren Rahmen zu zeichnen.
Erinnern Sie sich an start() und stop() in der 8. Lektion? Das sind die Methoden, die die Ausführung eines Applets starten und stoppen. Sie haben start() und stop() gestern nicht benutzt, weil die gestrigen Applets außer der einmaligen Anzeige nichts bewirkten. Bei Animationen und anderen Java-Applets, die länger verarbeitet und ausgeführt werden, müssen Sie start() und stop() verwenden, um die Ausführung des Applets zu starten und beim Verlassen der Seite, die das Applet enthält, zu stoppen. Bei den meisten Applets werden start und stop aus genau diesem Grund überschrieben.
Die start()-Methode löst die Ausführung des Applets aus. Sie können die gesamte Arbeit eines Applets in diese Methode einbinden oder Methoden anderer Objekte aufrufen, um dies zu erreichen. Normalerweise wird start() verwendet, um die Ausführung eines Threads zu beginnen, damit das Applet zum gegebenen Zeitpunkt abläuft.
stop() unterbricht demgegenüber die Ausführung eines Applets, damit es beim Verlassen der Seite, auf der das Applet angezeigt wird, nicht weiterläuft und Systemressourcen verbraucht. Meist wird beim Erstellen einer start()-Methode auch eine entsprechende stop()-Methode definiert.
Eine ausführliche Beschreibung, wie Java-Animationen funktionieren, ist viel schwieriger als das Aufzeigen anhand eines praktischen Beispiels. Mit einem oder zwei Beispielen können Sie die Beziehung zwischen diesen Methoden besser erkennen und nachvollziehen.
Listing 10.1 zeigt ein Beispiel-Applet, das auf den ersten Blick eine einfache Applet-Animation nutzt, um Datum und Uhrzeit anzuzeigen, und jede Sekunde konstant aktualisiert. Der Code erstellt eine einfache animierte digitale Uhr (ein Rahmen von dieser Uhr ist in Abb. 10.1 ersichtlich).
Die Wörter »auf den ersten Blick« im vorletzten Absatz sind wichtig: Dieses Applet funktioniert nicht! Ungeachtet dessen können Sie aber eine Menge über einfache Animationen lernen, deshalb ist es sinnvoll, diesen Code durchzuarbeiten. Im nächsten Abschnitt lernen Sie, was damit nicht stimmt.
Versuchen Sie herauszufinden, was in diesem Code passiert, bevor Sie mit der Analyse weitermachen.
Datum
-Applet
1: import java.awt.Graphics; 2: import java.awt.Font; 3: import java.util.Date; 4: 5: public class DigitalClock extends java.applet.Applet { 6: 7: Font theFont = new Font("TimesRoman",Font.BOLD,24); 8: Date theDate; 9: 10: public void start() { 11: while (true) { 12: theDate = new Date(); 13: repaint(); 14: try { Thread.sleep(1000); } 15: catch (InterruptedException e) { } 16: } 17: } 18: 19: public void paint(Graphics g) { 20: g.setFont(theFont); 21: g.drawString(theDate.toString(),10,50); 22: } 23: }
Abbildung 10.1: Die digitale Uhr
Bei diesem Beispiel ist etwas Wichtiges zu beachten. Sie denken sicherlich, daß es einfacher ist, das neue Date-Objekt in der paint()-Methode zu erstellen. Auf diese Weise könnten Sie eine lokale Variable benutzen, anstatt eine Instanzvariable an das Date-Objekt weiterzugeben. Das würde zwar einen klareren Code ergeben, jedoch ein weniger effizientes Programm. Die paint()-Methode wird jedesmal, wenn ein Rahmen gewechselt werden muß, aufgerufen. In diesem Fall ist das nicht kritisch. In einer Animation, bei der Rahmen sehr schnell wechseln, muß die paint()-Methode aber pausieren, um das Objekt jedesmal zu aktualisieren. Indem man der paint()-Methode nur das überläßt, was sie gut kann, nämlich am Bildschirm zeichnen, und neue Objekte im voraus festlegt, erreicht man effizientere Darstellungen am Bildschirm. Aus diesem Grund befindet sich auch das Font-Objekt in einer Instanzvariablen.
Je nach Ihren Erfahrungen mit Betriebssystemen und Umgebungen in diesen Systemen kennen Sie eventuell schon das Konzept von Threads. Wir beginnen zunächst mit ein paar Erklärungen.
Wenn ein Programm abläuft, beginnt es mit der Ausführung seines Initialisierungscodes, ruft Methoden oder Prozeduren auf und fährt mit der Ausführung und Verarbeitung fort, bis es fertig ist oder das Programm beendet wird. Das Programm nutzt einen einzelnen Thread, wobei der Thread für das Programm ein einzelner Kontrollpunkt ist.
Multithreading, wie in Java, ermöglicht die Ausführung mehrerer verschiedener Threads gleichzeitig im gleichen Programm, ohne sich gegenseitig zu beeinträchtigen.
Hier ein einfaches Beispiel. Nehmen wir an, Sie haben eine lange Berechnung nahe am Start eines Programms. Diese lange Berechnung ist später im weiteren Verlauf der Ausführung des Programms eventuell nicht mehr erforderlich. Sie ist für die Hauptaufgabe des Programms nebensächlich, wird aber dennoch gebraucht. In einem Einzel-Thread-Programm müssen Sie warten, bis diese Berechnung abgeschlossen ist, bevor der Rest des Programms ausgeführt werden kann. In einem Multithreading-System können Sie diese Berechnung in einen eigenen Thread stellen, so daß das übrige Programm unabhängig weiterlaufen kann.
Durch Verwendung von Threads in Java können Sie Applets so erstellen, daß sie in ihrem eigenen Thread laufen, ohne andere Systemteile zu stören. Mit Threads können viele Applets gleichzeitig auf der gleichen Seite laufen. Je nach dem, wie viele Sie haben, ist eventuell irgendwann die Kapazität des Systems erschöpft, so daß alle langsamer laufen, jedoch laufen sie weiterhin unabhängig voneinander.
Doch auch wenn Sie nicht viele Applets haben, empfiehlt sich die Verwendung von Threads in der Java-Programmierung. Als allgemeine Faustregel für wohlerzogene Applets gilt: Wann immer Sie eine Verarbeitung ausführen müssen, die wahrscheinlich längere Zeit andauert (z. B. eine Animationsschleife oder ein längerer Codeteil), setzen Sie diese Teile in je einen Thread.
Im obigen Datum-Applet wurden keine Threads verwendet. Statt dessen wurde die while-Schleife definiert, die die Animation direkt in die start()-Methode führt, so daß sie weiterläuft, bis der Browser oder Appletviewer beendet wird. Das mag zwar praktisch erscheinen, jedoch funktioniert die digitale Uhr nicht, weil die while-Schleife in der start()-Methode alle Systemressourcen, einschließlich der Anzeige am Bildschirm, für sich in Anspruch nimmt. Wenn Sie versuchen, das Datum-Applet zu kompilieren und auszuführen, erscheint auf dem Bildschirm gähnende Leere. Außerdem können Sie das Applet auch nicht stoppen, weil keine Möglichkeit besteht, eine stop()-Methode aufzurufen.
Die Lösung dieses Problems ist das erneute Schreiben des Applets mit Threads. Threads ermöglichen diesem Applet, sich selbst zu animieren, ohne andere Systemoperationen zu stören, und es kann gestartet und gestoppt werden. Auf diese Weise können Sie es gleichzeitig mit anderen Applets ausführen.
Wie schreiben Sie ein Applet, in dem Threads verwendet werden? Sie müssen mehrere Dinge tun. Zum Glück sind sie alle nicht schwierig. Viele Grundlagen der Verwendung von Threads in Applets sind lediglich Standardcode, den Sie von einem Applet in ein anderes kopieren können. Da das so einfach ist, gibt es angesichts ihres Nutzens fast keinen Grund, Threads in Applets nicht zu verwenden.
Um ein Applet zu schreiben, das Threads nutzt, müssen Sie vier Änderungen durchführen:
Die erste Änderung erfolgt in der ersten Zeile der Klassendefinition. Sie haben folgendes bereits vorliegen:
public class MyAppletClass extends java.applet.Applet { ... }
Ändern Sie diesen Code wie folgt ab:
public class MyAppletClass extends java.applet.Applet implements Runnable { ... }
Was wird dadurch bewirkt? Der Code beinhaltet jetzt die Unterstützung der Runnable-Schnittstelle für Ihr Applet. Wenn Sie kurz an den 2. Tag zurückdenken, werden Sie sich erinnern, daß Schnittstellen dazu dienen, Methodennamen aus verschiedenen Klassen zu sammeln, damit sie gemischt und in verschiedenen Klassen implementiert werden können, die das jeweilige Verhalten bewirken. Hier beinhaltet die Runnable-Schnittstelle das Verhalten des Applets, einen Thread auszuführen. Vor allem aber erhalten Sie eine Standarddefinition für die run()-Methode.
Im zweiten Schritt muß eine Instanzvariable eingefügt werden, die den Thread des Applets aufnimmt. Benennen Sie sie, wie Sie wollen. Es handelt sich um eine Variable vom Typ Thread (befindet sich in java.lang, deshalb müssen Sie sie nicht importieren):
Thread runner;
Im dritten Schritt müssen Sie eine start()-Methode einfügen bzw. eine vorhandene ändern, so daß sie nichts tut, außer einen neuen Thread zu erstellen und ihn auszuführen. Hier ein typisches Beispiel einer start()-Methode:
public void start() { if (runner == null); { runner = new Thread(this); runner.start(); } }
Wo steht der Körper des Applets, wenn start() so eingerichtet wird, daß die Methode außer der Ausführung eines Threads nichts bewirkt? Er steht in einer neuen Methode namens run(), die so aussieht:
public void run() { // was Ihr Applet eigentlich macht }
run() kann alles enthalten, was im separaten Thread ausgeführt werden soll: Initialisierungscode, die eigentliche Schleife für das Applet oder etwas anderes, das in einem eigenen Thread ablaufen soll. Sie können innerhalb von run() auch neue Objekte erstellen und Methode aufrufen, dann laufen sie ebenfalls in diesem Thread ab. Die run-Methode ist der Kern eines Applets.
Nun sollten Sie zu dem laufenden Thread und einer start-Methode eine stop()-Methode hinzufügen, um die Ausführung dieses Threads zu stoppen (und damit alles, was das Applet macht), wenn der Benutzer die Seite verläßt. stop() sieht ähnlich aus wie start():
public void stop() { if (runner != null) { runner.stop(); runner = null; } }
Die stop()-Methode bewirkt hier zwei Aktionen: Sie stoppt die Ausführung des Threads und setzt die Variable des Threads (runner) auf Null. Dadurch wird das Thread-Objekt für die »Müllabfuhr« bzw. den Papierkorb verfügbar, kann also jederzeit nach einer gewissen Zeit vom Speicher entfernt werden. Kehrt der Benutzer auf diese Seite zurück und ruft er dieses Applet wieder auf, erstellt die start-Methode einen neuen Thread und startet das Applet wieder.
Das ist alles! Vier einfache Änderungen und schon haben Sie ein wohlerzogenes Applet, das in einem eigenen Thread abläuft.
Wie Sie wissen, enthält das erste Beispiel mit dem Datum-Applet am Anfang dieser Lektion einen Fehler. Wir wollen diesen Fehler beheben, damit Sie einen Einblick erhalten, wie sich ein echtes Applet mit Threads verhält. Sie führen die vier Schritte durch, die oben beschrieben wurden.
Zuerst ändern Sie die Klassendefinition, um die Runnable-Schnittstelle einzufügen (die Klasse DigitalClock wird in DigitalThreads umbenannt):
public class DigitalThreads extends java.applet.Applet implements Runnable { ...
Dann fügen Sie für den Thread eine Instanzvariable ein:
Thread runner;
Im dritten Schritt kehren Sie alles um. Da der Großteil des Applets noch in der start()-Methode steht, Sie aber eine Methode namens run() brauchen, vermeiden wir umständliches Kopieren und Einfügen und benennen die vorhandene start()-Methode in run() um:
public void run() { while (true) { ...
Jetzt fügen Sie die start()- und stop()-Methode ein:
public void start() { if (runner == null); { runner = new Thread(this); runner.start(); } } public void stop() { if (runner != null) { runner.stop(); runner = null; } }
Fertig! Damit haben Sie in einer Minute ein Applet so umgewandelt, daß es Threads verwendet. Der Code für das vollständige Applet steht in Listing 10.2.
1: import java.awt.Graphics; 2: import java.awt.Font; 3: import java.util.Date; 4: 5: public class DigitalThreads extends java.applet.Applet 6: implements Runnable { 7: 8: Font theFont = new Font("TimesRoman",Font.BOLD,24); 9: Date theDate; 10: Thread runner; 11: 12: public void start() { 13: if (runner == null); { 14: runner = new Thread(this); 15: runner.start(); 16: } 17: } 18: 19: public void stop() { 20: if (runner != null) { 21: runner.stop(); 22: runner = null; 23: } 24: } 25: 26: public void run() { 27: while (true) { 28: theDate = new Date(); 29: repaint(); 30: try { Thread.sleep(1000); } 31: catch (InterruptedException e) { } 32: } 33: } 34: 35: public void paint(Graphics g) { 36: g.setFont(theFont); 37: g.drawString(theDate.toString(),10,50); 38: } 39: } 40:
Wenn Sie alle Beispiele und Übungen in diesem Buch bisher nachvollzogen und das Buch nicht in der Badewanne oder in einem Flugzeug gelesen haben, haben Sie eventuell festgestellt, daß bei der Ausführung des Datum-Programms hier und da in der Animation ein lästiges Flimmern auftritt (Nichts gegen das Lesen eines Buches in der Badewanne. Sie sehen nur das Flimmern nicht, aber glauben Sie mir, es flimmert wirklich!). Das ist kein Programmfehler. Flimmern ist eine Nebenwirkung von Java-Animationen. Da es wirklich lästig ist, lernen Sie in diesem Teil der heutigen Lektion, wie man dieses Flimmern reduzieren kann, damit Ihre Animationen sauberer ablaufen und am Bildschirm besser aussehen.
Dieses Flimmern wird durch die Art verursacht, in der Java die einzelnen Rahmen eines Applets zeichnet und wiederholt. Am Anfang der heutigen Lektion haben Sie gelernt, daß die repaint()-Methode paint() aufruft. Das ist nicht ganz richtig. Ein Aufruf von paint() findet als Reaktion auf repaint() statt, jedoch laufen tatsächlich folgende Schritte ab:
1. Der Aufruf von repaint() führt zu einem Aufruf der Methode update(). 2. Die update()-Methode leert den Bildschirm (füllt ihn mit der aktuellen Hintergrundfarbe) und ruft dann paint() auf. 3. Die paint()-Methode zeichnet den Inhalt des aktuellen Rahmens.
In Schritt 2, dem Aufruf von update(), entsteht dieses Flimmern. Da der Bildschirm zwischen einzelnen Rahmen geleert wird, wechseln die Teile des Bildschirms, die sich nicht ändern, schneller als diejenigen, die neu gezeichnet werden müssen. Dadurch entsteht der Flimmereffekt.
Im wesentlichen gibt es zwei Möglichkeiten, diesen Flimmereffekt in Java-Applets zu vermeiden:
Das zweite Verfahren hört sich kompliziert an und ist es auch. Doppeltes Puffern bedeutet das Zeichnen auf einer Grafikoberfläche außerhalb des Bildschirms und das anschließende Kopieren dieser gesamten Oberfläche auf den Bildschirm. Da dieses Verfahren kompliziert ist, lernen Sie es morgen. Heute lernen Sie die einfachere Lösung: Überschreiben von update.
Die Ursache des Flimmerns liegt an der update()-Methode. Um das Flimmern zu reduzieren, überschreiben wir sowohl update() als auch paint(). Nachfolgend sehen Sie, was die Standardversion von update() bewirkt (befindet sich in der Component-Klasse, über die Sie am 13. Tag mehr lernen):
public void update(Graphics g) { g.setColor(getBackground()); g.fillRect(0, 0, width, height); g.setColor(getForeground()); paint(g); }
Im Grunde leert update() den Bildschirm (oder, um genauer zu sein, füllt das Applet-Rechteck mit der Hintergrundfarbe), setzt alles auf Normal zurück und ruft dann paint() auf. Wenn Sie update() überschreiben, müssen Sie diese zwei Aspekte berücksichtigen und sicherstellen, daß Ihre Version von update() ähnliche Aufgaben ausführt. In den nächsten zwei Abschnitten arbeiten Sie ein paar Beispiele durch, in denen update() unterschiedlich überschrieben wird, um den Flimmereffekt zu verringern.
Die erste Lösung zur Verringerung des Flimmereffekts ist, den Bildschirm überhaupt nicht zu leeren. Das funktioniert selbstverständlich nicht bei allen Applets. Hier ein Beispiel eines Applets, bei dem das möglich ist. Das Applet ColorSwirl gibt eine Zeichenkette am Bildschirm aus (»All the Swirly colors«), jedoch wird diese Zeichenkette in verschiedenen Farben dargestellt, die dynamisch ineinander übergehen. Dieses Applet flimmert furchtbar. Listing 10.3 enthält den Sourcecode für dieses Applet und Abb. 10.2 zeigt das Ergebnis.
1: import java.awt.Graphics; 2: import java.awt.Color; 3: import java.awt.Font;
4: 5: public class ColorSwirl extends java.applet.Applet 6: implements Runnable { 7: 8: Font f = new Font("TimesRoman",Font.BOLD,48); 9: Color colors[] = new Color[50]; 10: Thread runThread; 11: 12: public void start() { 13: if (runThread == null) { 14: runThread = new Thread(this); 15: runThread.start(); 16: } 17: } 18: 19: public void stop() { 20: if (runThread != null) { 21: runThread.stop(); 22: runThread = null; 23: } 24: } 25: 26: public void run() } 27: 28: // Farbenreihe initialisieren 29: float c = 0; 30: for (int i = 0; i < colors.length; i++) { 31: colors[i] = 32: Color.getHSBColor(c, (float)1.0,(float)1.0); 33: c += .02; 34: } 35: 36: // Farben durchlaufen 37: int i = 0; 38: while (true) { 39: setForeground(colors[i]); 40: repaint(); 41: i++; 42: try { Thread.sleep(50); } 43: catch (InterruptedException e) { } 44: if (i == colors.length ) i = 0; 45: } 46: } 47: 48: public void paint(Graphics g) { 49: g.setFont(f); 50: g.drawString("All the Swirly Colors", 15,50); 51: } 52: }
Abbildung 10.2: Das ColorSwirl-Applet
Da Sie nun wissen, was das Applet macht, wenden wir uns der Korrektur des Flimmereffekts zu. Flimmern entsteht, weil jedesmal, wenn das Applet gezeichnet wird, der Bildschirm geleert wird. Der Text wechselt nicht sanft von Rot auf Pink und Lila, sondern springt von Rot auf Grau, Pink auf Grau, Lila auf Grau usw., was nicht besonders schön aussieht.
Da dieses Problem allein auf das Leeren des Bildschirms zurückzuführen ist, ist die Lösung einfach: Überschreiben von update() und Entfernen des Teils, in dem der Bildschirm geleert wird. An sich muß der Bildschirm ohnehin nicht geleert werden, weil sich außer der Textfarbe nichts ändert. Wird die Eigenschaft des Bildschirmleerens aus update() entfernt, muß update() nur noch paint() aufrufen. Diese update()-Methode sieht so aus:
public void update(Graphics g) { paint(g); }
Durch Einfügen dieses kleinen Dreizeilers haben Sie den lästigen Flimmereffekt ausgemerzt. Einfacher geht's nicht.
Bei manchen Applets kann diese einfache Lösung nicht angewandt werden. Im folgenden Beispiel befassen wir uns mit einem Applet namens Checkers. Ein rotes Oval (ein Damestein) bewegt sich von einem schwarzen auf ein weißes Viereck wie auf einem Damebrett. Listing 10.4 enthält den Code für dieses Applet. Das Applet ist in Abb. 10.3 zu sehen.
Listing 10.4: Das Checkers-Applet
1: import java.awt.Graphics; 2: import java.awt.Color; 3: 4: public class Checkers extends java.applet.Applet 5: implements Runnable { 6: 7: Thread runner; 8: int xpos; 9: 10: public void start() { 11: if (runner == null); { 12: runner = new Thread(this); 13: runner.start(); 14: } 15: } 16: 17: public void stop() { 18: if (runner != null) { 19: runner.stop(); 20: runner = null; 21: } 22: } 23: 24: public void run() { 25: setBackground(Color.blue); 26: while (true) { 27: for (xpos = 5; xpos <= 105; xpos+=4) { 28: repaint(); 29: try { Thread.sleep(100); } 30: catch (InterruptedException e) { } 31: } 32: for (xpos = 105; xpos > 5; xpos -=4) { 33: repaint(); 34: try { Thread.sleep(100); } 35: catch (InterruptedException e) { } 36: } 37: } 38: } 39: 40: public void paint(graphics g) { 41: // Hintergrund zeichnen 42: g.setColor(Color.black); 43: g.fillrect(0,0,100,100); 44: g.setColor(Color.white); 45: g.fillRect(101,0,100,100); 46: 47: // Damestein zeichnen 48: g.setColor(Color.red); 49: g.fillOval(xpos,5,90,90); 50: } 51: }
Abbildung 10.3: Das Checkers-Applet
In der paint()-Methode werden die Hintergrundvierecke (ein schwarzes und ein weißes) gezeichnet, dann wird der Damestein in seiner aktuellen Position gezeichnet.
Wie das ColorSwirl-Applet flimmert dieses Applet sehr stark. (In Zeile 25 ist der Hintergrund blau, deshalb können Sie das Flimmern sehen, wenn Sie das Applet ausführen.)
Die Lösung des Flimmerproblems ist bei diesem Applet allerdings schwieriger als beim letzten, weil der Bildschirm geleert werden muß, bevor der nächste Rahmen gezeichnet wird. Andernfalls ist der rote Damestein beim Übergang von einer Position zur anderen nicht sichtbar. Vielmehr sieht man nur etwas verschmiertes Rotes, das sich auf dem Hintergrund bewegt.
Wie kann man das vermeiden? Der Bildschirm muß geleert werden, damit der Animationseffekt gewahrt bleibt. Anstatt den ganzen Bildschirm zu leeren, wird nur der Teil geleert, der sich ändert. Durch Eingrenzen der erneuten Zeichnung auf einen kleinen Bereich können Sie einen Großteil des Geflimmers vermeiden.
Um den nachzuzeichnenden Umfang einzugrenzen, müssen Sie zwei Dinge berücksichtigen. Erstens ist eine Möglichkeit erforderlich, um den Zeichnungsbereich zu begrenzen, so daß bei jedem Aufruf von paint() nur der nachzuzeichnende Teil ausgegeben wird. Hierfür gibt es in Java einen einfachen Mechanismus namens Clipping.
Zweitens brauchen Sie eine Möglichkeit, den tatsächlich nachzuzeichnenden Bereich zu verfolgen. Sowohl die linke als auch die rechte Ecke des Zeichnungsbereichs ändert sich bei jedem Animationsrahmen (auf einer Seite muß das neue Oval nachgezeichnet werden, auf der andere muß das vom vorherigen Rahmen übriggebliebene Ovalstückchen links gelöscht werden), um diese zwei x-Werte zu verfolgen. Sie benötigen Instanzvariablen für die linke und rechte Seite.
Mit diesen zwei Konzepten vor Augen beginnen wir mit dem Ändern des Chekkers-Applets, so daß nur das nachgezeichnet wird, was nötig ist. Zuerst fügen Sie Instanzvariablen für die linke und rechte Kante des Zeichnungsbereichs ein. Wir nennen diese Instanzvariablen ux1 und ux2 (u für Update), wobei ux1 die linke und ux2 die rechte Seite des zu zeichnenden Bereichs ist.
int ux1, ux2;
Dann ändern wir die run()-Methode so ab, daß sie den tatsächlich zu zeichnenden Bereich verfolgt. Das erreichen wir aber nicht einfach nur durch Aktualisieren der Seiten für die Wiederholungen der Animation. Aufgrund der Art, wie Java paint() und repaint() anwendet, ist das etwas komplizierter.
Bei der Aktualisierung der Kanten des Zeichnungsbereichs für jeden Rahmen der Animation liegt das Problem daran, daß es für jeden Aufruf von repaint() nicht unbedingt ein einzelnes entsprechendes paint() gibt. Werden die Systemressourcen knapp (weil andere Programme laufen oder aus einem anderen Grund), wird paint() eventuell nicht sofort ausgeführt, so daß mehrere Aufrufe von paint() in einer Schlange warten, bis sich ihre Pixel am Bildschirm ändern. In diesem Fall kompensiert Java, indem es nur den letzten Aufruf von paint() ausführt und alle anderen überspringt, anstatt zu versuchen, alle Aufrufe von paint() in der richtigen Reihenfolge zu bedienen.
Wenn Sie die Kanten des Zeichnungsbereichs bei jedem repaint() aktualisieren und einige Aufrufe von paint() übersprungen werden, werden Teile der Zeichnungsoberfläche nicht aktualisiert und Teile des Ovals hinken hinterher. Es gibt eine einfache Möglichkeit, dies zu vermeiden: Sie aktualisieren die führende Kante des Ovals jedesmal, wenn der Rahmen aktualisiert wird, jedoch aktualisieren Sie die nachfolgende Kante erst, nachdem das letzte Paint stattgefunden hat. Auf diese Weise werden einige Aufrufe von paint() übersprungen, der Zeichnungsbereich wird für jeden Rahmen größer und wenn paint() schließlich aufholt, wird alles korrekt ausgegeben.
Das ist ziemlich komplex. Wenn es eine einfachere Möglichkeit gäbe, hätte ich dieses Applet einfacher geschrieben. Leider wird das Applet nur mit diesem Mechanismus richtig angezeigt. Wir gehen den Code nun langsam durch, damit Sie nachvollziehen können, was in jedem Schritt passiert.
Wir beginnen mit run(), weil sich dort die Rahmen der Animation befinden. Hier berechnen Sie jede Seite des Zeichnungsbereichs auf der Grundlage der alten und neuen Position des Ovals. Bei der Bewegung des Ovals nach links am Bildschirm ist das einfach. Der Wert von ux1 (die linke Seite des Zeichnungsbereichs) ist die x-Position des vorherigen Ovals (xpos). Der Wert von ux2 ist die x-Position des aktuellen Ovals plus dessen Breite (in diesem Beispiel 90 Pixel).
Hier noch einmal die alte run()-Methode, um Ihr Gedächtnis aufzufrischen:
public void run() { setBackground(Color.blue); while (true) { for (xpos = 5; xpos <= 105; xpos+=4) { repaint(); try { Thread.sleep(100); } catch (InterruptedException e) { } } for (xpos = 105; xpos > 5; xpos -=4) { repaint(); try { Thread.sleep(100); } catc (InterruptedException e) { } } } }
In der ersten for-Schleife der run()-Methode, in der sich das Oval nach rechts bewegt, aktualisieren Sie zuerst ux2 (die rechte Kante des Zeichnungsbereichs):
ux2 = xpos + 90;
Nach repaint() aktualisieren Sie ux1, um die alte x-Position des Ovals festzuhalten. Allerdings muß dieser Wert erst aktualisiert werden, nachdem paint stattgefunden hat. Wie können Sie wissen, wann das der Fall war? Sie können ux1 in paint() auf einen bestimmten Wert (0) zurücksetzen und dann das Applet testen, um zu sehen, ob Sie diesen Wert aktualisieren können oder auf die Ausführung von paint() warten müssen:
if (ux1 == 0) ux1 = xpos;
Hier die neue for-Schleife für die Bewegung des Ovals nach rechts:
for (xpos = 5; xpos <= 105; xpos+=4) } ux2 = xpos + 90; repaint(); try { Thread.sleep(100); } catch (InterruptedException e) { } if (ux1 == 0) ux1 = xpos; }
Wenn sich das Oval nach links bewegt, wird alles umgedreht. ux1, die linke Seite, ist die führende Kante des Ovals, die jedesmal aktualisiert wird. ux2, die rechte Seite, muß warten, bis sie aktualisiert wird. Deshalb müssen Sie in der zweiten for-schleife zuerst ux1 auf die x-Position des aktuellen Ovals fortschreiben:
ux1 = xpos;
Nachdem repaint() aufgerufen wurde, testen Sie Ihre Arbeit zuerst, dann aktualisieren Sie ux2:
if (ux2 == 0) ux2 = xpos + 90;
Die neue Version der zweiten for-Schleife innerhalb von run() sieht so aus:
for (xpos = 105; xpos > 5; xpos -=4) { ux1 = xpos; repaint(); try { thread.sleep(100); } catch (InterruptedException e) { } if (ux2 == 0) ux2 = xpos + 90; }
Das sind die einzigen in run() erforderlichen Änderungen. Wir überschreiben nun update, um den zu zeichnenden Bereich auf die linke und rechte Kante des Zeichnungsbereichs, den Sie in run() festgelegt haben, einzuschränken. Um den Zeichnungsbereich auf ein spezifisches Rechteck zu clippen, verwenden Sie die clipRect()-Methode. clipRect() ist wie drawRect(), fillRect() und clearRect() für Graphics-Objekte definiert und erhält vier Argumente: die Anfangspositionen x und y sowie die Breite und Höhe des Bereichs.
ux1 und ux2 spielen folgende Rolle: ux1 ist der x-Punkt der oberen Ecke des Bereichs. Anhand von ux2 wird die Breite des Bereichs ermittelt, indem ux1 davon abgezogen wird. Schließlich wird paint() aufgerufen, um update() zu vervollständigen:
public void update(Graphics g) { g.clipRect(ux1, 5, ux2 - ux1, 95);; paint(g); }
Da Sie den Bereich bereits mitgeclippt haben, brauchen Sie an der paint()-Methode nichts mehr zu ändern. paint() zeichnet zwar den gesamten Bildschirm jedesmal, jedoch ändert sich nur der Clipping-Bereich.
Die nachfolgende Kante der Zeichnungsbereiche in paint() muß für den Fall aktualisiert werden, daß mehrere Aufrufe von paint() übersprungen werden. Da Sie in run() auf einen Wert von 0 testen, setzen Sie einfach ux1 und ux2 nach der Ausgabe am Bildschirm zurück:
ux1 = ux2 = 0;
Das sind die Änderungen, die Sie in diesem Applet durchführen müssen, damit nur die Teile gezeichnet werden, die sich ändern (und zu berücksichtigen, daß eventuell einige Rahmen nicht sofort aktualisiert werden). Dadurch wird zwar der Flimmereffekt in der Animation nicht ganz ausgemerzt, jedoch stark verringert. Versuchen Sie es selbst. Der korrigierte Code für das Checkers-Applet ist in Listing 10.5 aufgeführt.
Listing 10.5: Das korrigierte Checkers-Applet
1: import java.awt.Graphics; 2: import java.awt.Color; 3: 4: public class Checkers2 extends java.applet.Applet implements Runnable { 5: 6: Thread runner; 7: int xpos; 8: int ux1,ux2; 9: 10: public void start() { 11: if (runner == null); { 12: runner = new Thread(this); 13: runner.start(); 14: } 15: } 16: 17: public void stop() { 18: if (runner != null) { 19: runner.stop(); 20: runner = null; 21: } 22: } 23: 24: public void run() } 25: setbackground(Color.blue); 26: while (true) { 27: for (xpos = 5; xpos <= 105; xpos+=4) { 28: ux2 = xpos + 90; 29: repaint(); 30: try { Thread.sleep(100); } 31: catch (InterruptedException e) { } 32: if (ux1 == 0) ux1 = xpos; 33: } 34: for (xpos = 105; xpos > 5; xpos -=4) { 35: ux1 = xpos; 36: repaint(); 37: try { Thread.sleep(100); } 38: catch (Interruptedexception e) { } 39: if (ux2 == 0) ux2 = xpos + 90; 40: } 41: } 42: } 43: public void update(Graphics g) { 44: g.clipRect(ux1, 5, ux2 - ux1, 95); 45: paint(g); 46: } 47: 48: public void paint(Graphics g) { 49: // Hintergrund zeichnen 50: g.setColor(Color.black); 51: g.fillRect(0,0,100,100); 52: g.setColor(Color.white); 53: g.fillRect(101,0,100,100); 54: 55: // Damestein zeichnen 56: g.setColor(Color.red); 57: g.fillOval(xpos,5,90,90); 58: 59: // Zeichnungsbereich zurücksetzen 60: ux1 = ux2 = 0; 61: } 62: }
Herzlichen Glückwunsch! Sie haben diese etwas schwierige Lektion bewältigt und eine Menge gelernt. Sie haben die Anwendung und das Überschreiben zahlreicher Methoden gelernt - start(), stop(), paint(), repaint(), run() und update() - und Sie haben eine solide Basis zum Erstellen und Anwenden von Threads erworben.
Mit dem heutigen Tag haben Sie den Großteil der Grundlagen für die Entwicklung von Applets überwunden. Abgesehen vom Arbeiten mit Bitmap-Bildern, das Sie morgen lernen, haben Sie sich die Grundlagen angeeignet, um fast jede Animation in Java zu entwickeln.
F: Warum muß man so umständlich mit paint, repaint und update und all dem arbeiten? Warum erstellt man nicht eine einfache paint-Methode, die Sachen zum gewünschten Zeitpunkt am Bildschirm ausgibt?
A: Das AWT-Toolkit von Java ermöglicht Ihnen, mehrere zeichenbare Oberflächen ineinander zu verschachteln. Wenn paint ausgeführt wird, werden alle Teile des Systems nachgezeichnet, beginnend mit der äußersten Oberfläche nach unten zur innersten in der Verschachtelung. Ein Applet wird gleichzeitig mit allem anderen ausgegeben und nicht besonders behandelt. Sie opfern dabei zwar die Unmittelbarkeit des sofortigen Zeichnens, jedoch schmiegt sich das Applet sauberer in den Rest des Systems ein.
F: Sind Java-Threads mit Threads in anderen Systemen vergleichbar?
A: Java-Threads wurden von anderen Thread-Systemen beeinflußt, und wenn Sie mit Threads vertraut sind, kommen Ihnen viele Konzepte von Java-Threads bekannt vor. Sie haben die Grundlagen heute gelernt. Am 17.Tag lernen Sie mehr darüber.
F: Muß ich, wenn ein Applet Threads benutzt, lediglich festlegen, wo der Thread beginnen und enden soll? Mehr nicht? Muß ich nicht etwas in meinen Schleifen testen oder den Zustand der Threads verfolgen?
A: Nein, das genügt. Wenn Sie Ihr Applet in einen Thread stellen, kann Java die Ausführung des Applets schneller steuern. Wird der Thread zum Stoppen veranlaßt, wird die Ausführung des Applets unterbrochen und wieder aufgenommen, wenn der Thread wieder startet. Herrlich, daß das alles automatisch abläuft.
F: Das ColorSwirl-Applet zeigt bei mir nur fünf oder sechs Farben an. Woran liegt das?
A: Das ist das gleiche Problem, das Sie gestern schon hatten. Bei einigen Systemen sind eventuell nicht genügend Farben verfügbar, um alle anzuzeigen. Abgesehen von der Aufrüstung Ihrer Hardware können Sie in diesem Fall versuchen, andere Anwendungen zu schließen, da sie eventuell die im Applet fehlenden Farben verwenden. Andere Browser oder Farbwerkzeuge belegen eventuell mehrere Farben.
F: Ich habe alle Anweisungen befolgt, um das Checkers-Applet zu korrigieren, trotzdem flimmert es sehr stark.
A: Und leider wird das auch weiterhin so sein. Durch Clipping, d. h. Reduzieren der Größe des Zeichnungsbereichs, wird der Flimmereffekt stark verringert, jedoch nicht völlig beseitigt. Bei vielen Applets genügen eventuell die heute beschriebenen Methoden, um Flimmern in einer Animation so zu reduzieren, daß es nicht mehr störend wirkt. Morgen lernen Sie eine Technik namens Double-Buffering, durch die der Flimmereffekt in einer Animation völlig beseitigt werden kann.
Copyright ©1996 Markt&Technik