Ostatnia myśl przed zapadnięciem w sen: 'Shutdown completed'
Część 4.3. Animacja

Zegar analogowy

Poznałeś już metody rysowania. Kolejnym krokiem będzie wprowadzenie do apletu elementów animacji. Zaczniemy od napisania apletu wyświetlającego zegar analogowy wyświetlający aktualny czas systemowy. Zastosujemy najprostszą metodę animacji: zmazywanie elementu przez wyświetlenie go w kolorze tła, a następnie wyrysowanie go w nowej pozycji. Oto kod tego apletu:

import java.util.*;
import java.awt.*;
import java.applet.*;

public class ZegarAnalogowy extends Applet implements Runnable
{ int lastxs=0, lastys=0, lastxm=0, lastym=0, lastxh=0, lastyh=0;

  public void start()
  { while (true)
    { repaint();
	  for (int i=0 ; i<=10000 ; i++) { }
    }
  }

  // główna część apletu rysująca zegar
  public void paint(Graphics g)
  { int xh, yh, xm, ym, xs, ys, s, m, h;
    Date dat = new Date();

    // pobranie aktualnego czasu systemowego
    s = dat.getSeconds();
    m = dat.getMinutes();
    h = dat.getHours();
  
    // rysowanie tła apletu
    g.setColor(new Color(255,255,221));
    g.fillRect(0, 0, 210, 210);
    g.setColor(new Color(0,0,153));
     
    // wyliczenie współrzędnych końca każdej wskazówki zegara
    // (początek jest zawsze w środku zegara)
    xs = (int)(Math.cos(s * 3.14f/30 - 3.14f/2) * 97 + 105);
    ys = (int)(Math.sin(s * 3.14f/30 - 3.14f/2) * 97 + 105);
    xm = (int)(Math.cos(m * 3.14f/30 - 3.14f/2) * 90 + 105);
    ym = (int)(Math.sin(m * 3.14f/30 - 3.14f/2) * 90 + 105);
    xh = (int)(Math.cos((h*30 + m/2) * 3.14f/180 - 3.14f/2) * 75 + 105);
    yh = (int)(Math.sin((h*30 + m/2) * 3.14f/180 - 3.14f/2) * 75 + 105);
  
    // rysowanie tarczy zegara i cyfr
    g.setFont(new Font("Arial", Font.BOLD, 16));
    g.setColor(Color.blue);
    g.drawOval(5, 5, 200, 200);
    g.setColor(Color.white);
    g.fillOval(7, 7, 196, 196);
    g.setColor(Color.darkGray);
    g.drawString("9", 200, 108); 
    g.drawString("3", 194, 108);
    g.drawString("12",96,  25);
    g.drawString("6", 102, 203);

    // "ścieranie" wskazówek, jeżeli zmieniły się współrzędne
    g.setColor(Color.white);
    if (xs != lastxs || ys != lastys)
    { g.drawLine(105, 105, lastxs, lastys);
    }
    if (xm != lastxm || ym != lastym)
    { g.drawLine(105, 104, lastxm, lastym);
      g.drawLine(104, 105, lastxm, lastym); 
    }
    if (xh != lastxh || yh != lastyh)
    { g.drawLine(105, 104, lastxh, lastyh);
      g.drawLine(104, 105, lastxh, lastyh);
    }

    // rysowanie wskazówek zegara
    g.setColor(Color.red);
    g.drawLine(105, 105, xs, ys);
    g.setColor(Color.blue);
    g.drawLine(105, 104, xm, ym);
    g.drawLine(104, 105, xm, ym);
    g.drawLine(105, 104, xh, yh);
    g.drawLine(104, 105, xh, yh);

    // zapamiętanie aktualnych współrzędnych wskazówek
    lastxs=xs;
    lastys=ys;
    lastxm=xm;
    lastym=ym;
    lastxh=xh;
    lastyh=yh;
  }
}

