Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage
<< | < | > | >> | API | Kapitel 34 - Bitmaps und Animationen

## 34.2 Animation

- 34.2 Animation
  - 34.2.1 Prinzipielle Vorgehensweise
    - Die repaint-Schleife
    - Verwendung von Threads
  - 34.2.2 Abspielen einer Folge von Bitmaps
  - 34.2.3 Animation mit Grafikprimitiven
  - 34.2.4 Reduktion des Bildschirmflackerns
    - Bildschirm nicht löschen
    - Nur den wirklich benötigten Teil des Bildschirms löschen
    - Doppelpufferung

### 34.2.1 Prinzipielle Vorgehensweise

Das Darstellen einer Animation auf dem Bildschirm ist im Prinzip nichts anderes als die schnell aufeinanderfolgende Anzeige einer Sequenz von Einzelbildern. Die Bildfolge erscheint dem menschlichen Auge aufgrund seiner Trägheit als zusammenhängende Bewegung.

Obwohl die prinzipielle Vorgehensweise damit klar umrissen ist, steckt die Tücke bei der Darstellung von animierten Bildsequenzen im Detail. Zu den Problemen, die in diesem Zusammenhang zu lösen sind, gehören:

- Für die Anzeige der Einzelbilder muß das richtige Timing gewählt werden. Werden zu wenig Bilder je Zeiteinheit angezeigt, erscheint das Ergebnis ruckelig. Werden zu viele gezeigt, kann es sein, daß `paint`-Events verlorengehen und Teile des Bildes nicht korrekt angezeigt werden.
- Wird zur Anzeige der Einzelbilder der normale `paint`-/`repaint`-Mechanismus von Java verwendet, wird die Darstellung durch ein starkes Flackern gestört. Wir werden eine Reihe von Techniken kennenlernen, mit denen dieses Flackern verhindert werden kann.
- Die Darstellung der Einzelbilder darf nicht die Interaktivität des Programms beeinträchtigen. Vor allem darf die Nachrichtenschleife nicht blockiert werden. Wir werden lernen, wie man diese Probleme mit Hilfe separater Threads umgehen kann.

All dies sind Standardprobleme, die vom Programmierer bei der Entwicklung von Animationen zu lösen sind. Wir werden feststellen, daß Java dafür durchweg brauchbare Lösungen zu bieten hat und die Programmierung kleiner Animationen recht einfach zu realisieren ist.

#### Die repaint-Schleife

Das Grundprinzip einer Animation besteht darin, in einer Schleife die Methode `repaint` wiederholt aufzurufen. Ein Aufruf von `repaint` führt dazu, daß die `paint`-Methode aufgerufen wird, und innerhalb von `paint` generiert die Anwendung dann die für das aktuelle Einzelbild benötigte Bildschirmausgabe.

`paint` muß sich also merken (oder mitgeteilt bekommen), welches Bild bei welchem Aufruf erzeugt werden soll. Typischerweise wird dazu ein Schleifenzähler verwendet, der das gerade anzuzeigende Bild bezeichnet. Nach dem Ausführen der Ausgabeanweisungen terminiert `paint`, und der Aufrufer wartet eine bestimmte Zeitspanne. Dann zählt er den Bildzähler hoch und führt den nächsten Aufruf von `repaint` durch. Dies setzt sich so lange fort, bis die Animation beendet ist oder das Programm abgebrochen wird.

