Ilkka Koivistoinen 13.02.2002

Edellinen

Seuraava

10.5 Kuvat, animaatiot ja ääni appletissa

Animaatiot perustuvat kuviin tai grafiikka -olioihin, jotka seuraavat toisiaa siten, että katsojasta kohde näyttää liikkuvan. Haluttaessa applettiin useita erilaisia toimintoja yhtäaikaa, laitetaan eri toiminnot eri ohjelmasäikeisiin (threads). Jokainen säie saa järjestelmäresursseja samassa suhteessa, joten yhtäaikaa voidaan lukea tiedostoa, piirtää näyttöä ja käyttää kaiuttimia.  Säikeitä voidaan luoda ja poistaa milloin halutaan sekä säikeet voivat kommunikoida keskenään. Edelleen eri säikeissä voidaan suorittaa yhteistä koodia ja niiden suoritusjärjestykseen voidaan vaikuttaa (synchronized)

10.5.1 Ikkunan päivitys

Ikkunan sisältöä muutetaan samalla tavalla, mitä 2D-piirto -olioiden yhteydessä opittiin. Ensin ikkunan sisältöä muutetaan ja sitten ikkuna piirretään. Uudelleenpiirtäminen tehdään repaint() -metodilla. Näitä kahta vaihetta toistamalla ikkunan sisältöä saadaan muutettua siten, että katsojalle syntyy liikkeen vaikutelma. Kuten aikaisemmin on opittu, paint() -metodia kutsutaan aina, kun ikkuna pitää syystä tai toisesta piirtää uudelleen. Sitä voidaan ohjelmasta kutsua uudelleen repaint() -metodilla. Kun repaint() -käsky annetaan, paint() -metodi aktivoituu välittömästi. Jos kuitenkin koneen liiallisen kuorman tai liian tiheään tulevien repain() -käskyjen vuoksi paint() -metodikutsuja rupeaa kertymään liikaa jonoon, java hyppää muutaman kuvaruutupäivityksen yli. Tämä saattaa joskua aiheuttaa kuvan nykimistä.Toisaalta tällä tavalla ruuhkasta selvitään ilman, että koko järjestelmä jumiutuisi.

paint() -metodia ei pitäisi käyttää kuvaruudun sisällön muuttamiseen. paint() on vastuussa ainoastaan nykyisen kuvan piirtämisestä, milloin se on tarpeellista. Muista, että sitä kutsutaan muulloinkin kuin pelkästään ohjelmoijan pyynnöstä ohjelman suorituksen aikana. Itse kuvan muodostaminen ja muokkaaminen pitää tehdä toisaalla ohjelmassa ja lopputulos piirretään ainoastaan kutsumalla välilliseti paint() -metodia repaint() -kutsussa. (Siis edellä opittu tapa pitäisi nyt ylikirjoittaa uudella tavalla tehdä appletteja.)

Animaatioita varten kannattaa start() ja stop() -metodit ylikirjoittaa. Niiden avulla voidaan hallita eri säikeiden aloittamista ja pysäyttämistä tarpeen mukaan.

10.5.2 Animaatioiden hallinta säikeiden avulla

Säikeellä tarkoitetaan sellaista ohjelman osaa joka toimii omillaan sillä aikaa kun muu osa ohjelmaa tekee samaan aikaan jotain muuta

wpe15.jpg (16816 bytes)

Sivusäikeitä voi luoda kuinka paljon tahansa ja milloin haluaa. Siis myös sivusäikeissä voi luoda lisää säikeitä.

Säikeitä käytetään sellaisille ohjelmanosille, jotka vievät runsaasti ja jatkuvasti prosessoriaikaa. Tälläinen tehtävä on tyypillisesti kokoajan pyörivä animaatio tai laskutehtävä (esimerkiksi ison taulukon lajittelu tai funktion nollakohdan hakeminen)

Säikeistys tehdään seuraavasti:

luodaan uusi luokka periyttämällä appletti java.applet.Applett -luokasta, joka toteuttaa  runnable -ohjelmarajapinnan
luodaan säikeeksi Thread -olio
ylikirjoitetaan start() -metodi, jossa säie alustetaan ja käynnistetään
ylikirjoitetaan stop() -metodi asettamaan säikeen osoitteeksi null, jolloin säie ei enää tee mitään
ylikirjoitetaan runnable -rajapinnan abstrakti run() -metodi (joka on siis pakko kirjoittaa), jolla käynnistetään appletti