Przeanalizujmy działanie apletu krok po kroku:

  • po załadowaniu do pamięci apletu tworzone są zmienne klasy ZegarAnalogowy potrzebne do przechowywawnia pozycji wskazówek;
     
  • następnie wykonywana jest metoda start( ) (zobacz metody specjalne); nasza metoda start( ) stanowi - jak widać - jedną tylko instrukcję: nieskończoną (warunek true) pętlę while); wewnątrz tej pętli wykonujemy kolejno dwie czynności:
     
    • odrysowujemy aplet metodą repaint (), która zawsze wywoła metodę paint ( );
       
    • wykonujemy pętlę for, która ma "uśpić" aplet na jakiś czas.
       
  • następnie wykonywana jest metoda paint( ) (zobacz metody specjalne), która wykonuje kolejno:
     
    • pobiera aktualny czas systemowy;
       
    • rysuje tło apletu;
       
    • wylicza współrzędne końców wskazówek zegara: godzinowej, minutowej i sekundowej (początek każdej wskazówki znajduje siś jest w środku tarczy zegara);
       
    • rysuje tarczę zegara i wyświetla na niej cyfry;
       
    • wyświetla - tylko jeżeli zmieniła się któraś ze współrzędnych - stare wskazówki w kolorze tła, co powoduje ich ścieranie;
       
    • wyświetla wszystkie trzy wskazówki zegara;
       
    • zapamiętuje aktualnych współrzędne wskazówek potrzebne do kolejnego cyklu wyświetlania.
       
Spróbuj teraz samodzielnie napisać aplet, który będzie wyświetlał koło, kwadrat i trójkąt w losowo wybranych miejscach. Figury powinny pojawiać siś i znikać w różnych miejscach apletu, ale zawsze ma być widoczna tylko jedna z nich

Animacja tekstu

Kolejny nasz aplet będzie wyświetlał napis pływający wewnątrz utworzonego okna i "odbijał" się od krawędzi. Zastosujemy na początek poznaną już z poprzedniego przykładu metodę animacji: zmazywanie aktualnie wyświetlanego tekstu i wyrysowanie go w nowej pozycji. Oto kod tego apletu:
import java.awt.*;
import java.applet.*;

public class AniTekst1 extends Applet
{ int fontSize = 10, x=0, y=299, dx=1, dy=-1, wys, szer;
  // zmienna do określenia czy aplet ma być przerysowany, czy nie
  boolean zatrzymaj = true;
  String napis = new String ("TEKST");
  Font font = new Font("Arial", Font.BOLD, 24);
  FontMetrics fm;

  // każde wejście na stronę (do okna) uruchamia odrysowywanie apletu
  public void start ()
  { zatrzymaj = false;
    repaint();
  }

  // każde opuszczenie strony (okna) zatrzymuje odrysowywanie apletu
  public void stop ()
  { zatrzymaj = true;
  }

  // rysowanie zawartości apletu
  public void paint(Graphics g)
  { // możesz w warunku pętli while zamienić zmienna zatrzymaj na stałą true
    // otrzymany efekt będzie raczej mało zadawalający: odrysowywanie "oszaleje"
    while (! zatrzymaj)
    { g.setColor (Color.white);		// rysowanie białego tła apletu
      g.fillRect (0, 0, 299, 299);

      // zamazywanie istniejącego napisu
      g.setFont  (font);
      g.drawString (napis, 5, 75);

      // rysowanie ramki wokół apletu
      g.setColor (Color.red);
      g.drawRect (0, 0, 299, 299);

      // pobranie wysokości i szerokości wyświetlanego tekstu
      fm   = g.getFontMetrics();
      wys  = fm.getHeight();
      szer = fm.stringWidth(napis);

      // zmiana współrzędnych i ustalenie nowej pozycji tekstu
      if (x <= 0  &&  dx == -1)         dx=1;
      if (x + szer >= 299  &&  dx == 1) dx=-1;
      if (y <= wys  &&  dy == -1)       dy=1;
      if (y >= 299  &&  dy == 1)        dy=-1;
      x += dx;
      y += dy;

      // wyświetlenie tekstu
      g.drawString (napis, x, y);

      // pętla opóźniająca kolejne wyświetlanie
      for (int i = 0; i < 100000; i++);
    }
  }
}