Das folgende Listing stellt eines der einfachsten Beispiele für eine Grafikanimation dar:

    001 /* Listing3406.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005
    006 public class Listing3406
    007 extends Frame
    008 {
    009   int cnt = 0;
    010
    011   public static void main(String[] args)
    012   {
    013     Listing3406 wnd = new Listing3406();
    014     wnd.setSize(250,150);
    015     wnd.setVisible(true);
    016     wnd.startAnimation();
    017   }
    018
    019   public Listing3406()
    020   {
    021     super("Animierter Zähler");
    022     setBackground(Color.lightGray);
    023     addWindowListener(new WindowClosingAdapter(true));
    024   }
    025
    026   public void startAnimation()
    027   {
    028     while (true) {
    029       repaint();
    030     }
    031   }
    032
    033   public void paint(Graphics g)
    034   {
    035     ++cnt;
    036     g.drawString("Counter = "+cnt,10,50);
    037     try {
    038       Thread.sleep(1000);
    039     } catch (InterruptedException e) {
    040     }
    041   }
    042 }
| Listing3406.java

Listing 34.6: Ein animierter Zähler

Das Programm öffnet ein Fenster und zählt in Sekundenabständen einen Zähler um eins hoch:

Abbildung 34.3: Ein animierter Zähler

|  |  |  | Leider hat das Programm einen entscheidenden Nachteil. Die Animation selbst funktioniert zwar wunderbar, aber das Programm reagiert nur noch sehr schleppend auf Windows-Nachrichten. Wir wollen zunächst dieses Problem abstellen und uns ansehen, wie man die `repaint`-Schleife in einem eigenen `Thread` laufen läßt. |  |
Warnung

#### Verwendung von Threads

Um die vorherige Version des Programms zu verbessern, sollte die `repaint`-Schleife in einem eigenen `Thread` laufen. Zusätzlich ist es erforderlich, die Zeitverzögerung aus `paint` herauszunehmen und statt dessen in die `repaint`-Schleife zu verlagern. So bekommt der Haupt-Thread des Animationsprogramms genügend Zeit, die Bildschirmausgabe durchzuführen und kann andere `Events` bearbeiten. Daß in einem anderen `Thread` eine Endlosschleife läuft, merkt er nur noch daran, daß in regelmäßigen Abständen `repaint`-Ereignisse eintreffen.

Um das Programm auf die Verwendung mehrerer Threads umzustellen, sollte die Fensterklasse das Interface `Runnable` implementieren und eine Instanzvariable vom Typ `Thread` anlegen. Dann wird die Methode `startAnimation` so modifiziert, daß sie den neuen `Thread` instanziert und startet. Die eigentliche `repaint`-Schleife wird in die Methode `run` verlagert. Schließlich sollte beim Beenden des Programms auch der laufende `Thread` beendet werden. Hier ist die modifizierte Fassung:

    001 /* Listing3407.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005
    006 public class Listing3407
    007 extends Frame
    008 implements Runnable
    009 {
    010   int cnt = 0;
    011
    012   public static void main(String[] args)
    013   {
    014     Listing3407 wnd = new Listing3407();
    015     wnd.setSize(250,150);
    016     wnd.setVisible(true);
    017     wnd.startAnimation();
    018   }
    019
    020   public Listing3407()
    021   {
    022     super("Animations-Threads");
    023     setBackground(Color.lightGray);
    024     addWindowListener(new WindowClosingAdapter(true));
    025   }
    026
    027   public void startAnimation()
    028   {
    029     Thread th = new Thread(this);
    030     th.start();
    031   }
    032
    033   public void run()
    034   {
    035     while (true) {
    036       repaint();
    037       try {
    038         Thread.sleep(1000);
    039       } catch (InterruptedException e) {
    040         //nichts
    041       }
    042     }
    043   }
    044
    045   public void paint(Graphics g)
    046   {
    047     ++cnt;
    048     g.drawString("Counter = "+cnt,10,50);
    049   }
    050 }
| Listing3407.java

Listing 34.7: Verwendung von Threads zur Animation

Das so modifizierte Programm erzeugt dieselbe Ausgabe wie das vorige, ist aber in der Lage, in der gewohnten Weise auf Ereignisse zu reagieren. Selbst wenn die Verzögerungsschleife ganz entfernt und der Hauptprozeß so pausenlos mit `repaint`-Anforderungen bombardiert würde, könnte das Programm noch normal beendet werden.

### 34.2.2 Abspielen einer Folge von Bitmaps

Eine der einfachsten und am häufigsten verwendeten Möglichkeiten, eine Animation zu erzeugen, besteht darin, die zur Darstellung erforderliche Folge von Bitmaps aus einer Reihe von Bilddateien zu laden. Jedem Einzelbild wird dabei ein `Image`-Objekt zugeordnet, das vor dem Start der Animation geladen wird. Alle Images liegen in einem Array oder einem anderen Container und werden in der `repaint`-Schleife nacheinander angezeigt.

Das folgende Programm speichert die 30 anzuzeigenden Einzelbilder in einem Array `arImg`, das nach dem Start des Programms komplett geladen wird. Da dieser Vorgang einige Sekunden dauern kann, zeigt das Programm den Ladefortschritt auf dem Bildschirm an:

Abbildung 34.4: Die Ausgabe während des Ladevorgangs

Erst nach dem vollständigen Abschluß des Ladevorgangs, der mit einem `MediaTracker` überwacht wird, beginnt die eigentliche Animation. Die ganzzahlige Instanzvariable `actimage` dient als Zähler für die Bildfolge und wird nacheinander von 0 bis 29 hochgezählt, um dann wieder bei 0 zu beginnen. Nach jedem Einzelbild wartet das Programm 50 Millisekunden und führt dann den nächsten Aufruf von `repaint` durch:

    001 /* Listing3408.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005
    006 public class Listing3408
    007 extends Frame
    008 implements Runnable
    009 {
    010   Thread th;
    011   Image[] arImg;
    012   int actimage;
    013
    014   public static void main(String[] args)
    015   {
    016     Listing3408 wnd = new Listing3408();
    017     wnd.setSize(200,150);
    018     wnd.setVisible(true);
    019     wnd.startAnimation();
    020   }
    021
    022   public Listing3408()
    023   {
    024     super("Bitmap-Folge");
    025     addWindowListener(new WindowClosingAdapter(true));
    026   }
    027
    028   public void startAnimation()
    029   {
    030     th = new Thread(this);
    031     actimage = -1;
    032     th.start();
    033   }
    034
    035   public void run()
    036   {
    037     //Bilder laden
    038     arImg = new Image[30];
    039     MediaTracker mt = new MediaTracker(this);
    040     Toolkit tk = getToolkit();
    041     for (int i = 1; i <= 30; ++i) {
    042       arImg[i-1] = tk.getImage("images/jana"+i+".gif");
    043       mt.addImage(arImg[i-1], 0);
    044       actimage = -i;
    045       repaint();
    046       try {
    047         mt.waitForAll();
    048       } catch (InterruptedException e) {
    049         //nothing
    050       }
    051     }
    052     //Animation beginnen
    053     actimage = 0;
    054     while (true) {
    055       repaint();
    056       actimage = (actimage + 1) % 30;
    057       try {
    058         Thread.sleep(50);
    059       } catch (InterruptedException e) {
    060         //nichts
    061       }
    062     }
    063   }
    064
    065   public void paint(Graphics g)
    066   {
    067     if (actimage < 0) {
    068       g.drawString("Lade Bitmap "+(-actimage),10,50);
    069     } else {
    070       g.drawImage(arImg[actimage],10,30,this);
    071     }
    072   }
    073 }
| Listing3408.java

Listing 34.8: Abspielen einer Folge von Bitmaps

|  | Das vorliegende Beispiel verwendet die Bilddateien jana1.gif bis jana30.gif. Sie zeigen die verschiedenen Phasen des in Schreibschrift geschriebenen Namens »Jana«. Alternativ kann aber auch jede andere Sequenz von Bilddateien verwendet werden. Die folgenden Abbildungen zeigen einige Schnappschüsse der Programmausgabe: |  |
Hinweis

Abbildung 34.5: Animation eines Schriftzugs, Schnappschuß 1

Abbildung 34.6: Animation eines Schriftzugs, Schnappschuß 2

Abbildung 34.7: Animation eines Schriftzugs, Schnappschuß 3

### 34.2.3 Animation mit Grafikprimitiven

Alternativ zur Anzeige von Bilddateien kann jedes Einzelbild der Animation natürlich auch mit den Ausgabeprimitiven der Klasse `Graphics` erzeugt werden. Dies hat den Vorteil, daß der Anwender nicht auf das Laden der Bilder warten muß. Außerdem ist das Verfahren flexibler als der bitmap-basierte Ansatz. Der Nachteil ist natürlich, daß die Grafikoperationen zeitaufwendiger sind und eine zügige Bildfolge bei komplexen Sequenzen schwieriger zu erzielen ist.

Als Beispiel für diese Art von Animation wollen wir uns die Aufgabe stellen, eine aus rechteckigen Kästchen bestehende bunte Schlange über den Bildschirm laufen zu lassen. Sie soll an den Bildschirmrändern automatisch umkehren und auch innerhalb des Ausgabefensters von Zeit zu Zeit ihre Richtung wechseln.

Das folgende Programm stellt die Schlange als `Vector` von Objekten des Typs `ColorRectangle` dar. `ColorRectangle` ist aus `Rectangle` abgeleitet und besitzt zusätzlich eine Membervariable zur Darstellung der Farbe des Rechtecks.

Dieses Beispiel folgt dem allgemeinen Architekturschema für Animationen, das wir auch in den letzten Beispielen verwendet haben. Der erste Schritt innerhalb von `run` besteht darin, die Schlange zu konstruieren. Dazu wird eine Folge von Objekten der Klasse `ColorRectangle` konstruiert, und ab Position `(100,100)` werden die Objekte horizontal nebeneinander angeordnet. Die Farben werden dabei so vergeben, daß die Schlange in fließenden Übergängen von rot bis blau dargestellt wird. Alle Elemente werden in dem `Vector` `snake` gespeichert.

Nachdem die Schlange konstruiert wurde, beginnt die Animation. Dazu wird die aktuelle Schlange angezeigt, eine Weile pausiert und dann durch Aufruf der Methode `moveSnake` die nächste Position der Schlange berechnet. `moveSnake` ist relativ aufwendig, denn hier liegt der Löwenanteil der »Intelligenz« der Animation. Die Richtung der Bewegung der Schlange wird durch die Variablen `dx` und `dy` getrennt für die x- und y-Richtung bestimmt. Steht hier der Wert -1, bewegt sich die Schlange im nächsten Schritt um die Breite eines Rechtecks in Richtung kleinerer Koordinaten. Bei 1 vergrößert sie die Koordinate entsprechend, und wenn der Wert 0 enthalten ist, verändert sich der zugehörige Koordinatenwert im nächsten Schritt gar nicht.

`dx` und `dy` werden entweder dann verändert, wenn die Schlange an einem der vier Bildschirmränder angekommen ist und umkehren muß oder (im Mittel bei jedem zehnten Schritt) auch auf freier Strecke. Nachdem auf diese Weise die neue Richtung bestimmt wurde, wird das erste Element der Schlange auf die neue Position bewegt. Alle anderen Elemente der Schlange bekommen dann die Position zugewiesen, die zuvor ihr Vorgänger hatte.

Eine alternative Art, die Schlange neu zu berechnen, würde darin bestehen, lediglich ein neues erstes Element zu generieren, an vorderster Stelle in den `Vector` einzufügen und das letzte Element zu löschen. Dies hätte allerdings den Nachteil, daß die Farbinformationen von vorne nach hinten durchgereicht würden und so jedes Element seine Farbe ständig ändern würde. Dieses (sehr viel performantere) Verfahren könnte verwendet werden, wenn alle Elemente der Schlange dieselbe Farbe hätten.

Hier ist der Quellcode zu der Schlangenanimation:

    001 /* Listing3409.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005 import java.util.*;
    006
    007 class ColorRectangle
    008 extends Rectangle
    009 {
    010   public Color color;
    011 }
    012
    013 public class Listing3409
    014 extends Frame
    015 implements Runnable
    016 {
    017   //Konstanten
    018   private static final int   SIZERECT    = 7;
    019   private static final int   SLEEP       = 40;
    020   private static final int   NUMELEMENTS = 20;
    021   private static final Color BGCOLOR     = Color.lightGray;
    022
    023   //Instanzvariablen
    024   private Thread th;
    025   private Vector snake;
    026   private int dx;
    027   private int dy;
    028
    029   public static void main(String[] args)
    030   {
    031     Listing3409 frame = new Listing3409();
    032     frame.setSize(200,150);
    033     frame.setVisible(true);
    034     frame.startAnimation();
    035   }
    036
    037   public Listing3409()
    038   {
    039     super("Animierte Schlange");
    040     setBackground(BGCOLOR);
    041     addWindowListener(new WindowClosingAdapter(true));
    042     snake = new Vector();
    043   }
    044
    045   public void startAnimation()
    046   {
    047     th = new Thread(this);
    048     th.start();
    049   }
    050
    051   public void run()
    052   {
    053     //Schlange konstruieren
    054     ColorRectangle cr;
    055     int x = 100;
    056     int y = 100;
    057     for (int i=0; i < NUMELEMENTS; ++i) {
    058       cr = new ColorRectangle();
    059       cr.x = x;
    060       cr.y = y;
    061       cr.width = SIZERECT;
    062       cr.height = SIZERECT;
    063       x += SIZERECT;
    064       cr.color = new Color(
    065         i*(256/NUMELEMENTS),
    066         0,
    067         240-i*(256/NUMELEMENTS)
    068       );
    069       snake.addElement(cr);
    070     }
    071
    072     //Vorzugsrichtung festlegen
    073     dx = -1;
    074     dy = -1;
    075
    076     //Schlange laufen lassen
    077     while (true) {
    078       repaint();
    079       try {
    080         Thread.sleep(SLEEP);
    081       } catch (InterruptedException e){
    082         //nichts
    083       }
    084       moveSnake();
    085     }
    086   }
    087
    088   public void moveSnake()
    089   {
    090     Dimension size = getSize();
    091     int sizex = size.width-getInsets().left-getInsets().right;
    092     int sizey = size.height-getInsets().top-getInsets().bottom;
    093     ColorRectangle cr = (ColorRectangle)snake.firstElement();
    094     boolean lBorder = false;
    095     int xalt, yalt;
    096     int xtmp, ytmp;
    097
    098     //Kopf der Schlange neu berechnen
    099     if (cr.x <= 1) {
    100       dx = 1;
    101       lBorder = true;
    102     }
    103     if (cr.x + cr.width >= sizex) {
    104       dx = -1;
    105       lBorder = true;
    106     }
    107     if (cr.y <= 1) {
    108       dy = 1;
    109       lBorder = true;
    110     }
    111     if (cr.y + cr.height >= sizey) {
    112       dy = -1;
    113       lBorder = true;
    114     }
    115     if (! lBorder) {
    116       if (rand(10) == 0) {
    117         if (rand(2) == 0) {
    118           switch (rand(5)) {
    119           case 0: case 1:
    120             dx = -1;
    121             break;
    122           case 2:
    123             dx = 0;
    124             break;
    125           case 3: case 4:
    126             dx = 1;
    127             break;
    128           }
    129         } else {
    130           switch (rand(5)) {
    131           case 0: case 1:
    132             dy = -1;
    133             break;
    134           case 2:
    135             dy = 0;
    136             break;
    137           case 3: case 4:
    138             dy = 1;
    139             break;
    140           }
    141         }
    142       }
    143     }
    144     xalt = cr.x + SIZERECT * dx;
    145     yalt = cr.y + SIZERECT * dy;
    146     //Rest der Schlange hinterherziehen
    147     Enumeration e = snake.elements();
    148     while (e.hasMoreElements()) {
    149       cr = (ColorRectangle)e.nextElement();
    150       xtmp = cr.x;
    151       ytmp = cr.y;
    152       cr.x = xalt;
    153       cr.y = yalt;
    154       xalt = xtmp;
    155       yalt = ytmp;
    156     }
    157   }
    158
    159   public void paint(Graphics g)
    160   {
    161     ColorRectangle cr;
    162     Enumeration e = snake.elements();
    163     int inleft    = getInsets().left;
    164     int intop     = getInsets().top;
    165     while (e.hasMoreElements()) {
    166       cr = (ColorRectangle)e.nextElement();
    167       g.setColor(cr.color);
    168       g.fillRect(cr.x+inleft,cr.y+intop,cr.width,cr.height);
    169     }
    170   }
    171
    172   private int rand(int limit)
    173   {
    174     return (int)(Math.random() * limit);
    175   }
    176 }
| Listing3409.java

Listing 34.9: Die animierte Schlange

|  | Die Schlange kann in einem beliebig kleinen oder großen Fenster laufen. Hier sind ein paar Beispiele für die Ausgabe des Programms, nachdem das Fenster in der Größe verändert wurde: |  |
Hinweis

Abbildung 34.8: Die animierte Schlange, Schnappschuß 1

Abbildung 34.9: Die animierte Schlange, Schnappschuß 2

Abbildung 34.10: Die animierte Schlange, Schnappschuß 3

### 34.2.4 Reduktion des Bildschirmflackerns

Alle bisher entwickelten Animationen zeigen während der Ausführung ein ausgeprägtes Flackern, das um so stärker ist, je später ein Bildanteil innerhalb eines Animationsschrittes angezeigt wird. Der Grund für dieses Flackern liegt darin, daß vor jedem Aufruf von `paint` zunächst das Fenster gelöscht wird und dadurch unmittelbar vor der Ausgabe des nächsten Bildes ganz kurz ein vollständig leerer Hintergrund erscheint.

Leider besteht die Lösung für dieses Problem nicht einfach darin, das Löschen zu unterdrücken. Bei einer animierten Bewegung beispielsweise ist es erforderlich, all die Bestandteile des vorigen Bildes zu löschen, die im aktuellen Bild nicht mehr oder an einer anderen Stelle angezeigt werden.

Auch wenn `paint` deshalb aufgerufen wird, weil ein bisher verdeckter Bildausschnitt wieder sichtbar wird, muß natürlich der entsprechende Bildausschnitt zunächst gelöscht werden, um die Bestandteile des anderen Fensters zu entfernen. Im Grunde ist es also eine ganz vernünftige Vorgehensweise, das Fenster vor jedem Aufruf von `paint` zu löschen.

Das Flackern kann nun auf unterschiedliche Weise unterdrückt werden. Die drei gebräuchlichsten Methoden sind folgende:

- den Bildschirm gar nicht zu löschen (was - wie zuvor erwähnt - problematisch ist)
- nur den wirklich benötigten Teil des Bildschirms zu löschen
- das Verfahren der Doppelpufferung anzuwenden

Jedes dieser Verfahren hat Vor- und Nachteile und kann in verschiedenen Situationen unterschiedlich gut angewendet werden. Wir werden sie in den folgenden Unterabschnitten kurz vorstellen und ein Beispiel für ihre Anwendung geben. Es gibt noch einige zusätzliche Möglichkeiten, das Flackern zu unterdrücken oder einzuschränken, wie beispielsweise das Clipping der Ausgabe auf den tatsächlich veränderten Bereich, aber darauf wollen wir hier nicht näher eingehen.

#### Bildschirm nicht löschen

Den Bildschirm überhaupt nicht zu löschen, um das Flackern zu unterdrücken, ist nur bei nicht bewegten Animationen möglich. Wir wollen uns als Beispiel für ein Programm, das hierfür geeignet ist, das folgende Lauflicht ansehen:

    001 /* Listing3410.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005
    006 public class Listing3410
    007 extends Frame
    008 implements Runnable
    009 {
    010   //Konstanten
    011   private static final int NUMLEDS  = 20;
    012   private static final int SLEEP    = 60;
    013   private static final int LEDSIZE  = 10;
    014   private static final Color ONCOLOR  = new Color(255,0,0);
    015   private static final Color OFFCOLOR = new Color(100,0,0);
    016
    017   //Instanzvariablen
    018   private Thread th;
    019   private int switched;
    020   private int dx;
    021
    022   public static void main(String[] args)
    023   {
    024     Listing3410 frame = new Listing3410();
    025     frame.setSize(270,150);
    026     frame.setVisible(true);
    027     frame.startAnimation();
    028   }
    029
    030   public Listing3410()
    031   {
    032     super("Leuchtdiodenkette");
    033     setBackground(Color.lightGray);
    034     addWindowListener(new WindowClosingAdapter(true));
    035   }
    036
    037   public void startAnimation()
    038   {
    039     th = new Thread(this);
    040     th.start();
    041   }
    042
    043   public void run()
    044   {
    045     switched = -1;
    046     dx = 1;
    047     while (true) {
    048       repaint();
    049       try {
    050         Thread.sleep(SLEEP);
    051       } catch (InterruptedException e){
    052         //nichts
    053       }
    054       switched += dx;
    055       if (switched < 0 || switched > NUMLEDS - 1) {
    056         dx = -dx;
    057         switched += 2*dx;
    058       }
    059     }
    060   }
    061
    062   public void paint(Graphics g)
    063   {
    064     for (int i = 0; i < NUMLEDS; ++i) {
    065       g.setColor(i == switched ? ONCOLOR : OFFCOLOR);
    066       g.fillOval(10+i*(LEDSIZE+2),80,LEDSIZE,LEDSIZE);
    067     }
    068   }
    069 }
| Listing3410.java

Listing 34.10: Bildschirmflackern reduzieren bei stehenden Animationen

|  | Das Programm zeigt eine Kette von 20 Leuchtdioden, die nacheinander an- und ausgeschaltet werden und dadurch ein Lauflicht simulieren, das zwischen linkem und rechtem Rand hin- und herläuft: |  |
Hinweis

Abbildung 34.11: Die Lauflicht-Animation

Wie kann nun aber das Löschen verhindert werden? Die Lösung basiert auf der Tatsache, daß bei einem Aufruf von `repaint` nicht gleich `paint`, sondern zunächst die Methode `update` aufgerufen wird. In der Standardversion der Klasse `Component` könnte `update` etwa so implementiert sein:

    001 public void update(Graphics g)
    002 {
    003   g.setColor(getBackground());
    004   g.fillRect(0, 0, width, height);
    005   g.setColor(getForeground());
    006   paint(g);
    007 }

Listing 34.11: Standard-Implementierung von update

Zunächst wird die aktuelle Hintergrundfarbe ausgewählt, um in dieser Farbe ein ausgefülltes Rechteck in der Größe des Bildschirms zu zeichnen. Erst nach diesem Löschvorgang wird die Vordergrundfarbe gesetzt und `paint` aufgerufen.

Da in Java alle Methodenaufrufe dynamisch gebunden werden, kann das Löschen dadurch verhindert werden, daß `update` durch eine eigene Version überlagert wird, die den Hintergrund unverändert läßt. Durch einfaches Hinzufügen der folgenden drei Zeilen kann das Flackern des Lauflichts vollkommen unterdrückt werden:

    001 /* update1.inc */
    002
    003 public void update(Graphics g)
    004 {
    005   paint(g);
    006 }
