打造一個同時支援 Canvas 繪製與 MIDP 2.0 CustomItem的計數器組件,核心需求:
paint();Form 透過 CustomItem 封裝。
public class CounterArea {
public static final int DEFAULT_RATE = 500; // ms
public static final int MIN_RATE = 100;
// 回呼介面:將無效化、重繪、尺寸變更委派給外層控制器 (Canvas/CustomItem)
public interface Callback {
void invalidateCounter(CounterArea counter);
void repaintCounter(CounterArea counter);
void resizeCounter(CounterArea counter);
}
private int value = 0;
private int rate = DEFAULT_RATE;
private int left, top, width = -1, height = -1;
private int backColor = 0xFFFFFF, textColor = 0x000000;
private Font font;
private Callback callback;
private Thread timerThread;
private volatile boolean running = false;
// --- 屬性存取 (含自動無效化) ---
public void setBackColor(int c) { backColor = c; invalidate(); }
public void setTextColor(int c) { textColor = c; invalidate(); }
public void setFont(Font f) { font = f; invalidate(); }
public void setRate(int r) { rate = Math.max(r, MIN_RATE); }
public void setLeft(int l) { left = l; }
public void setTop(int t) { top = t; }
public void setWidth(int w) { width = w; invalidate(); }
public void setHeight(int h) { height = h; invalidate(); }
public synchronized void setValue(int v) { value = v; repaint(); }
public int getValue() { return value; }
public int getWidth() { return width < 0 ? getMinWidth() : width; }
public int getHeight() { return height < 0 ? getMinHeight() : height; }
public int getMinWidth() {
Font f = font != null ? font : Font.getDefaultFont();
return f.stringWidth(String.valueOf(value));
}
public int getMinHeight() {
Font f = font != null ? font : Font.getDefaultFont();
return f.getHeight();
}
// --- 繪製核心:右對齊、垂直置中 ---
public void paint(Graphics g) {
String s = String.valueOf(value);
Font f = font != null ? font : Font.getDefaultFont();
int tw = f.stringWidth(s);
int th = f.getHeight();
int w = getWidth(), h = getHeight();
g.setColor(backColor);
g.fillRect(left, top, w, h);
g.setColor(textColor);
g.drawString(s, left + w - tw, top + (h - th) / 2, Graphics.TOP | Graphics.LEFT);
// 自適應擴展
if (tw > w || th > h) resize();
}
// --- 計時器執行緒 ---
public synchronized void start() {
if (running) return;
running = true;
timerThread = new Thread(() -> {
while (running) {
try { Thread.sleep(rate); } catch (InterruptedException ignored) {}
synchronized (CounterArea.this) {
if (!running) break;
value++;
repaint();
}
}
}, "CounterTimer");
timerThread.start();
}
public synchronized void stop() {
running = false;
timerThread = null;
}
public boolean isCounting() { return running; }
// --- 回呼委派 ---
private void invalidate() { if (callback != null) callback.invalidateCounter(this); }
private void repaint() { if (callback != null) callback.repaintCounter(this); }
private void resize() { if (callback != null) callback.resizeCounter(this); }
public void setCallback(Callback cb) { this.callback = cb; }
}
public class CounterCanvas extends Canvas {
private CounterArea counter;
public CounterCanvas() {
counter = new CounterArea(10, 10); // left=10, top=10
counter.setCallback(new CounterArea.Callback() {
public void invalidateCounter(CounterArea c) { repaint(); }
public void repaintCounter(CounterArea c) { repaint(c.getLeft(), c.getTop(), c.getWidth(), c.getHeight()); }
public void resizeCounter(CounterArea c) { repaint(); } // 簡單全畫面重繪
});
counter.setRate(1000); // 1 秒遞增
counter.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE));
counter.setTextColor(0x0000FF);
}
protected void paint(Graphics g) {
g.setColor(0xFFFFFF); g.fillRect(0,0,getWidth(),getHeight());
counter.paint(g);
}
protected void showNotify() { counter.start(); }
protected void hideNotify() { counter.stop(); }
}
public class CounterItem extends CustomItem implements CounterArea.Callback {
private CounterArea area;
public CounterItem() {
super(null);
area = new CounterArea();
area.setCallback(this);
}
// CustomItem 必須實作:最小/偏好尺寸
public int getMinContentWidth() { return area.getMinWidth(); }
public int getMinContentHeight() { return area.getMinHeight(); }
public int getPrefContentWidth(int h) { return getMinContentWidth(); }
public int getPrefContentHeight(int w) { return getMinContentHeight(); }
// 佈局變更時同步尺寸
protected void sizeChanged(int w, int h) {
area.setWidth(w);
area.setHeight(h);
}
// 顯示/隱藏控制計時器
protected void showNotify() { area.start(); }
protected void hideNotify() { area.stop(); }
// 繪製委派
protected void paint(Graphics g, int w, int h) {
area.setWidth(w);
area.setHeight(h);
area.paint(g);
}
// Callback 實作:轉發給 CustomItem 系統
public void invalidateCounter(CounterArea c) { invalidate(); }
public void repaintCounter(CounterArea c) { repaint(); }
public void resizeCounter(CounterArea c) { invalidate(); }
// 對外便利方法
public void setValue(int v) { area.setValue(v); }
public int getValue() { return area.getValue(); }
public void setFont(Font f) { area.setFont(f); }
public void setRate(int r) { area.setRate(r); }
public void setColors(int bg, int fg) { area.setBackColor(bg); area.setTextColor(fg); }
// 不支援焦點遍歷
public boolean traverse(int dir, int vw, int vh, int[] vrect) { return false; }
}
public class CounterDemoMIDlet extends MIDlet implements CommandListener {
private Display display;
private Command exitCmd = new Command("Exit", Command.EXIT, 1);
private Command switchCmd = new Command("Switch", Command.SCREEN, 2);
private boolean showCanvas = true;
protected void startApp() {
if (display == null) {
display = Display.getDisplay(this);
showCanvasView();
}
}
private void showCanvasView() {
CounterCanvas canvas = new CounterCanvas();
canvas.addCommand(exitCmd);
canvas.addCommand(switchCmd);
canvas.setCommandListener(this);
display.setCurrent(canvas);
showCanvas = true;
}
private void showFormView() {
Form form = new Form("CounterItem Demo");
CounterItem item = new CounterItem();
item.setLayout(Item.LAYOUT_CENTER | Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER);
item.setFont(Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_ITALIC, Font.SIZE_MEDIUM));
item.setColors(0xFFFF00, 0xFF0000); // 黃底紅字
form.append(item);
form.addCommand(exitCmd);
form.addCommand(switchCmd);
form.setCommandListener(this);
display.setCurrent(form);
showCanvas = false;
}
public void commandAction(Command c, Displayable d) {
if (c == exitCmd) notifyDestroyed();
else if (c == switchCmd) {
if (showCanvas) showFormView(); else showCanvasView();
}
}
protected void pauseApp() {}
protected void destroyApp(boolean u) {}
}
| 特性 | 傳統寫法 | CounterArea 方案 |
|---|---|---|
| 程式碼複用 | Canvas/Form 寫兩套 | 核心邏輯單一來源 |
| 記憶體效率 | 每幀新建 String/Font | 複用緩衝區、字型、回呼 |
| 擴充性 | 修改分散 | 擴充 CounterArea 即雙模受益 |
| 測試性 | 需 UI 環境 | 純邏輯可單元測試 |
| 生命週期管理 | 手動控制 Thread | showNotify/hideNotify 自動綁定 |
value 映射為進度百分比,paint() 繪製長條而非文字。「邏輯與 UI 分離、回呼介面雙向綁定、Canvas 與 CustomItem 共享核心類」是 J2ME 自定義元件開發的黃金模式。CounterArea 以不足 150 行代碼展示了此模式的精髓:高內聚、低耦合、零浪費。建議開發者將此模式內化為元件庫基因,加速後續 UI 開發。