首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2ME

J2ME 低級 UI 文本自動換行完整方案:從定寬斷行到雙語智能排版

     編輯:J2ME

問題背景

在 GameCanvas 等低級 UI 中顯示大段文本 (遊戲幫助、RPG 對話、任務描述) 時,**自動換行排版**是必須解決的痛點。高級 UI (Form/StringItem) 雖內建換行,但遊戲往往強制使用低級 UI 以控制渲染時機、特效、動畫。本文演進式展示從「硬編碼斷行」到「雙語智能換行」的完整解決方案。

階段 1:定寬字符硬編碼斷行 (僅適用定寬中文)


// 痛點:每行固定 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 字) → 溢出;文案微調需全量重排;英文比例字體完全失效。

階段 2:定寬字符自動斷行函數 (解決機型適配、文案微調)


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 常量。

❌ **仍無法處理英文比例字體**:單詞被拆分、行寬不均、視覺極差。

階段 3:雙語智能換行 — 基於 Font.stringWidth 精準測量

核心算法原理

  1. 以 Font.stringWidth(str) 精確測量像素寬度
  2. 貪心擴展:在不超過最大寬度前提下,盡可能多納入字符
  3. 英文單詞保護:遇空格/標點 (" ,.?!") 才允許斷行,單詞內不斷行
  4. 中文逐字檢測:無分隔符,直接按像素寬度截斷
  5. 顯式換行符 (\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緩存換行結果,文本不變時復用

性能優化

  1. 緩存行寬:Hashtable 緩存 stringWidth 結果
  2. 預計算:載入/關卡切換時預先換行,遊戲循環只繪製
  3. 對象池:Vector、String 子串對象池化 (J2ME 字符串不可變,子串共享底層 char[],開銷可接受)
  4. 位圖字體替代:極致性能用自定義 BitmapFont,自實現 stringWidth (查表累加字符寬度),避免系統字體調用開銷

總結

從硬編碼數組 → 定寬自動斷行 → **雙語智能換行**,三階段演進解決了:

  • 通用性:自適應任意屏幕寬度、字體大小、語言混排
  • 維護性:文案修改零代碼改動,策劃可直接編輯文本文件
  • 視覺質量:英文單詞完整、中文逐字緊湊、支持顯式換行排版

核心關鍵:**Font.stringWidth() 精準測量 + 貪心邊界搜索 + 分隔符回退保護**。僅 60 行代碼,徹底消除「文案改一字、程序改半天」的低級勞動。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved