Анимация с Java аплети

Светлин Наков

Използване на картинки от аплет

Освен директното чертане на геометрични фигури, AWT позволява и изобразяване на картинки, заредени от GIF или JPEG файлове. За целта се използва класа java.awt.Image и метода на класа Graphics drawImage(), който има няколко варианта с различни входни параметри. Най-лесният начин за зареждане на картинка в Image обект се дава от метода getImage() на класа Applet, който приема URL като параметър. Ето един пример как можем да заредим картинка:

URL imageURL = new URL("http://www.nakov.com/images/dot.jpg");
java.awt.Image img = this.getImage(imageURL);

За да начертаем върху аплета заредената картинка можем да използваме следния код:

public void paint(Graphics g) {
    g.drawImage(img, 20, 10, this);
}

Ако искаме да начертаем картинката с променени размери, можем да използваме същия метод drawImage(), но с други параметри:

g.drawImage(img, 0, 0, img.getWidth(this)/4,
    img.getHeight(this)/4, this);

Тънкости при работата с картинки в AWT

Внимателният читател вероятно е забелязал, че методът drawImage() приема един параметър, за който в нашите примери даваме стойност this. Това съвсем не е случайно и се обяснява с архитектурата на AWT и начина, по който се работи с картинки. Методът drawImage() приема като последен параметър обект, който реализира интерфейса ImageObserver. Зареждането на картинка в AWT винаги става асинхронно, т.е. извършва се паралелно с работата на програмата. Това е съвсем обосновано, като се има предвид, че зареждането на картинка от Интернет отнема известно време, а програмата може да го използва за други цели, вместо да чака. По идея методът drawImage() не изчаква картинката да бъде заредена и тогава да я начертае, а чертае само тази част от нея, която вече е заредена и веднага връща управлението на извикващия метод. Когато картинката се зареди напълно, се извиква методът imageUpdate() на интерфейса ImageObserver, който трябва да обработи ситуацията по подходящ начин. Най-често реализацията на метода imageUpdate() пречертава картинката, т.е. извиква метода drawImage().

Стандартно класът java.awt.Component, който е прародител на класа java.applet.Applet реализира интерфейсът ImageObserver и в метода си imageUpdate() пречертава областта от екрана, която е обхваната от картинката, която се е завършила своето зареждане. Използвайки тази базова функционалност на класа Applet,  можем винаги, когато зареждаме картинки от аплет, да подаваме за ImageObserver самия аплет, т.е. обекта this.

Когато зареждаме картинки винаги трябва да внимаваме да не разчитаме, че дадена картинка ще се зареди веднага. Картинките винаги се зареждат асинхронно и затова трябва да се грижим да ги чертаем едва след като са заредени или поне да ги пречертаваме след това.

За изчакване на асинхронното зареждане на картинка в AWT има специален клас java.awt.MediaTracker, на който чрез метода addImage(…) може да му се подават картинки, които са в процес на зареждане, след което може да се изчака приключването на зареждането на всички картинки чрез метода waitForAll().

Пример за аплет за анимация

Да разгледаме един цялостен пример за аплет, който използва картинки и реализира проста анимация чрез допълнителна нишка. Да си поставим за задача направата на Java аплет, в който една топка постоянно се движи и се отблъсква в стените на аплета при удар (както при игра на билярд). Едно възможно решение на задачата е следното:

BallApplet.java
import java.awt.*; 
import java.applet.*; 
 
public class BallApplet extends Applet implements Runnable { 
    public static final int ANIMATION_SPEED = 10; 
    public static final String IMAGE_NAME_PARAM = "imageName"; 
 
    private int mBallX, mBallY, mBallSpeedX, mBallSpeedY; 
    private Image mBallImage; 
    private Image mImageBuffer; 
    private Graphics mImageBufferGraphics; 
 
    private Thread mAnimationThread; 
    private boolean mAnimationThreadInterrupted = false; 
 