| update1.inc

Listing 34.12: Modifizierte Version von update

#### Nur den wirklich benötigten Teil des Bildschirms löschen

Wie schon erwähnt, kann auf das Löschen des Bildschirms nur dann komplett verzichtet werden, wenn die Animation keine Bewegung enthält. Ist sie dagegen bewegt, kann es sinnvoll sein, nur die Teile des Bildes zu löschen, die beim aktuellen Animationsschritt leer sind, im vorigen Schritt aber Grafikelemente enthielten.

Um welche Teile der Grafik es sich dabei handelt, ist natürlich von der Art der Animation abhängig. Zudem muß jeder Animationsschritt Informationen über den vorigen Schritt haben, um die richtigen Stellen löschen zu können. Ein Beispiel, bei dem diese Technik gut angewendet werden kann, ist die bunte Schlange aus dem Abschnitt »Animation mit Grafikprimitiven«.

Da die Schlange bei jedem Schritt einen neuen Kopf bekommt und alle anderen Elemente die Plätze ihres jeweiligen Vorgängers einnehmen, bleibt als einziges wirklich zu löschendes Element das letzte Element der Schlange aus dem vorherigen Animationsschritt übrig. Dessen Position könnte man sich bei jedem Schritt merken und im nächsten Schritt in der Hintergrundfarbe neu zeichnen.

