Java Applet 動畫的核心在於「幀與幀之間的快速切換」。本文系統講解動畫實現原理、閃爍成因與兩大解決方案 (覆寫 update()、雙緩衝),並提供一個完整的類比時鐘動畫範例。
Runnable,在 run() 中循環:修改狀態 → repaint() → Thread.sleep(間隔)。start() 啟動執行緒,stop() 設為 null 結束。paint(Graphics g) 繪製當前幀畫面。| 閃爍原因 | 解決方案 | 核心做法 |
|---|---|---|
AWT 預設 update() 先清背景再呼叫 paint() | 覆寫 update() | 不清背景,僅繪製必要區域,或直接在 update() 完成所有繪圖 |
| 直接在螢幕繪圖過程可見 | 雙緩衝 | 先繪至離屏 Image,再一次 drawImage 到螢幕 |
public void update(Graphics g) {
// 不呼叫 g.clearRect() 或 fillRect(背景)
// 直接繪製增量或完整畫面
paint(g); // 或將繪圖邏輯直接寫在這裡
}
Image offScreenImage;
Graphics offScreenGC;
Dimension offScreenSize;
public void update(Graphics g) {
Dimension d = getSize();
if (offScreenImage == null || !d.equals(offScreenSize)) {
offScreenImage = createImage(d.width, d.height);
offScreenGC = offScreenImage.getGraphics();
offScreenSize = d;
}
// 離屏繪圖
offScreenGC.setColor(getBackground());
offScreenGC.fillRect(0, 0, d.width, d.height);
offScreenGC.setColor(getForeground());
paint(offScreenGC); // 實際繪圖邏輯在 paint()
// 一次性繞到螢幕
g.drawImage(offScreenImage, 0, 0, this);
}
展示雙緩衝、三角函數計算指針角度、update() 整合、執行緒控制的綜合範例。
import java.awt.*;
import java.applet.*;
import java.util.Date;
import java.text.SimpleDateFormat;
public class AnimatorDemo extends Applet implements Runnable {
Thread timer;
int lastXs, lastYs, lastXm, lastYm, lastXh, lastYh;
SimpleDateFormat formatter;
String lastDate;
Font clockFont;
Color handColor, numberColor;
public void init() {
lastXs = lastYs = lastXm = lastYm = lastXh = lastYh = 0;
formatter = new SimpleDateFormat("yyyy EEE MMM dd HH:mm:ss");
lastDate = formatter.format(new Date());
clockFont = new Font("Serif", Font.PLAIN, 14);
handColor = Color.BLUE;
numberColor = Color.DARK_GRAY;
setBackground(Color.WHITE);
resize(300, 300);
}
// 畫圓 (Bresenham 演算法)
void plotPoints(int x0, int y0, int x, int y, Graphics g) { ... }
void circle(int x0, int y0, int r, Graphics g) { ... }
public void paint(Graphics g) {
Date now = new Date();
int s = Integer.parseInt(new SimpleDateFormat("s").format(now));
int m = Integer.parseInt(new SimpleDateFormat("m").format(now));
int h = Integer.parseInt(new SimpleDateFormat("h").format(now));
String today = formatter.format(now);
int xc = 80, yc = 55; // 表盤中心
// 計算指針端點 (角度轉弧度: 度 * PI/180, 12點鐘方向為 -90度)
int xs = (int)(Math.cos(s * Math.PI/30 - Math.PI/2) * 45 + xc);
int ys = (int)(Math.sin(s * Math.PI/30 - Math.PI/2) * 45 + yc);
int xm = (int)(Math.cos(m * Math.PI/30 - Math.PI/2) * 40 + xc);
int ym = (int)(Math.sin(m * Math.PI/30 - Math.PI/2) * 40 + yc);
int xh = (int)(Math.cos((h*30 + m/2) * Math.PI/180 - Math.PI/2) * 30 + xc);
int yh = (int)(Math.sin((h*30 + m/2) * Math.PI/180 - Math.PI/2) * 30 + yc);
// 表盤
g.setFont(clockFont);
g.setColor(handColor);
circle(xc, yc, 50, g);
g.setColor(numberColor);
g.drawString("9", xc-45, yc+3);
g.drawString("3", xc+40, yc+3);
g.drawString("12", xc-5, yc-37);
g.drawString("6", xc-3, yc+45);
// 擦除舊指針 (畫背景色)
g.setColor(getBackground());
if (xs != lastXs || ys != lastYs) g.drawLine(xc, yc, lastXs, lastYs);
if (xm != lastXm || ym != lastYm) {
g.drawLine(xc, yc-1, lastXm, lastYm);
g.drawLine(xc-1, yc, lastXm, lastYm);
}
if (xh != lastXh || yh != lastYh) {
g.drawLine(xc, yc-1, lastXh, lastYh);
g.drawLine(xc-1, yc, lastXh, lastYh);
}
// 畫新指針
g.setColor(numberColor);
g.drawLine(xc, yc, xs, ys); // 秒針
g.setColor(handColor);
g.drawLine(xc, yc-1, xm, ym);
g.drawLine(xc-1, yc, xm, ym); // 分針 (加粗)
g.drawLine(xc, yc-1, xh, yh);
g.drawLine(xc-1, yc, xh, yh); // 時針 (加粗)
g.drawString(today, 5, 125); // 顯示完整時間
// 記錄本幀座標供下幀擦除
lastXs = xs; lastYs = ys;
lastXm = xm; lastYm = ym;
lastXh = xh; lastYh = yh;
lastDate = today;
}
public void start() { if (timer == null) { timer = new Thread(this); timer.start(); } }
public void stop() { timer = null; }
public void run() {
while (timer != null) {
repaint();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
run(),paint() 只負責繪圖。g.getClipBounds() 只重繪必要區塊。paint() 內 `new` 物件 (Font、Color、StringBuilder 等),造成 GC 壓力。雖然 Applet 已退出歷史舞台,但其動畫程式設計思想 (遊戲迴路、雙緩衝、插值補幀) 完整傳承至 JavaFX、LibGDX、Android Canvas 等現代圖形框架中。