動畫 = 連續幀高速切換。人眼視覺暫留效應使 ≥ 15fps 即感知為流暢運動。MIDP 1.0 無內建動畫 API,但 java.util.Timer + TimerTask 提供精確週期性觸發,配合 Canvas.repaint() 即可實現。
public class AnimatedImage extends TimerTask {
private Canvas canvas; // 關聯畫布,用於觸發重繪
private Image[] frames; // 等尺寸幀陣列
private int[][] clipRects; // 可選:每幀透明區域裁剪矩陣 [x,y,w,h]...
private int currentFrame = 0;
private int x, y; // 繪製左上角座標
private int frameW, frameH; // 單幀尺寸
// 三種建構子重載
public AnimatedImage(Image[] frames) { this(null, frames, null); }
public AnimatedImage(Canvas canvas, Image[] frames) { this(canvas, frames, null); }
public AnimatedImage(Canvas canvas, Image[] frames, int[][] clipRects) {
this.canvas = canvas;
this.frames = frames;
this.clipRects = clipRects;
if (frames != null && frames.length > 0) {
frameW = frames[0].getWidth();
frameH = frames[0].getHeight();
}
// 驗證 clipRects 長度
if (clipRects != null && clipRects.length < frames.length) {
throw new IllegalArgumentException("clipRects length < frames.length");
}
}
// 移動精靈位置
public void move(int x, int y) { this.x = x; this.y = y; }
// 手動推進幀 (供 TimerTask 執行緒呼叫)
public void advance(boolean repaint) {
if (++currentFrame >= frames.length) currentFrame = 0; // 循環
if (repaint && canvas != null && canvas.isShown()) {
canvas.repaint(x, y, frameW, frameH); // 部分重繪優化
canvas.serviceRepaints(); // 立即執行 paint (關鍵!)
}
}
// 實際繪製邏輯:支援透明色模擬 (clipRects)
public void draw(Graphics g) {
if (frameW == 0 || frameH == 0) return;
Image frame = frames[currentFrame];
if (clipRects == null || clipRects[currentFrame] == null) {
g.drawImage(frame, x, y, Graphics.TOP | Graphics.LEFT);
} else {
// 多次繪製模擬透明:每次設定不同裁剪區
int[] clip = clipRects[currentFrame];
int oldX = g.getClipX(), oldY = g.getClipY();
int oldW = g.getClipWidth(), oldH = g.getClipHeight();
for (int i = 0; i + 3 < clip.length; i += 4) {
g.setClip(x + clip[i], y + clip[i+1], clip[i+2], clip[i+3]);
g.drawImage(frame, x, y, Graphics.TOP | Graphics.LEFT);
}
g.setClip(oldX, oldY, oldW, oldH);
}
}
// TimerTask 介面:週期性推進
public void run() {
if (frameW == 0 || frameH == 0) return;
advance(true);
}
}
// 從 JAR 載入連續編號 PNG: /images/bird0.png, bird1.png ...
private Image[] loadFrames(String prefix, int count) throws IOException {
Image[] imgs = new Image[count];
for (int i = 0; i < count; i++) {
imgs[i] = Image.createImage(prefix + i + ".png");
}
return imgs;
}
public class AnimatedCanvas extends Canvas {
private Display display;
private Image offscreen; // 離屏緩衝
private Vector sprites = new Vector(); // AnimatedImage 集合
public AnimatedCanvas(Display display) {
this.display = display;
if (!isDoubleBuffered()) {
offscreen = Image.createImage(getWidth(), getHeight());
}
}
public void add(AnimatedImage sprite) {
sprites.addElement(sprite);
}
protected void paint(Graphics g) {
Graphics target = g;
if (offscreen != null) target = offscreen.getGraphics();
// 清背景
target.setColor(0xFFFFFF);
target.fillRect(0, 0, getWidth(), getHeight());
// 繪製所有精靈
for (int i = 0; i < sprites.size(); i++) {
((AnimatedImage) sprites.elementAt(i)).draw(target);
}
// 雙緩衝合成
if (target != g) {
g.drawImage(offscreen, 0, 0, Graphics.TOP | Graphics.LEFT);
}
}
}
public class AnimationTest extends MIDlet implements CommandListener {
private static final int BIRD_FRAMES = 7;
private static final int NUM_BIRDS = 5;
private Display display;
private Timer timer = new Timer();
private AnimatedImage[] birds;
private Random rand = new Random();
private Command exitCmd = new Command("Exit", Command.EXIT, 1);
protected void startApp() {
if (display == null) {
display = Display.getDisplay(this);
initAnimation();
}
}
private void initAnimation() {
try {
AnimatedCanvas canvas = new AnimatedCanvas(display);
Image[] frames = loadFrames("/images/bird", BIRD_FRAMES);
int w = canvas.getWidth(), h = canvas.getHeight();
birds = new AnimatedImage[NUM_BIRDS];
for (int i = 0; i < NUM_BIRDS; i++) {
birds[i] = new AnimatedImage(canvas, frames);
birds[i].move(rand.nextInt(w), rand.nextInt(h));
canvas.add(birds[i]);
// 錯開啟動時間與幀率,產生自然群飛效果
timer.schedule(birds[i], rand.nextInt(1000), rand.nextInt(300) + 100);
}
canvas.addCommand(exitCmd);
canvas.setCommandListener(this);
display.setCurrent(canvas);
} catch (IOException e) {
System.out.println("載入動畫資源失敗");
notifyDestroyed();
}
}
public void commandAction(Command c, Displayable d) {
if (c == exitCmd) {
timer.cancel();
notifyDestroyed();
}
}
// pauseApp, destroyApp 略...
}
| 優化點 | 說明 |
|---|---|
| 部分重繪 | canvas.repaint(x,y,w,h) 僅刷新精靈區域,減少像素處理量。 |
| serviceRepaints() | 強制立即在 Timer 執行緒執行 paint,避免事件佇列延遲造成幀率抖動。 |
| 雙緩衝 | AnimatedCanvas 自動檢測 isDoubleBuffered(),軟體實作消除閃爍。 |
| 資源預載 | 所有幀在 initAnimation 一次載入,運行期零 I/O。 |
| 幀率控制 | 每個精靈獨立 Timer.schedule(task, delay, period),避免單一 Timer 任務堆積。 |
| 記憶體管理 | 精靈數量建議 < 10;單幀 < 5KB;總動畫資源 < 100KB。 |
advance() 中檢查邊界與其他精靈矩形重疊。state (idle/walk/jump),依狀態切換不同幀陣列。x = startX + (endX-startX)*t),極度省記憶體。Manager.playTone 或 MMAPI 播放音效。MIDP 1.0 雖無動畫框架,但 Timer/TimerTask + Canvas 組合極具彈性。AnimatedImage 封裝了幀管理、定時推進、透明繪製、位置控制,配合 AnimatedCanvas 統一雙緩衝與多精靈合成,構成一套輕量、可擴充的動畫引擎核心。掌握此模式,即可在 64KB JAR 限制下打造媲美原生遊戲的視覺體驗。