Noch einfacher geht es, indem man an die Schlange einfach ein zusätzliches unsichtbares Element anhängt. Wird nämlich das letzte Element grundsätzlich in der Hintergrundfarbe dargestellt, hinterläßt es keine Spuren auf dem Bildschirm und braucht damit auch nicht explizit gelöscht zu werden! Wir brauchen also nur hinter die `for`-`next`-Schleife zur Konstruktion der Schlange ein weiteres, unsichtbares Element an den `snake`-`Vector` anzuhängen (in Listing 34.13 in den Zeilen 025 bis 031 eingefügt):

    001 /* Schlange2.inc */
    002
    003 public void run()
    004 {
    005   //Schlange konstruieren
    006   ColorRectangle cr;
    007   int x = 100;
    008   int y = 100;
    009   for (int i=0; i < NUMELEMENTS; ++i) {
    010     cr = new ColorRectangle();
    011     cr.x = x;
    012     cr.y = y;
    013     cr.width = SIZERECT;
    014     cr.height = SIZERECT;
    015     x += SIZERECT;
    016     cr.color = new Color(
    017       i*(256/NUMELEMENTS),
    018       0,
    019       240-i*(256/NUMELEMENTS)
    020     );
    021     snake.addElement(cr);
    022   }
    023
    024   //Löschelement anhängen
    025   cr = new ColorRectangle();
    026   cr.x = x;
    027   cr.y = y;
    028   cr.width = SIZERECT;
    029   cr.height = SIZERECT;
    030   cr.color = BGCOLOR;
    031   snake.addElement(cr);
    032
    033   //Vorzugsrichtung festlegen
    034   dx = -1;
    035   dy = -1;
    036
    037   //Schlange laufen lassen
    038   while (true) {
    039     repaint();
    040     try {
    041       Thread.sleep(SLEEP);
    042     } catch (InterruptedException e){
    043       //nichts
    044     }
    045     moveSnake();
    046   }
    047 }
