在 GameCanvas 等低級 UI 中顯示大段文本 (遊戲幫助、RPG 對話、任務描述) 時,**自動換行排版**是必須解決的痛點。高級 UI (Form/StringItem) 雖內建換行,但遊戲往往強制使用低級 UI 以控制渲染時機、特效、動畫。本文演進式展示從「硬編碼斷行」到「雙語智能換行」的完整解決方案。
// 痛點:每行固定 7 字,手工分行
final String[] lines = {
"年份不詳的一個",
"時代中,妖與人",
"類都存在於世界",
"上,並基本為對",
"立狀態,但是不",
"排除有相處一起",
"的可能,因為人",
"類基本已經接受",
"世界上有妖的事",
"實了。"
};
// 繪製
for (int i = 0; i < lines.length; i++) {
g.drawString(lines[i], 5, 5 + 20*i, Graphics.TOP | Graphics.LEFT);
}
❌ **致命缺陷**:機型字體寬度不同 (K700 每行只容 6 字) → 溢出;文案微調需全量重排;英文比例字體完全失效。
final int CHAR_PER_LINE = 6; // 取最窄機型實測值
public Vector autoWrapFixedWidth(String text) {
Vector v = new Vector();
int len = text.length();
for (int i = 0; i < len; i += CHAR_PER_LINE) {
int end = Math.min(i + CHAR_PER_LINE, len);
v.addElement(text.substring(i, end));
}
return v;
}
// 使用
String fullText = "在年份不詳的一個時代中,妖與人類都存在於世界上...";
Vector lines = autoWrapFixedWidth(fullText);
for (int i = 0; i < lines.size(); i++) {
g.drawString((String)lines.elementAt(i), 5, 5 + 20*i, Graphics.TOP | Graphics.LEFT);
}
✅ 解決:文案修改只改全量字符串,機型適配只改 CHAR_PER_LINE 常量。
❌ **仍無法處理英文比例字體**:單詞被拆分、行寬不均、視覺極差。
Font.stringWidth(str) 精確測量像素寬度" ,.?!") 才允許斷行,單詞內不斷行\n) 支持:強制換行
/**
* 智能文本換行,支持中英混排、單詞保護、顯式換行
* @param source 待換行文本
* @param font 繪製使用的字體 (必須與實際 drawString 字體一致!)
* @param maxWidth 最大行寬 (像素),通常 getWidth() - 左右邊距
* @param delimiters 英文單詞分隔符,如 " ,.?!\t\n"
* @return 行字符串 Vector
*/
public Vector smartWrap(String source, Font font, int maxWidth, String delimiters) {
Vector lines = new Vector();
if (source == null || source.length() == 0) return lines;
String remaining = source;
int lastFitLen = 1; // 上一行成功長度,作為下一行起始嘗試
while (remaining.length() > 0) {
// 1. 處理顯式換行符
int nl = remaining.indexOf('\n');
if (nl >= 0) {
// 檢查 \n 前內容是否超寬
if (font.stringWidth(remaining.substring(0, nl)) > maxWidth) {
nl = -1; // 視為無換行符,按寬度斷行
}
}
int cut = -1; // 斷行位置
if (nl == -1) { // 需按寬度斷行
// 2. 確定搜索起點:不超過剩餘長度,不小於上行長度
int start = Math.min(lastFitLen, remaining.length());
// 3. 判斷擴展方向:先測 start 寬度
int step = font.stringWidth(remaining.substring(0, start)) > maxWidth ? -1 : 1;
// 4. 微調到「剛好放下」的最大長度
if (start < remaining.length()) {
while (true) {
int wCur = font.stringWidth(remaining.substring(0, start));
int wNext = (start + 1 <= remaining.length())
? font.stringWidth(remaining.substring(0, start + 1))
: maxWidth + 1;
if (wCur <= maxWidth && wNext > maxWidth) break; // 找到邊界
if (start == remaining.length()) break; // 已到底
start += step;
}
}
cut = start;
// 5. 英文單詞保護:向回退避至分隔符
if (delimiters != null && delimiters.length() > 0 && cut < remaining.length()) {
int backup = cut;
while (backup > 0 && delimiters.indexOf(remaining.charAt(backup - 1)) == -1) {
backup--;
}
if (backup > 0) cut = backup; // 找到分隔符
// else 保持 cut,單詞過長只能硬斷
}
} else {
cut = nl; // 顯式換行
}
// 6. 截取本行
lines.addElement(remaining.substring(0, cut));
lastFitLen = cut; // 記錄供下行參考
// 7. 推進剩餘文本
if (cut == remaining.length()) {
remaining = "";
} else {
remaining = remaining.substring(cut);
// 跳過換行符本身
if (remaining.startsWith("\n")) remaining = remaining.substring(1);
}
}
return lines;
}
// 繪製前設置字體 (關鍵:測量字體 = 繪製字體)
Font font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
g.setFont(font);
// 智能換行
Vector lines = smartWrap(
fullText,
font,
getWidth() - 10, // 留 5px 左右邊距
" ,.?!\t" // 英文分隔符
);
// 繪製
int lineHeight = font.getHeight() + 2; // 行間距 2px
for (int i = 0; i < lines.size(); i++) {
g.drawString((String)lines.elementAt(i), 5, 5 + i * lineHeight,
Graphics.TOP | Graphics.LEFT);
}
| 坑點 | 後果 | 規避 |
|---|---|---|
| 測量字體 ≠ 繪製字體 | 行寬錯誤、溢出或過疏 | 同一 Font 實例 用於 stringWidth() 和 drawString() |
| maxWidth 過大 | 超出屏幕、繪製無效區 | getWidth() - padding*2 |
| delimiters 缺失 | 英文單詞中間斷行 | 至少包含 " " (空格) |
| 中文標點不換行 | 行尾出現「。」等 | delimiters 加入中文標點 "。,、;:」 |
| 超長單詞/連續中文 | 單行超寬無法斷 | 允許硬斷 (backup==0 時不回退),或插入零寬空格 |
| Vector 內存壓力 | 長文本頻繁 GC | 緩存換行結果,文本不變時復用 |
Hashtable 緩存 stringWidth 結果Vector、String 子串對象池化 (J2ME 字符串不可變,子串共享底層 char[],開銷可接受)stringWidth (查表累加字符寬度),避免系統字體調用開銷從硬編碼數組 → 定寬自動斷行 → **雙語智能換行**,三階段演進解決了:
核心關鍵:**Font.stringWidth() 精準測量 + 貪心邊界搜索 + 分隔符回退保護**。僅 60 行代碼,徹底消除「文案改一字、程序改半天」的低級勞動。