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)
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.
Säikeellä tarkoitetaan sellaista ohjelman osaa joka toimii omillaan sillä aikaa kun muu osa ohjelmaa tekee samaan aikaan jotain muuta
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
i
mport 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.
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.
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.
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.
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);
}
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).
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ä
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.
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.
Alla olevat kuvat muodostavat animaation
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.