| Schlange2.inc

Listing 34.13: Modifizierte Schlangenanimation

Wird nun zusätzlich die Methode `update` überlagert, wie es auch im vorigen Abschnitt getan wurde, läuft die Schlange vollkommen flackerfrei.

#### Doppelpufferung

Das Doppelpuffern bietet sich immer dann an, wenn die beiden vorigen Methoden versagen. Das kann beispielsweise dann der Fall sein, wenn es bei einer bewegten Animation zu aufwendig ist, nur den nicht mehr benötigten Teil der Bildschirmausgabe zu löschen, oder wenn der aktuelle Animationsschritt keine Informationen darüber besitzt, welcher Teil zu löschen ist.

Beim Doppelpuffern wird bei jedem Animationsschritt zunächst die gesamte Bildschirmausgabe in ein Offscreen-Image geschrieben. Erst wenn alle Ausgabeoperationen abgeschlossen sind, wird dieses Offscreen-Image auf die Fensteroberfläche kopiert. Im Detail sind dazu folgende Schritte erforderlich:

- Das Fensterobjekt beschaft sich durch Aufruf von `createImage` ein Offscreen-Image und speichert es in einer Instanzvariablen.
- Durch Aufruf von `getGraphics` wird ein Grafikkontext zu diesem Image beschafft.
- Alle Bildschirmausgaben (inklusive Löschen des Bildschirms) gehen zunächst auf den Offscreen-Grafikkontext.
- Wenn alle Ausgabeoperationen abgeschlossen sind, wird das Offscreen-Image mit `drawImage` in das Ausgabefenster kopiert.