    /** 
     * Applet's init() method. Makes some initializations 
     * and loads the ball image. This method is called before 
     * creating the animation thread so no synchronization 
     * is needed. 
     */ 
    public void init() { 
        // Load the ball image from the server 
        String imageName = getParameter(IMAGE_NAME_PARAM); 
        if (imageName == null) { 
            System.err.println("Applet parameter " + 
                IMAGE_NAME_PARAM + " is missing."); 
            System.exit(-1); 
        } 
        mBallImage = getImage(getCodeBase(), imageName); 
 
        // Wait for the image to load completely 
        MediaTracker tracker = new MediaTracker(this); 
        tracker.addImage(mBallImage,0); 
        try { 
            tracker.waitForAll(); 
        } catch (InterruptedException ie) { } 
        if (tracker.statusAll(true) != MediaTracker.COMPLETE) { 
            System.err.println("Can not load " + imageName); 
            System.exit(-1); 
        } 
 
        // Initialize the ball image coordinates and speed 
        mBallX = 1; 
        mBallY = 1; 
        mBallSpeedX = 1; 
        mBallSpeedY = 1; 
 
        // Create an image buffer for the animation 
        mImageBuffer = createImage( 
            getSize().width, getSize().height); 
        mImageBufferGraphics = mImageBuffer.getGraphics(); 
    } 
 
    /** 
     * Applet's paint() method. Draws the ball on its current 
     * position. This method can be called in the same time 
     * from both the applet's thread and from the animation 
     * thread so it should be thread safe (synchronized). 
     */ 
    public void paint(Graphics aGraphics) { 
        synchronized (this) { 
            if (mAnimationThread != null) { 
                // Paint in the buffer 
                mImageBufferGraphics.fillRect( 
                    0, 0, getSize().width, getSize().height); 
                mImageBufferGraphics.drawImage( 
                    mBallImage, mBallX, mBallY, this); 
 
                // Copy the buffer contents to the screen 
                aGraphics.drawImage(mImageBuffer, 0, 0, this); 
            } 
        } 
    } 
 
    /** 
     * Applet's start() method. Creates the animation thread 
     * and starts it if it is not already running. This method 
     * can be called only from the applet's thread so it does 
     * not require synchronization. 
     */ 
    public void start() { 
        if (mAnimationThread == null) { 
            mAnimationThreadInterrupted = false; 
            mAnimationThread = new Thread(this); 
            mAnimationThread.start(); 
        } 
    } 
 
    /** 
     * Applet's stop() method. Asks the animation thread to 
     * stop its execution and waits until it is really stopped. 
     * This method is called only from the applet's thread so 
     * it does not need synchronization except when accessing 
     * the variable mAnimationThreadInterrupted that is common 
     * for applet's thread and animation thread. 
     */ 
    public void stop() { 
        synchronized (this) { 
            mAnimationThreadInterrupted = true; 
        } 
        try { 
            mAnimationThread.join(); 
        } catch (InterruptedException ie) { } 
        mAnimationThread = null; 
    } 
 
    /** 
     * Animation thread's run() method. Continuously changes 
     * the ball position and redraws it and thus an animation 
     * effect is achived. This method runs in a separate thread 
     * that is especially created for the animation. A 
     * synchronization is needed only when accessing variables 
     * that are common for the applet's thread and animation 
     * thread. 
     */ 
    public void run() { 
        // Calculate the animation area size 
        int maxX, maxY; 
        synchronized (this) { 
            maxX = this.getSize().width - 
                mBallImage.getWidth(this); 
            maxY = this.getSize().height - 
                mBallImage.getHeight(this); 
        } 
 
        // Perform continuously animation 
        while (true) { 
            synchronized (this) { 
                // Check if the thread should stop 
                if (mAnimationThreadInterrupted) 
                    break; 
 
                // Calculate the new ball coordinates 
                if ((mBallX >= maxX) || (mBallX <= 0)) 
                    mBallSpeedX = -mBallSpeedX; 
                mBallX = mBallX + mBallSpeedX; 
                if ((mBallY >= maxY) || (mBallY <= 0)) 
                    mBallSpeedY = -mBallSpeedY; 
                mBallY = mBallY + mBallSpeedY; 
            } 
 
            // Redraw the applet contents 
            paint(getGraphics()); 
 
            // Wait some time to slow down the animation speed 
            try { 
                Thread.sleep(ANIMATION_SPEED); 
            } catch (Exception ex) {} 
        } 
    } 
}

Как работи аплета за анимация

Създаването на анимация по принцип не е много проста работа, защото изисква управление на нишки. Необходимо е добро познаване на жизнения цикъл на аплетите, познаване на библиотеката AWT, умения за работа с нишки и синхронизация на достъпа до общи ресурси.

