Java ME (Micro Edition) 環境下,Graphics.drawString() 不支援自動換行。本文提供完整的文字換行演算法與繪製實作,適用於行動裝置、嵌入式裝置的有限螢幕空間。
public class TextWrapper {
/**
* 將文字按指定寬度進行換行
* @param g Graphics 物件 (用於測量字元寬度)
* @param text 原始文字
* @param maxWidth 最大像素寬度
* @param font 字型
* @return 換行後的字串陣列,每個元素為一行
*/
public static String[] wrapText(Graphics g, String text, int maxWidth, Font font) {
g.setFont(font);
List lines = new ArrayList<>();
String[] paragraphs = text.split("\n"); // 先按明確換行分段
for (String para : paragraphs) {
if (para.isEmpty()) {
lines.add("");
continue;
}
StringBuilder currentLine = new StringBuilder();
char[] chars = para.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
String testLine = currentLine.append(c).toString();
int width = g.getFont().stringWidth(testLine);
if (width > maxWidth) {
// 當前字元導致超寬,需換行
currentLine.deleteCharAt(currentLine.length() - 1); // 移除最後字元
String line = currentLine.toString().trim();
lines.add(line.isEmpty() ? String.valueOf(c) : line);
currentLine = new StringBuilder(String.valueOf(c)); // 新行從當前字元開始
}
}
// 加入最後一行
if (currentLine.length() > 0) {
lines.add(currentLine.toString());
}
}
return lines.toArray(new String[0]);
}
}
public class AdvancedTextWrapper {
private static final Set LINE_START_FORBIDDEN = Set.of(
',', '。', '、', '?', '!', ';', ':', '」', '』', '】', '〕', '》',
',', '.', '?', '!', ';', ':', ')', ']', '}'
);
private static final Set LINE_END_FORBIDDEN = Set.of(
'「', '『', '【', '〔', '《', '<', '(', '[', '{', '「', '『'
);
public static String[] wrapWithKinsoku(Graphics g, String text, int maxWidth, Font font) {
List rawLines = Arrays.asList(wrapText(g, text, maxWidth, font));
List adjusted = new ArrayList<>();
for (String line : rawLines) {
String processed = adjustLineStart(line);
processed = adjustLineEnd(processed, adjusted);
adjusted.add(processed);
}
return adjusted.toArray(new String[0]);
}
private static String adjustLineStart(String line) {
// 行首禁則:若行首為禁用字元,嘗試從上一行移一字元過來
// 實作需配合上一行處理,此處簡化示意
return line;
}
private static String adjustLineEnd(String line, List previousLines) {
// 行尾禁則:若行尾為開頭禁用字元,推到下一行
if (!line.isEmpty() && LINE_START_FORBIDDEN.contains(line.charAt(line.length() - 1))) {
if (!previousLines.isEmpty()) {
String lastLine = previousLines.remove(previousLines.size() - 1);
previousLines.add(lastLine + line.charAt(line.length() - 1));
return line.substring(0, line.length() - 1);
}
}
return line;
}
}
public class TextBoxComponent {
private final Font font;
private final int x, y, width, height;
private final int lineSpacing;
private String[] wrappedLines;
private int visibleStartLine = 0; // 捲動支援
public TextBoxComponent(int x, int y, int width, int height, Font font) {
this.x = x; this.y = y; this.width = width; this.height = height;
this.font = font;
this.lineSpacing = font.getHeight() + 2; // 行高 + 間距
}
public void setText(Graphics g, String text) {
this.wrappedLines = TextWrapper.wrapText(g, text, width, font);
this.visibleStartLine = 0;
}
public void paint(Graphics g) {
if (wrappedLines == null) return;
g.setFont(font);
g.setColor(0x000000); // 黑色文字
int maxVisibleLines = height / lineSpacing;
int endLine = Math.min(wrappedLines.length, visibleStartLine + maxVisibleLines);
for (int i = visibleStartLine; i < endLine; i++) {
int lineY = y + (i - visibleStartLine) * lineSpacing;
g.drawString(wrappedLines[i], x, lineY, Graphics.TOP | Graphics.LEFT);
}
}
// 捲動控制
public void scrollUp() {
if (visibleStartLine > 0) visibleStartLine--;
}
public void scrollDown() {
int maxVisibleLines = height / lineSpacing;
if (visibleStartLine + maxVisibleLines < wrappedLines.length) {
visibleStartLine++;
}
}
public boolean canScrollUp() { return visibleStartLine > 0; }
public boolean canScrollDown() {
int maxVisibleLines = height / lineSpacing;
return visibleStartLine + maxVisibleLines < wrappedLines.length;
}
}
public class MyCanvas extends GameCanvas implements Runnable {
private TextBoxComponent textBox;
private String longText = "這是一段很長的測試文字,用來驗證 Java ME 環境下的自動換行繪製功能。"
+ "包含中文、English、數字123、標點符號,以及很長很長的英文單詞 "
+ "supercalifragilisticexpialidocious 會怎麼處理呢?";
public MyCanvas() {
super(false);
Font font = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_PLAIN, Font.SIZE_MEDIUM);
textBox = new TextBoxComponent(10, 10, getWidth() - 20, getHeight() - 20, font);
}
public void start() {
Graphics g = getGraphics();
textBox.setText(g, longText);
new Thread(this).start();
}
public void run() {
while (true) {
// 處理按鍵捲動
int key = getKeyStates();
if ((key & UP_PRESSED) != 0) textBox.scrollUp();
if ((key & DOWN_PRESSED) != 0) textBox.scrollDown();
// 重繪
Graphics g = getGraphics();
g.setColor(0xFFFFFF); // 白底
g.fillRect(0, 0, getWidth(), getHeight());
textBox.paint(g);
flushGraphics();
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
}
}
}
| 優化點 | 說明 |
|---|---|
| 快取換行結果 | 文字不變時避免重複計算,只在 setText() 時重算 |
| 預測量字元寬度 | 建立 char[] 寬度查找表,避免重複呼叫 stringWidth() |
| 只繪製可見區域 | 配合 visibleStartLine 只畫螢幕內的行 |
| 使用 StringBuilder | 避免字串拼接產生大量暫時物件 |
| 釋放參考 | 不再需要的 wrappedLines 設為 null 協助 GC |
強制斷字:在演算法中檢測單字長度 > maxWidth 時,按字元強制斷行,或以連字號 "-" 斷開。
使用等寬字型 (Font.FACE_MONOSPACE) 可簡化對齊,但比例字型 (FACE_PROPORTIONAL) 閱讀體驗更好,需接受字元寬度不一的複雜度。
Java ME 文字自動換行雖需自行實作,但掌握核心演算法 (逐字測量、寬度判斷、禁則處理) 並封裝為可復用元件,即可在資源受限裝置上呈現優質閱讀體驗。關鍵在於:快取計算結果、只繪製可見區域、善用 StringBuilder。