Durch diese Vorgehensweise wird erreicht, daß das Bild komplett aufgebaut ist, bevor es angezeigt wird. Da beim anschließenden Kopieren die neuen Pixel direkt über die alten kopiert werden, erscheinen dem Betrachter nur die Teile des Bildes verändert, die auch tatsächlich geändert wurden. Ein Flackern, das entsteht, weil Flächen für einen kurzen Zeitraum gelöscht und dann wieder gefüllt werden, kann nicht mehr auftreten.

|  |  |  | Die Anwendung des Doppelpufferns ist nicht immer sinnvoll. Sollte eine der anderen Methoden mit vertretbarem Aufwand implementiert werden können, kann es sinnvoller sein, diese zu verwenden. Nachteilig sind vor allem der Speicherbedarf für die Konstruktion des Offscreen-Images und die Verzögerungen durch das doppelte Schreiben der Bilddaten. Hier muß im Einzelfall entschieden werden, welche Variante zum Einsatz kommen soll. In vielen Fällen allerdings können die genannten Nachteile vernachlässigt werden, und die Doppelpufferung ist ein probates Mittel, um das Bildschirmflackern zu verhindern. |  |
Tip

Das folgende Programm ist ein Beispiel für die Anwendung des Doppelpufferns bei der Ausgabe einer bewegten Animation. Wir wollen uns dafür die Aufabe stellen, eine große Scheibe über den Bildschirm laufen zu lassen, über deren Rand zwei stilisierte »Ameisen« mit unterschiedlicher Geschwindigkeit in entgegengesetzte Richtungen laufen.