Aplet działa w zasadzie poprawnie. Ma jednak kilka wad:

  • denerwujący jest efekt migotania tekstu będący skutkiem zmazywania i rysowania tekst w krótkich odstępach czasu;
     
  • działanie pętli opóźniającej kolejne odrysowanie zależy od szybkości komputera;
     
  • każdorazowe rysowanie tła znacznie wydłuża odrysowywanie apletu.
     
Zrobimy kolejną wersję tego samego apletu próbując wyeliminować powyższe mankamenty. W tym celu będziemy musieli wprowadzić kilka nowych, dotychczas nie używanych przez nas elementów:
  • najprościej wyeliminować konieczność każdorazowego rysowania tła: metoda setBackground pozwoli nam ustawić tło apletu podczas jego inicjacji bez konieczności ciągłego odświeżania;
     
  • efekt migotania wyeliminujemy poprzez buforowanie wyświetlanej grafik: kolejny obraz będzie tworzony w pamięci; nie będzie więc efektu zmazywania i rysowania; zamiast tego jeden obraz zostanie jednorazowo zastąpiony innym;
     
  • zmienimy też pętlę opóźniającą: aplet uruchomimy jako wątek - klasa Thread, której jednak tutaj szerzej nie będziemy omawiać, gdyż wątki w programach Javy wykraczają poza zakres tego kursu (najogólniej wątki stanowią niezależne procesy uruchomione wewnątrz jednego programu); skorzystamy z metod uruchamiających, usypiających i zatrzymujących wątek; istotnymi dla nas informacjami jest to, że użycie a programie wątków wymaga zaimplementowania w niej klasy Runnable oraz, że każdy wątek zaczyna działanie od metody run().
     
Uwzględnijąc wszystkie powyższe uwagi napiszemy nową wersję naszego apletu (później przeprowadzimy dokładniejszą analizę jego działania):
import java.awt.*;
import java.applet.*;

public class AniTekst2 extends Applet implements Runnable
{ int fontSize = 10, x = 0, y = 299, dx = 1, dy = -1, wys, szer;
  String napis = new String ("TEKST");
  Font font = new Font("Arial", Font.BOLD, 24);
  FontMetrics fm;
  Thread thread;        // klasa obsługi wątków programu
  Image obraz;          // obiekt do przechowywania obrazu w pamięci
  Graphics g1, g2;      // obiekty graficzne

  // inicjacje apletu wykonywana tylko raz podczas jego ładowania do pamięci
  public void init()
  { setBackground(Color.white);
    obraz = createImage (300, 300);
    g2 = obraz.getGraphics();
    g2.setFont (font);
    g2.setColor (Color.red);
    fm   = g2.getFontMetrics();
    wys  = fm.getHeight();
    szer = fm.stringWidth(napis);
    g1 = getGraphics();
  }

  // utworzenie i uruchomienie wątku przy każdym uaktywnieniu apletu
  public void start()
  { thread = new Thread (this);
    thread.start();
  }

  // metoda, od której zaczyna się wykonanie uruchomionego wątku
  public void run()
  { while (true)
    { if (x<=0 && dx==-1)       dx=1;
      if (x+szer>=299 && dx==1) dx=-1;
      if (y<=wys && dy==-1)     dy=1;
      if (y>=299 && dy==1)      dy=-1;
      x += dx;
      y += dy;

      g2.clearRect(0, 0, 299, 299);
      g2.drawRect (0, 0, 299, 299);
      g2.drawString (napis, x, y);

      g1.drawImage (obraz, 0, 0, this);

      try
      {  Thread.sleep(10);  }
      catch (InterruptedException e) {  }
    }
  }
}

