低階 UI (Canvas) 繪製大段文字(遊戲說明、RPG 對話)時,必須自行處理換行排版。本文從簡單定寬切割,演進到支援多語言、非等寬字體、單詞不拆分的完整解決方案。
final int CHARS_PER_LINE = 7; // 實測一行容納字數
final String[] helpText = {
"年份不詳的一個", "時代中,妖與人", "類都存在於世界",
"上,並基礎為對", "立狀態,但是不", "消除有相處一起",
"的可能,由於人", "類基礎已經接收", "世界上有妖的事",
"實了。"
};
// 繪製
for (int i = 0; i < helpText.length; i++) {
g.drawString(helpText[i], 5, 5 + 20*i, Graphics.TOP | Graphics.LEFT);
}
缺點:修改文字需重新手工切割;不同手機螢幕寬度/字體大小導致顯示異常(如 K700 一行只容 6 字)。
final int CHAR_LIMIT = 6;
private Vector autoWrap(String text) {
Vector lines = new Vector();
while (text.length() > 0) {
if (text.length() > CHAR_LIMIT) {
lines.addElement(text.substring(0, CHAR_LIMIT));
text = text.substring(CHAR_LIMIT);
} else {
lines.addElement(text);
text = "";
}
}
return lines;
}
// 使用:將長字串拆行
String fullText = "在年份不詳的一" + "個時代中,妖" + "與人類都存在" + ...;
Vector lines = autoWrap(fullText);
for (int i = 0; i < lines.size(); i++) {
g.drawString((String)lines.elementAt(i), 5, 5+20*i, Graphics.TOP|Graphics.LEFT);
}
優點:文字修改只需改長字串,自動重新排版。缺點:不支援英文等非等寬字體,單詞會被強制拆分。
核心 API:Font.stringWidth(String) 回傳字串實際繪製寬度(像素)。
stringWidth 為依據,二分搜尋或線性嘗試找出「最長不超過行寬」的切割點\n 強制換行
/**
* 智慧自動換行
* @param source 待換行文字
* @param font 繪製字體
* @param maxWidth 行最大像素寬度
* @param delimiters 分隔符字串(如 " ,.?!"),用於單詞邊界判斷
* @return 行字串 Vector
*/
public Vector smartWrap(String source, Font font, int maxWidth, String delimiters) {
Vector lines = new Vector();
String text = source;
int lastLen = 0;
while (text.length() > 0) {
// 1. 檢查硬性換行
int nl = text.indexOf('\n');
if (nl >= 0 && font.stringWidth(text.substring(0, nl)) <= maxWidth) {
lines.addElement(text.substring(0, nl));
text = text.substring(nl + 1);
lastLen = 0;
continue;
}
// 2. 估算搜尋範圍
int tryLen = (lastLen > 0 && lastLen < text.length()) ? lastLen : text.length();
int step = font.stringWidth(text.substring(0, tryLen)) > maxWidth ? -1 : 1;
// 3. 尋找最長不超寬的切點
while (tryLen > 0 && tryLen <= text.length()) {
int w = font.stringWidth(text.substring(0, tryLen));
if (w <= maxWidth && (tryLen == text.length() ||
font.stringWidth(text.substring(0, tryLen + 1)) > maxWidth)) {
break; // 找到最佳長度
}
tryLen += step;
}
if (tryLen <= 0) tryLen = 1; // 保底:至少放一字元
// 4. 向前回溯至分隔符(避免拆分單詞)
if (tryLen < text.length() && delimiters.length() > 0) {
int back = tryLen;
while (back > 0 && delimiters.indexOf(text.charAt(back - 1)) == -1) {
back--;
}
if (back > 0) tryLen = back; // 找到分隔符
}
// 5. 截取該行
lines.addElement(text.substring(0, tryLen));
lastLen = tryLen;
text = text.substring(tryLen);
// 跳過行首分隔符
if (text.length() > 0 && delimiters.indexOf(text.charAt(0)) != -1) {
text = text.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, " ,.?!");
for (int i = 0; i < lines.size(); i++) {
g.drawString((String)lines.elementAt(i), 5, 5 + 20*i, Graphics.TOP | Graphics.LEFT);
}
| 特性 | 階段一 | 階段二 | 階段三 |
|---|---|---|---|
| 修改文字便利性 | 差(需手工切) | 好 | 好 |
| 跨裝置適應性 | 差 | 差 | 優(像素級) |
| 英文/非等寬支援 | 無 | 無(強制拆字) | 完美(單詞不拆) |
| 多語言混排 | 無 | 無 | 支援 |
調用 smartWrap 時使用的 Font,必須與實際 g.drawString 時設定的 Font 完全一致,否則像素寬度計算會偏差,導致換行錯位。這是最常見的坑!
這套方案已在多款商業 J2ME 遊戲驗證,能完美支援中英日韓混排、不同螢幕解析度、動態字體大小調整,是低階 UI 文字排版的終極解法。