Das folgende Programm löst diese Aufgabe. Dabei folgt die Animation unserem bekannten Architekturschema für bewegte Grafik und braucht hier nicht weiter erklärt zu werden. Um das Flackern zu verhindern, deklarieren wir zwei Instanzvariablen, `dbImage` und `dbGraphics`:

    private Image dbImage;
    private Graphics dbGraphics;

|  | Glücklicherweise können die zum Doppelpuffern erforderlichen Schritte gekapselt werden, wenn man die Methode `update` geeignet überlagert:
    001 /* update2.inc */
    002
    003 public void update(Graphics g)
    004 {
    005   //Double-Buffer initialisieren
    006   if (dbImage == null) {
    007     dbImage = createImage(
    008      this.getSize().width,
    009      this.getSize().height
    010     );
    011     dbGraphics = dbImage.getGraphics();
    012   }
    013   //Hintergrund löschen
    014   dbGraphics.setColor(getBackground());
    015   dbGraphics.fillRect(
    016     0,
    017     0,
    018     this.getSize().width,
    019     this.getSize().height
    020   );
    021   //Vordergrund zeichnen
    022   dbGraphics.setColor(getForeground());
    023   paint(dbGraphics);
    024   //Offscreen anzeigen
    025   g.drawImage(dbImage,0,0,this);
    026 }