Przeanalizujmy teraz działanie tego apletu krok po kroku:

  • po załadowaniu do pamięci apletu tworzone są wszystkie zmienne klasy AniTekst2 oraz wykonywana jest metod init( ) (zobacz metody specjalne); w metodzie tej wykonywane są kolejno:
     
    • ustawienie tła apletu metodą setBackground, co wyeliminuje konieczność jego nieustannego odrysowywania;
       
    • utworzenie w pamięci obiektu obraz o wymiarach 300 na 300 pikseli;
       
    • kolejny wiersz tworzy referencję do graficznego kontekstu obiektu obraz, co umożliwia nam rysowanie w pamięci tak, jakby odbywało się to na ekranie;
       
    • kolejne instrukcje ustawiają czcionkę i kolor rysowania dla utworzonej referencji obrazu oraz zapamięrują wysokość i szerokość naszego napisu dla ustalonej czcionki;
       
    • ostania instrukcją metody jest utworzenie referencji go grafiki naszego apletu;
       
  • po załadowaniu apletu i wykonaniu metody init( ) oraz zawsze po uaktywnieniu okna apletu wykonywana jest metoda start( ) (zobacz metody specjalne); nasza metoda start( ) tworzy nowy wątek i go uruchamia, co praktycznie oznacza wywołanie metody run( );
     
  • cała praca apletu odbywa się w metodzie run( ), która stanowi - jak widać - jedną tylko instrukcję: nieskończoną (warunek true) pętlę while; wewnątrz tej pętli wykonujemy kolejno następujące działanie:
     
    • ustalamy współrzędne, gdzie ma być wyrysowany napis po zmianie pozycji;
       
    • czyścimy cały obszar rysunku;
       
    • rysujemy ramkę wokół apletu;
       
    • wyświetlamy napis we wcześniej ustalonej pozycji;
       
    • wszystkie te czynności odbywają się na obrazie przechowywanym w pamięci, nie widać ich żadnego efektu na ekranie komputera; dopiero po całkowitym skompletowaniu obrazu wyświetlamy go na monitorze metodą drawImage, która w kolejnych parametrach podaje referencję do wyświetlanego obiektu, współrzędne X, Y, od których obiekt ma być wyświetlony oraz referencję do obiektu wyświetlającego (słowo this w tym przypadku oznacza wyświetlenie w aplecie);
       
    • na zakończenie działanie pętli "usypiamy" wątek metodą sleep( ) na 10 milisekund, po czym pętla zacznie wykonywać się od nowa.
       
Tak napisany aplet będzie już pozbawiony wad swojej poprzedniej wersji.
Napisz samodzielnie aplet, który będzie animował tekst przewijany tylko w poziomie. Tekst powinien przewijać się z prawej strony w lewo i po całkowitym zniknięciu za lewą krawędzią pojawić się ponownie z prawej strony.

Aplet z parametrami

Poprzedni aplet może stanowić efektowne uatrakcyjnienie strony WWW. Z tego punktu widzenia ma jednak bardzo istotną wadę: jeżeli chcielibyśmy zmienić w nim tekst, kolory, czcionki, czy choćby sam rozmiar, to musimy tworzyć kolejną wersję apletu i ponownie go kompilować, aby użyć na stronie. Jest to bardzo niewygodne, a dodatkowo mnoży ilość tworzonych apletów wykonujących w zasadzie takie samo zadanie.

Istnieje metoda pozwalająca wykorzystać ten sam aplet w różnych sytuacjach: musimy nasz aplet sparametryzować. Oznacza to, że samo działanie będzie zawsze takie samo, ale podczas uruchamiania apletu możemy każdorazowo przekazywać mu inne informacje o tym co i w jaki sposób ma wyświetlać. Osiągnięcie tego wymaga:

  • przekazania ze strony www informacji o tym, co i jak aplet ma wykonywać (służy do tego znacznik <param ...>);
     
  • odczytania i przetworzenia tych informacji przez aplet.
     
Część wykonywalna apletu (metoda run()) w zasadzie nie ulega zmianie: wykonuje to samo i w takiej samej kolejności. Jednak parametry instrukcji i metod nie mogą być - jak dotychczas - stałymi. Wszystkie wartości (wyświetlany tekst, czcionki, kolory, rozmiar apletu) musimy zapamiętać w zmiennych i używać tych zmiennych zamiast dotychczasowych stałych. Wartości tych zmiennych pobieramy właśnie z parametrów przekazanych ze strony www. Służy to tego metoda getParameter(nazwaParametru), która zwraca wartość parametru o podanej nazwie lub wartość null, jeżeli parametr o podanej nazwie nie został podany na stronie www. Pobrana wartość parametru jest zawsze typu String. Dlatego należy pamiętać o konwersji pobranego parametru na poprawny typ zmiennej, do której go przypisujemy.