Luodaan luokka digitaaliKello, joka näyttää ajan kahdessa kellossa siten, että ylemmän kellon aika muuttuu sekunnin välein ja alemman viiden sekunnin välein. Tulos näyttää tältä (jos kello ei käynnisty, päivitä näyttö)

Luodaan appletin ohjelmakoodi. aloitetaan luokasta digitaaliKello.

public class digitaaliKello extends java.applet.Applet implements Runnable
  // appletti, joka näyttää kellonajan kahdessa kellossa
{

Luodaan säikeet runner ja runner1. Säie runner vastaa ylemmästä kellosta ja runner1 alemmasta kellosta. Lisäksi luodaan muut tarvittavatr oliot

Font theFont = new Font("TimesRoman",Font.BOLD,24);
Date theDate; 
Thread runner, runner1;
String aika,aika1;

Ylikirjoitetaan start() -metodi, jossa luodaan uudet säikeet. Huomaa, että start() -metodikin ajetaan aina tietyissä tilanteissa uudestaan, kuten aiemmin on ollut puhetta. Siksi luodaan uusi säi vasta sen jälkeen, kun on testattu, että säiettä todella ei ole olemassa. this -muuttujalla viitataan käynnistyneeseen luokan digitaaliKello olioon, jonka säikeitä  runner ja runner1 ovat

public void start() {
  if (runner==null) {
    runner = new Thread(this);
    runner.start();
  }
  if (runner1==null) {
    runner1 = new Thread(this);
    runner1.start();
  }
}

Ihmetteletkö, mihin start() -metodin  lauseessa runner.start() viitataan?. Ylikirjoitettu start -metodi liittyy luokan digitaaliKello olioon ja runner.start() -lauseessa esiintyvä start() -metodi kuuluu Thread -luokan olioon runner. Niillä ei ole mitään tekemistä toistensa kanssa. Pelkästään huonoa tuuria on se, että metodeilla on samat nimet.

Kutsuttaessa luokkametodia Thread.start(), kutsutaan automaattisesti myös metodia run(). (vrt. repaint() kutsuu automaattisesti metodia paint().)

Ylikirjoitetaan stop() -metodi, jossa säie poistetaan laittamalla sen osoitteeksi null.

public void stop() {
  if (runner != null) runner=null;
  if (runner1 != null) runner1=null;
}

Lopuksi tehdään säikeiden suorittava osa (edellä olleessa kuviossa tätä kutsuttiin sivusäikeeksi) run() -metodiin.

public void run(){
  Thread thisThread=Thread.currentThread(); // run() -metodia kutsutaan seka
//runner.start() että runner1.start() -metodeista. currenThread() -metodilla selvitetään, mikä säie on kysessä
  while (runner==thisThread) {
    aika = " "+theDate.toString();
    repaint();
    try {
      Thread.sleep(1000);
    }
    catch (InterruptedException e) {}  //pakollinen
  }
  while (runner1==thisThread) {
    aika1 = " "+theDate.toString();
    repaint();
    try {
      Thread.sleep(5000);
    }
    catch (InterruptedException e) {}
  }
}

Luokkametodi Thread.currentThread() palauttaa run() -metodia kutsuneen säikeen nimen. Tämän perusteella run() -metodissa voidaan reagoida halutulla tavalla. Nyt  kummassakin säikeessä kirjataan ylös senhetkinen aika ja odotetaan sleep -metodilla hetki, ennen while -silmukan jatkoa. Luokkametodi Thread.sleep(). pysäyttää säikeen suorituksen ajaksi, joka ilmoitetaan parametrina millisekunneissa. repain() hoitaa näytön päivityksen. sleep() -metodi vaatii keskeytyksen hallinnan.

Lopuksi ylikirjoitetaan paint() -metodi. Siinä ylemmälle riville kirjoitetaan aika ja alemmalle aika1.

public void paint(Graphics screen) {
  screen.setFont(theFont);
  screen.setColor(Color.black);
  screen.drawString(aika,10,50);
  screen.drawString(aika1,10,75);
}

Seuraavassa on listattu koko ohjelma digitaaliKello. Paketissa java.util   on ajanhakumetodi

Esimerkki digitaaliKello

import java.awt.*;
import java.util.*;
public class digitaaliKello extends java.applet.Applet implements Runnable
{
  Font theFont = new Font("TimesRoman",Font.BOLD,24);
  Date theDate;
  Thread runner, runner1;
  String aika,aika1;
  public void start() {
    if (runner==null) {
      runner = new Thread(this);
      runner.start();
    }
    if (runner1==null) {
      runner1 = new Thread(this);
      runner1.start();
    }
  }
  public void stop() {
    if (runner != null) runner=null;
    if (runner1 != null) runner1=null;
  }
  public void run(){
     Thread thisThread=Thread.currentThread();
     while (runner==thisThread) {
      aika = " "+theDate.toString();
      repaint();
      try {
        Thread.sleep(1000);
      }
      catch (InterruptedException e) {}
    }
    while (runner1==thisThread) {
      aika1 = " "+theDate.toString();
      repaint();
      try {
        Thread.sleep(5000);
      }
      catch (InterruptedException e) {}
    }
  }
  public void paint(Graphics screen) {
    screen.setFont(theFont);
    screen.setColor(Color.black);
    screen.drawString(aika,10,50);
    screen.drawString(aika1,10,75);
  }
}

Kuten digitaaliKello -appletin ikkunasta yllä nähdään, välkyy kuva hieman huolimatta säikeistyksestä. Välkkyminen lisääntyisi, jos ikkunaa päivitettäisiin useammin. Tähän apuna on kaksi keinoa, jota käsitellään kappaleessa 10.5.4.

10.5.3 Kuvien käyttö appletissa

Kuvat tuodaan ja viedäään applettiin Applet- ja Graphics -luokan metodeilla. Kuvan käsittely puolestaan tehdään image luokan metodeilla.

Kuvat ovat javassa joko gif tai jpg -muodossa. Kuvat sijaitsevat fyysisesti omassa tiedostossaan, joten appletille tulee kertoa, missä tiedostosa kuva sijaitsee. Tiedostonimi ilmoitetaan antamalla appletille tiedoston URL -osoite (esim. http://wwnet.fi/kimpinen/kurssit/tt/kurssi3/pullo1.jpg). Nyt olisi hyvä hetkeksi pysähtyä  miettimään. Jos kuvatiedoston URL pitää kirjoittaa koodiin, niin mitä tapahtuu, kun kuvaa tai sivua siirretään? Ei voi olla järkevää, että kaikki kuvia sisältävät appletit pitää kääntää uudestaan, jos sivu vaihtaa paikkaa. Ratkaisu on seuraava. Kiinnitetään html -dokumentissä olevat .class ja .jpg (tai .gif) tiedostot toisiinsa. Jos html -dokumentti vaihtaa paikkaa, myös kaikki kuvat ja appletit vaihtavat paikkaa. Tällöin esimerkiksi kuva pullo1.jpg saataisiin käyttöön appletissa komenolla

Image kuva1 = getImage(getCodeBase(),"pullo1.jpg");

getCodeBase() -metodi palauttaa sen html -sivun URL -osoitteen, jossa appletti on. Jälkimmäinen parametri on kuvatiedoston osoite getCodeBase() -metodin palauttaman URL:n suhteen.

Ladatun kuvan leveys ja korkeus pikseleinä saadaan komennoilla

int leveys = kuva1.getWidth(this);
int korkeus = kuva1.getHeight(this);

Kun kuva on ladattu appletiin, se piirretään ikkunan koordinaatteihin (x,y) komennolla

ikkuna.drawImage(kuva1, x, y, this); //this pakollinen. Ilmoittaa, että kuva piirretään tähän appletti-ikkunaan

Jos kuvaa halutaan skaalata, niin ylikuormitettu drawImage olisi seuraavanlainen

ikkuna.drawImage(kuva1, x, y, xscale, yscale, this);

Kuvan koko määräytyy kertomalla leveys tekijällä xscale ja korkeus tekijällä y-scale.

Seuraavassa appletissa E62, on piirretty kuva omassa skaalassaaan ja kolminkertaisena suurennuksena.

Apletti E62

Ohjelmakoodi on seuraavanlainen.

import java.awt.Graphics;
import java.awt.Image;
public class E62 extends java.applet.Applet {
  Image kuva1;
  public void init() {
    kuva1 = getImage(getCodeBase(),"pullo1.jpg");
  }
  public void paint(Graphics ikkuna){
    int leveys = kuva1.getWidth(this);
    int korkeus = kuva1.getHeight(this);
    ikkuna.drawImage(kuva1,10,10,this);
    ikkuna.drawImage(kuva1,10+leveys+10,10,leveys*3, korkeus*3, this);
  }
}

Kuten nähdään kuvan suurentuessa suurentuvat myös kuvassa olevat virheet. Nyt voimme palata takaisin Välkkymisongelmaan.

10.5.4 Välkkymisen ehkäisy

Animoidun kuvan välkkyminen johtuu paitsi käytetystä laite-alustasta ja koneessa olevasta kuormasta (moniajojärjestelmässähän on paljon muitakin toimintoja käynnissä yhtäikaa Javan virtuaalikoneen kanssa), myös Javan itsensä tavasta päivittää ikkunaa.

Olemme oppineet, että kutsuttaessa repaint() metodia, kutsutaan myös paint() -metodia. Tässä ei ole koko totuus. paint() -metodi piirtää ikkunan sisällön. Kuka poistaa vanhan sisällön ennen uuden piirtoa. Vastaus on update() metodi, jota repaint() -metodissa kutsutaan ennen paint() -metodia. Välkkymisen aiheuttaa se, että aina ikkunan piirtopinnan uudelleen piirtämistä, update() -metodi alustaa piirtopinnan background värillä. Kokeile asettamalla taustan väriksi jokin räikeä väri (esim. oranssi) ja piirtämällä sitten iso valkea suorakaide ikkunaan. Siirrä applettia vierityspalkista.   Nyt silmukassa tehty repaint() välkyttää ikkunaa oikein selvästi ja update() -metodin merkitys näkyy. Ongelmaan on kaksi ratkaisua

ylikirjoitetaan update() joko siten, että se ei pyyhikään ikkunaa taustavärillä tai tekee sen vain ikkunan aktiiviseen osaan
ylikirjoitetaan sekä update() että paint() -metodit ja käytetään menetelmää nimeltä kaksoispuskurointi

Edellinen on helpompi tapa toteuttaa. Jälkimmäinen taas on huomattavasti parempi tapa poistaa välkkyminen.

10.5.4.1 Update() -metodin ylikirjoitus

Jotta update() -metodin osaisi ylikirjoittaa, niin listataan sen oletus näkyviin. Graphics -luokan metodi update() on likimain seuraavanlainen

public void update(Graphics ikkuna){
  ikkuna.setColor(getBackground());
  ikkuna.fillRect(0,0,getSize().width, getSize().height);
  ikkuna.setColor(getForeground());
  paint(ikkuna);
}

Ylikirjoittamassasi update() -metodissa pitäisi olla ainakin paint() -metodikutsu.

Seuraavassa on tehty digitaalikello -appletti siten, että update() -metodi ylikirjoitetaan seuraavasti

public void update(Graphics ikkuna){
  paint(ikkuna);
}

Tässä olisi appletti digitaalikello1.class

Vilkkuminen loppui, mutta muuten uudessa versiossa ei olekkaan mitään järkeä. Yritä ylikirjoittaa update() -metodi sillä tavalla, että vain vaihtuva osa numerosta pyyhitään yli. Tämä parantaa tilannetta oleellisesti (vrt E54).

10.5.4.2 Kaksoispuskurointi

Kaksoispuskuroinnin ideana on se, että käytetään kahta erillistä ikkunaa, joista toiseen piirretään ja toinen näytetään. Kun piirros on valmis, piirretään ruutuun piilossa ollut ikkuna. Siis piilossa olevaan ikkunaan piirretään ja edellinen ikkuna näytetään. Nyt näytettävään ikkunaan ei ikinä piirretä, joten se ei voi väristä. Kun piirros saadaan valmiiksi, kannattaa pitää pieni tauko (40 ms - 100 ms) ennen kuin se näytetään. Tämä säästää prosessoritehoa. Kannattaa muistaa, että ihmissilmä ei erota paljoa yli 20 kuvaa sekunnissa toisistaan. (20 Hz vastaa aikana 50 ms kuvien välillä).

Ainoa ongelma kaksoispuskuroinnissa on, että java ei tunne käsitteenä ikkunan vaihtoa. Tämä voidaan kuitenkin kiertää piirtämällä toinen ikkuna kuvaksi, joka sitten piirretään ikkunaan aina tarpeen tullen.

Puskurointia varten luodaan Image ja Graphics -oliot.

Image taustaKuva;
Graphics taustaIkkuna;

Alustuksen yhteydessä määrätään olion taustaKuva koko (sehän voi olla melkein mitä tahansa). Käyttämällä createImage() -metodia, luodaan Image -luokan olio, jonka osoite kopioidaan oliolle taustaKuva. Metodilla getGraphics() kuvasta luodaan Graphics olio.

taustaKuva = createImage(getsize().width, getSize().height);
taustaIkkuna = taustaKuva.getGraphics();

Piirretään kuva1 (vrt E62)  taustaIkkunaan seuraavasti

taustaIkkuna.drawImage(kuva1,10,10,this);

Lopuksi taustaIkkunan sisältämä kuva saadaan näkyville appletin ikkunaan seuraavasti. (taustaKuva ja taustaIkkuna osoittavat samaan osoitteeseen, vaikka ovatkin eri luokkien olioita.)

ikkuna.drawImage(taustaKuva,0,0,this);

Piirtometodit update() ja paint() tulee ylikirjoittaa tavalla, jotka käyvät selville seuraavasta esimerkistä E63

Lopuksi tulee ylimääräinen taustaIkkuna poistaa vielä destroy() -metodilla seuraavasti

public void destroy(){
  taustaIkkuna.dispose();
}

Tämä on hyvä tehdä siitäkin huolimatta, että javan ajoaikainen roskankeruu huolehtii loppuunkäytetyistä olioista. Taustaikkuna voi olla niin suuri, että sen lyhytaikainenkin turha taustaikkunan käyttö ja säilytys kuormittavat järjestelmää liikaa.

Esimerkki E63. Esimerkki kaksoispuskuroinnin käytöstä. Kuvaa siirretää edestakaisin keltaisen ja sinisen suorakaiteen päällä

E63.class -appletti

Ohjelmakoodi on seuraavanlainen

import java.awt.*;
public class E63 extends java.applet.Applet implements Runnable {
  Thread animaatio;
  Image kuva1;
  int xPos = 5, xMove = 2, leveys, korkeus;
  Image taustaKuva;
  Graphics taustaIkkuna;
  public void init() {
    kuva1 = getImage(getCodeBase(),"pullo4.jpg");
    leveys = kuva1.getWidth(this);
    korkeus = kuva1.getHeight(this);
    taustaKuva = createImage(getSize().width, getSize().height);
    taustaIkkuna = taustaKuva.getGraphics();
  }
  public void start(){
    if (animaatio == null) {
      animaatio = new Thread(this);
      animaatio.start();
    }
  }
  public void stop(){
    animaatio = null;
  }
  public void run(){
    Thread tämäSäie = Thread.currentThread();
    while (animaatio == tämäSäie){
      xPos += xMove;
      if (xPos > 105 | xPos < 5) xMove*=-1; // suunta vaihtuu
      repaint();
      try {
        Thread.sleep(30);
      } catch( InterruptedException e) { }
    }
  }
  public void update(Graphics ikkuna) {
    paint(ikkuna);
  }
  public void paint(Graphics ikkuna){
// piirretään taustaIkkunaan
    taustaIkkuna.setColor(Color.yellow);
    taustaIkkuna.fillRect(0,0,100,150);
    taustaIkkuna.setColor(Color.blue);
    taustaIkkuna.fillRect(100,0,101,150);
    taustaIkkuna.drawImage(kuva1,xPos,10,this);
// näytetään tausta
    ikkuna.drawImage(taustaKuva,0,0,this);
  }
  public void destroy(){
    taustaIkkuna.dispose();
  }
}

Nyt jokaisen ikkunapäivityksen välillä animaatio -säie lepää 20 ms. Tämä on varsin vähän ja heikkotehoisemmassa koneessa pienikin lisätyö prosessorille (esim hiiren liikuttelu) aiheuttaa nykimistä pullon edestakaisessa liikkeessä. Nykiminen aiheutuu joko siitä, että hidas prosessori ei ehdi laskea uutta kuvaa ja kuva tuntuu jäävän paikalleen tai sitten kuvia syntyy nopealla prosessorilla niin nopeasti, ettei paint()-metodeja ehditä suorittaa. Tässä tapauksessa JVM hyppää muutaman jonoon jääneen päivityksen yli ja kuva nytkähtää eteenpäin.

10.5.5 Kuva-animaatio

Edellisen perusteella on jo arvattavissakin, kuinka animaatio tehdään. Ohjelma on muuten samanlainen kuin E63, mutta valmiit liikkeen osat luetaan taulukkoon, jonka alkioden tyyppinä on Image.

Apletti E64.class

Alla olevat kuvat muodostavat animaation

wpe1D.jpg (1529 bytes)wpe1E.jpg (1894 bytes)wpe1F.jpg (2250 bytes)pullo4.jpg (2911 bytes)wpe21.jpg (3574 bytes)wpe23.jpg (3946 bytes)wpe24.jpg (4326 bytes)

Seuraavassa on ohjelmakoodi niiltä osin, kuin se eroaa esimerkistä E63

import java.awt.*;
import java.awt.Graphics;
import java.awt.Image;
public class E64 extends java.applet.Applet implements Runnable {
  Thread animaatio;
  Image kuvat[] = new Image[7];
  Image nykyKuva;
  String kuvaNimet[] = {"pullo1.jpg","pullo2.jpg","pullo3.jpg",
    "pullo4.jpg","pullo5.jpg","pullo6.jpg","pullo7.jpg"};
  int leveys, korkeus, kuvanNumero=0, muutos=1;
  Image taustaKuva;
  Graphics taustaIkkuna;
  public void init() {
// luetaan kuvat taulukkoon
    for (int i=0; i<kuvat.length;i++)
      kuvat[i]= getImage(getCodeBase(),kuvaNimet[i]);
    taustaKuva = createImage(getSize().width, getSize().height);
    taustaIkkuna = taustaKuva.getGraphics();
  }

public void run(){
  Thread tämäSäie = Thread.currentThread();
  while (animaatio == tämäSäie){
    nykyKuva = kuvat[kuvanNumero];
    kuvanNumero += muutos;
    if (kuvanNumero>=kuvat.length) {
      kuvanNumero=kuvat.length-1; muutos = -1;
    }
    if (kuvanNumero<0) {
      kuvanNumero=0; muutos = +1;
    }
    repaint();
    try {
      Thread.sleep(100);
    } catch( InterruptedException e) { }
  }
}
public void paint(Graphics ikkuna){
  // piirretään taustaIkkunaan
  leveys = kuvat[kuvat.length-1].getWidth(this); // suurin kuva
  korkeus = kuvat[kuvat.length-1].getHeight(this);
  taustaIkkuna.setColor(Color.red);
  taustaIkkuna.fillRect(0,0,leveys,korkeus);
  if (nykyKuva!=null)
  taustaIkkuna.drawImage(nykyKuva,5,10,this);
// näytetään tausta
  ikkuna.drawImage(taustaKuva,0,0,this);
}

Javalla on mahdollisuus muokata kuvaa siten, että jokin kuvan väri tulee läpinäkyväksi taustan suhteen. Tässä asiassa lukijaa pyydetään kääntymään Sun -yhtiön Java www -sivujen puoleen. Esimerkissä 64 on .gif -kuvissa asetettu jo piirtoohjelmassa valkea väri taustalle läpinäkyväksi. Lukija antakoot anteeksi kuvien huonon viimeistelyn, joka näkyy valkeana utuna punaista taustaa vasten.

Ilkka Koivistoinen 13.02.2002

Edellinen

Seuraava