| update2.inc
Listing 34.14: update-Methode mit Doppelpufferung |  |
Hinweis

Falls nicht schon geschehen, werden hier zunächst die beiden Variablen `dbImage` und `dbGraphics` initialisiert. Anschließend wird der Hintergrund gelöscht, wie es auch in der Standardversion von `update` der Fall ist. Im Gegensatz zu dieser erfolgt das Löschen aber auf dem Offscreen-Image und ist somit für den Anwender nicht zu sehen. Nun wird `paint` aufgerufen und bekommt anstelle des normalen den Offscreen-Grafikkontext übergeben. Ohne selbst etwas davon zu wissen, sendet `paint` damit alle seine Grafikbefehle auf das Offscreen-Image. Nachdem `paint` beendet wurde, wird durch Aufruf von `drawImage` das Offscreen-Image auf dem Bildschirm angezeigt.

Hier ist der komplette Quellcode des Programms:

    001 /* Listing3415.java */
    002
    003 import java.awt.*;
    004 import java.awt.event.*;
    005
    006 public class Listing3415
    007 extends Frame
    008 implements Runnable
    009 {
    010   private Thread th;
    011   private int actx;
    012   private int dx;
    013   private int actarc1;
    014   private int actarc2;
    015   private Image dbImage;
    016   private Graphics dbGraphics;
    017
    018   public static void main(String[] args)
    019   {
    020     Listing3415 frame = new Listing3415();
    021     frame.setSize(210,170);
    022     frame.setVisible(true);
    023     frame.startAnimation();
    024   }
    025
    026   public Listing3415()
    027   {
    028     super("Ameisenanimation");
    029     addWindowListener(new WindowClosingAdapter(true));
    030   }
    031
    032   public void startAnimation()
    033   {
    034     Thread th = new Thread(this);
    035     th.start();
    036   }
    037
    038   public void run()
    039   {
    040     actx = 0;
    041     dx = 1;
    042     actarc1 = 0;
    043     actarc2 = 0;
    044     while (true) {
    045       repaint();
    046       actx += dx;
    047       if (actx < 0 || actx > 100) {
    048         dx = -dx;
    049         actx += 2*dx;
    050       }
    051       actarc1 = (actarc1 + 1) % 360;
    052       actarc2 = (actarc2 + 2) % 360;
    053       try {
    054         Thread.sleep(40);
    055       } catch (InterruptedException e) {
    056         //nichts
    057       }
    058     }
    059   }
    060
    061   public void update(Graphics g)
    062   {
    063     //Double-Buffer initialisieren
    064     if (dbImage == null) {
    065       dbImage = createImage(
    066         this.getSize().width,
    067         this.getSize().height
    068       );
    069       dbGraphics = dbImage.getGraphics();
    070     }
    071     //Hintergrund löschen
    072     dbGraphics.setColor(getBackground());
    073     dbGraphics.fillRect(
    074       0,
    075       0,
    076       this.getSize().width,
    077       this.getSize().height
    078     );
    079     //Vordergrund zeichnen
    080     dbGraphics.setColor(getForeground());
    081     paint(dbGraphics);
    082     //Offscreen anzeigen
    083     g.drawImage(dbImage,0,0,this);
    084   }
    085
    086   public void paint(Graphics g)
    087   {
    088     int xoffs = getInsets().left;
    089     int yoffs = getInsets().top;
    090     g.setColor(Color.lightGray);
    091     g.fillOval(xoffs+actx,yoffs+20,100,100);
    092     g.setColor(Color.red);
    093     g.drawArc(xoffs+actx,yoffs+20,100,100,actarc1,10);
    094     g.drawArc(xoffs+actx-1,yoffs+19,102,102,actarc1,10);
    095     g.setColor(Color.blue);
    096     g.drawArc(xoffs+actx,yoffs+20,100,100,360-actarc2,10);
    097     g.drawArc(xoffs+actx-1,yoffs+19,102,102,360-actarc2,10);
    098   }
    099 }
| Listing3415.java

Listing 34.15: Animation mit Doppelpufferung

Ein Schnappschuß des laufenden Programms sieht so aus (die beiden »Ameisen« sind in der Abbildung etwas schwer zu erkennen, im laufenden Programm sieht man sie besser):

Abbildung 34.12: Eine Animation mit Doppelpufferung

Durch die Kapselung des Doppelpufferns können Programme sogar nachträglich flackerfrei gemacht werden, ohne daß in den eigentlichen Ausgaberoutinen irgend etwas geändert werden müßte. Man könnte beispielsweise aus `Frame` eine neue Klasse `DoubleBufferFrame` ableiten, die die beiden privaten Membervariablen `dbImage` und `dbGraphics` besitzt und `update` in der beschriebenen Weise implementiert. Alle Klassen, die dann von `DoubleBufferFrame` anstelle von `Frame` abgeleitet werden, unterstützen das Doppelpuffern ihrer Grafikausgaben automatisch.

Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de