Z użyciem parametrów wiążą się dwa problemu. Po pierwsze - użytkownik może w ogóle nie podać parametru. Uwzględniając tą sytuację należy wszystkim zmiennym korzystającym z parametrów nadać wartość domyślną, co zapobiegnie ewentualnym błędom. Po drugie - parametry mogą zawierać błędne wartości (np. tekst dla zmiennej numerycznej). Problem jest poważny i wymaga rozbudowanej kontroli. W naszym przykładzie go nie rozwiążemy ze względu na brak miejsca, ale pisząc aplet należy o nim pamiętać.

Pora już przerobić nasz aplet tak, aby można było nim sterować ze strony www bez konieczności przerabiania samego apletu i jego ponownej kompilacji, gdy chcemy coś w nim zmienić (oczywiście poza samym działaniem). Oto kod tego apletu:

import java.awt.*;
import java.applet.*;

public class AniTekst4 extends Applet implements Runnable
{ int x, y, dx = 1, dy = -1, wys, szer;
  FontMetrics fm;
  Thread thread;
  Image obraz;
  Graphics g1, g2;

  // zmienne do zapamiętania rozmiarów apletu:
  int Height;
  int Width;

  // zmienne do przetworzenia parametrów:
  String  strParam;                   // zmienna robocza

  String napis = "TEKST";             // wyświetlany tekst

  Color BgColor     = Color.white;    // kolor tła
  Color FgColor     = Color.red;      // kolor tekstu
  Color BorderColor = Color.red;      // kolor ramki

  String FontFamily = "Arial";        // czcionka
  int FontSize      = 24;             // wielkość czcionki w pikselach
  int FontStyle     = Font.PLAIN;     // krój czcionki

  Font font;

  public void init()
  { /********** POCZˇTEK USTAWIANIA PARAMETRÓW **********/
    // wyświetlany tekstu
    if (this.getParameter("Text") != null)
    { napis = this.getParameter("Text"); }

    // kolor tekstu
    if (this.getParameter("FgColor") != null)
    { strParam = this.getParameter("FgColor");
      FgColor = new Color(Integer.parseInt(strParam, 16));
    }

    // kolor tła apletu
    if (this.getParameter("BgColor") != null)
    { strParam = this.getParameter("BgColor");
      BgColor = new Color(Integer.parseInt(strParam, 16));
    }

    // kolor ramki apletu
    if (this.getParameter("BorderColor") != null)
    { strParam = this.getParameter("BorderColor");
      BorderColor = new Color(Integer.parseInt(strParam, 16));
    }

    // krój czcionki
    if (this.getParameter("FontFamily") != null)
    { FontFamily = this.getParameter("FontFamily"); }

    // wielkość czcionki
    if (this.getParameter("FontSize") != null)
    { strParam = this.getParameter("FontSize");
      FontSize = Integer.parseInt(strParam);
    }

    // styl czcionki
    if (this.getParameter("FontStyle") != null)
    { strParam = this.getParameter("FontStyle");
      strParam = strParam.toLowerCase();
      if (strParam.equals("bold"))   { FontStyle = Font.BOLD; }
      if (strParam.equals("italic")) { FontStyle = Font.ITALIC; }
      if (strParam.equals("plain"))  { FontStyle = Font.PLAIN; }
    } 
    font = new Font(FontFamily, FontStyle, FontSize);
    /********** KONIEC USTAWIANIA PARAMETRÓW **********/

    Height = getSize().height;
    Width  = getSize().width;
    setBackground(BgColor);
    obraz = createImage (Width, Height);
    g2 = obraz.getGraphics();
    g2.setFont (font);
    fm = g2.getFontMetrics();
    wys  = fm.getHeight();
    szer = fm.stringWidth(napis);

    g1 = getGraphics();
    x = 0;
    y = Height-1;
  }

  public void start()
  { thread = new Thread (this);
    thread.start();
  }