Нашият примерен аплет за анимация работи по следния начин: При инициализация, в метода init(), аплетът зарежда картинката, която ще се движи (в нашия случай това е топка), инициализира координатите и посоката й на движение и създава специален буфер за чертане за целите на анимацията.

При стартиране на аплета, когато се извиква методът му start(), се създава една нишка, която се грижи за анимацията. Всичко, което тя прави е да променя през определено време (в случая 10 милисекунди) координатите по x и по y на топката съгласно текущата посока на движение, да сменя посоката на движение при удар в стена и след всяка промяна на координатите на топката да пречертава цялото съдържание на аплета. Методът start() няма нужда от синхронизация, защото когато се изпълнява единствената работеща нишка е нишката на аплета.

При извикване на stop() метода аплетът спира нишката и я изчаква да приключи изпълнението си. Когато този метод се извика е възможно да работят едновременно две нишки – нишката на аплета и нишката за анимация и затова е необходима синхронизация при достъпа до общи за двете нишки променливи.

Пречертаването на аплета (paint(…) метода)  работи с буфериране за да се избегне трепкането, което би се получило ако се изтрие съдържането на аплета и след това се начертае върху него движещият се обект на текущата му позиция. Пречертаването е операция, която може да се извика едновременно от две различни нишки. Нишката на аплета може по всяко време да извика пречертаване заради засичане на аплета с друг прозорец или по някаква друга причина, а нишката за анимация също по всяко време да извика пречертаване заради нуждата от движение на топката. За да не се засичат две пречертавания, понеже те биха използвали един и същ работен буфер, е необходимо пречертаването да е синхронизирана операция.

Техниката „двойно буфериране”

Пречертаването на аплета работи със специален буфер. Този буфер се използва за да се избегне премигването и да се получи наистина плавно движение. При всяко пречертаване на аплета буферът се изчиства с fillRect(), след това в него се начертава топката на текущата й позиция и на екрана се изобразява съдържанието на този буфер. Тази техника за избягване на премигването при създаване на анимация се нарича „двойно буфериране” (double buffering). При стартиране на аплета се създава обект от класа Image и се работи чрез неговия Graphics обект. Вместо да се рисува директно върху аплета, се рисува в буфера и след това нарисуваният вече кадър от буфера се прехвърля върху повърхността на аплета.

Името на файла, който съдържа картинката, се задава като параметър на аплета и се взема с метода getParameter(). Параметрите на аплетите служат за задаване на различни настройки без да е необходима прекомпилация, ако се налага ако тези настройки бъдат променени. За задаването им има специален таг, който се влага в така <applet> – тага <param>.

Стартиране на аплета за анимация

Ето един примерен HTML код, който стартира нашия аплет и задава за параметъра име на картинка imageName стойността ball.jpg:

BallAppletTest.html
<html> 
    <head><title>Ball Applet – Test Page</title></head> 
<body> 
    <applet code="BallApplet.class" width="200" height="150"> 
        <param name="imageName" value="ball.jpg"> 
    </applet> 
</body> 
</html>

Разбира се, в директорията, където е записан този html файл е необходимо да запишем компилирания аплет BallApplet.class, както и файлa с топката, която ще подскача в аплета – ball.jpg.

В инициализационната си част аплетът взима параметъра imageName и зарежда картинката с това име от същата директория, от която е зареден аплетът. URL, сочещо към тази директория може да се получи чрез метода getCodeBase(). Такъв е правилният начин за извличане на ресурс от аплет – не чрез абсолютен URL, а чрез релативен URL, зададен спрямо директорията, от която е зареден аплетът.

След като е извикан методът за зареждане на картинка тя е започва да се зарежда от зададения URL. Понеже, както знаем, работата с картинки в AWT е асинхронна, е необходимо да изчакаме картинката с топката да се зареди напълно преди да се обръщаме към нея. За целта се използва класът MediaTracker, чрез който може да се проследи състоянието на започнали да се зареждат картинки.

Аплетът за анимация в действие

Ето как изглежда резултата от нашия аплет, видян през Internet Explorer:

За да изглежда добре е необходимо картинката, която представлява топката (ball.jpg) да е по-малка от размерите на аплета и да бъде на черен фон.