  // teraz metoda run(), ale całkowicie sparametryzowana
  public void run()
  { while (true)
    { if (x<=0 && dx==-1)           dx=1;
      if (x+szer>=Width-1 && dx==1) dx=-1;
      if (y<=wys && dy==-1)         dy=1;
      if (y>=Height-1 && dy==1)     dy=-1;
      x += dx;
      y += dy;

      g2.clearRect(0, 0, Width-1, Height-1);
      g2.setColor (BorderColor);
      g2.drawRect (0, 0, Width-1, Height-1);
      g2.setColor (FgColor);
      g2.drawString (napis, x, y);
      try
      { Thread.sleep(10); }
      catch (InterruptedException e){ }
      g1.drawImage (obraz, 0, 0, this);
    }
  }
}

Spróbujmy teraz uruchomić nasz aplet zmieniając mu parametry. Poniżej masz przykładowy zapis uruchomienia tego apletu ze strony www. Możesz go uruchomić bez podawania parametrów lub podając własne parametry. Pamiętaj jednak, że błędne wartości np. kolorów lub rozmiarów apletu mogą dać w efekcie nieprzewidziany efekt. Zwróć przy tym uwagę, że wielkość liter w nazwie parametru nie ma znaczenia.


	<APPLET CODE="AniTekst4.class" WIDTH="" HEIGHT="">

		<PARAM NAME="Text"  VALUE="">
		
		<PARAM NAME="fontfamily"  VALUE="">
		
		<PARAM NAME="FONTSIZE"    VALUE="">
		
		<PARAM NAME="FontStyle"   VALUE="">
		
		<!-- kolory podawaj szesnastkowo jako RRGGBB-->
		
		<PARAM NAME="FgColor"      VALUE="">
		
		<PARAM NAME="BgColor"      VALUE="">
		
		<PARAM NAME="BorderColor"  VALUE="">
		
	</APPLET>

Animacja grafiki

Na zakończenie animacji aplet najbardziej chyba efektowny, choć dość prosty do napisania. Mając serię grafik (zdjęć, obrazków) możemy stworzyć animację przez wyświetlanie kolejnych obrazów (tzw. animacja poklatkowa). Wykonujemy to przez wczytanie do tablicy wszystkich obrazów, a następnie wyświetlanie ich w określonej kolejności. Poniższy aplet jest tego przykładem. Po przeanalizowaniu poprzednich przykładów nie powinieneś mieć problemów ze zrozumieniem działania tego aplet, dlatego też nie ma w nim żadnych komentarzy. Spróbuj przeanalizować go samodzielnie. Zwróć uwagę na metodę załadowania wszystkich obrazków przed rozpoczęciem wyświetlania animacji.

import java.applet.*;
import java.awt.*;

public class Animacja extends Applet implements Runnable
{ Graphics g1, g2;
  Image imgArray[]=new Image[10];
  Image img;
  boolean start = true;

  public void init()
  { g1  = getGraphics();
    img = createImage(540, 240);
    g2  = img.getGraphics();
    for (int i = 0; i < 10; i++)
         imgArray[i] = getImage(getDocumentBase(), "T" + (i+1) + ".gif");
    setBackground(new Color(255, 255, 226));
  }

  public void start()
  { Thread thread = new Thread (this);
    thread.start();
  }

  public void run()
  { int n, i = 0, j = 1;
    Image imgR;

    while (true)
    { if (start)
      { for (n = 0; n < 10; n++)
        { imgR = getImage(getDocumentBase(), "T" + (n+1) + ".gif");
          imgArray[n] = imgR;
          g2.setColor(Color.red);
          g2.drawString("Ładowanie grafiki...", 200, 90);
          g2.drawRect(140, 130, 260, 30);
          g2.setColor(Color.blue);
          g2.fillRect(141, 131, 26*n, 28);
          g1.drawImage (img, 0, 0, this);
        }
        start = false;
      }
      else
      { g2.clearRect(0, 0, 540, 240);
        g2.setColor(new Color(0, 0, 200));
        for (n = 0 ; n < 4 ; n++)
              g2.drawRect(n ,n, getSize().width-2*n-1, getSize().height-2*n-1);
        g2.drawImage (imgArray[i], 40, 40, this);
        g1.drawImage (img, 0, 0, this);
        try  { Thread.sleep(100); }
        catch(InterruptedException e) { }
        i += j;
        if (i == 9)       j = -1;
        else if (i == 0)  j = 1;
      }
    }
  }
}
« wstecz   dalej »