驗證 Flutter 多語言 UI 時,發現「300 BPM」被拆成兩行

在 SANGE 節拍器中,組合 12 種語言、6 種畫面尺寸與 3 段文字倍率進行顯示測試。記錄因「300 BPM」換行與過長錯誤訊息而進行的 UI 修正,以及自動測試檢查項目的調整。

發布 更新
  • 多語言支援
  • Flutter
  • UI 測試
  • 個人發展

我在個人開發的「SANGE 節拍器 – 簡單節奏練習」中,驗證小畫面與放大文字設定下的顯示。

其中發現的問題,是收藏中顯示的「300 BPM」數字被拆成「30」與「0」。文字並沒有超出畫面,但作為速度數值,這樣的顯示仍難以閱讀。

沒有版面配置例外,以及能讀取必要資訊,兩者都需要分別確認。

本文整理透過 12 種語言的 Widget 測試及 Android 模擬器畫面檢查,發現了哪些顯示問題,又如何反映至修正與測試中。

文字倍率 130% 的首頁下方畫面總覽,包含法文、巴西葡萄牙文、印尼文、泰文、越南文與日文
將修正後的 6 種語言畫面並列。上排由左至右為法文、巴西葡萄牙文、印尼文,下排由左至右為泰文、越南文、日文。收藏中的德文名稱是為了測試長名稱而在各語言共用的測試資料。

日文放得下,不代表相同配置能通用

節拍器畫面除了速度數字,也顯示拍數、節奏名稱、音量及收藏名稱等資訊。儲存畫面還需要輸入欄位、錯誤說明,以及儲存、取消按鈕。

即使日文檢查時放得下,翻譯後的標籤長度仍會改變。例如日文「12拍」與德文「12 Schläge」,按鈕所需的寬度就不同。

進一步放大文字後,原本橫向排列的元素就可能放不下。因此決定不只切換語言,而是組合畫面尺寸、文字倍率與顯示畫面來檢查。

以 12 種語言、6 種尺寸及 3 段文字倍率進行自動驗證

Widget 測試組合了下列條件。Widget 測試是在測試環境中繪製 Flutter 畫面元件並加以確認的方法。

檢查面向條件
語言日文、英文、西班牙文、巴西葡萄牙文、德文、法文、韓文、印尼文、越南文、泰文、繁體中文、義大利文
邏輯尺寸・直向320×568、360×640、360×800、393×873、412×915、432×960
文字倍率100%・130%・200%
畫面首頁、拍數、節奏細分、設定、語言選擇、儲存、收藏管理、收藏清單
12 種語言 × 6 種尺寸 × 3 種文字倍率 × 8 個畫面 = 1,728 種條件

測試資料設定為 300 BPM、12 拍、十六分音符、音量 100%,以及 3 筆長名稱的收藏。儲存畫面也顯示了名稱重複時的錯誤訊息。

文字倍率透過 TextScaler.linear 指定。這次的 100%・130%・200% 是線性放大的測試條件,並未完整重現實機 OS 設定的所有文字放大行為。

也在 Android 模擬器上收集圖片,涵蓋德文、法文、巴西葡萄牙文、印尼文、泰文、越南文與日文共 7 種語言。並非窮舉所有尺寸、倍率與語言組合;以德文檢查畫面尺寸和文字倍率,其餘 6 種語言則以 360×640 設定、130% 進行確認。

模擬器中,系統列會占用部分空間,使應用程式可使用的區域變小。除了設定尺寸,也記錄繪製時的邏輯尺寸及 SafeArea 內的尺寸。

依文字所需空間調整拍數按鈕欄數

最先發現的問題,是速度說明文字與拍數單位超出右側。Android 模擬器也偵測到 RenderFlex overflow,因此需要重新調整版面配置。

拍數選擇按鈕原本也有固定 4 欄的配置,但翻譯標籤變長或文字倍率提高後,相同欄數就會寬度不足。

因此改用 TextPainter 測量翻譯後的標籤,根據包含文字倍率在內的所需寬度決定欄數。空間較窄時便減少欄數,改為垂直排列。

修正後畫面:顯示設定 320×568、德文 100%,1~12 拍的選擇按鈕排列成 2 欄
修正後的 100% 顯示。在此條件下,12 個按鈕可以排成 2 欄。
顯示設定 320×568、德文 200%,捲動至下方的拍數選擇畫面。7~12 拍按鈕排列成 1 欄
相同顯示尺寸在 200% 時改為 1 欄,並捲動確認至 12 拍按鈕及下方說明。與上圖的文字倍率及捲動位置不同,兩張均為修正後畫面。

首頁也調整了將較長節奏名稱擠進狹窄 2 欄的配置,必要時讓拍數與節奏垂直排列。對於首頁 10~12 拍的數字被拆開的問題,則讓整組拍子顯示能折至多行來處理。

「300」被拆行,也不一定會產生例外

檢查圖片時,發現了顯示設定 320×568、文字倍率 200% 的收藏卡片問題。

卡片仍維持 3 欄排列,因此在「300」中間換行。即使文字仍在顯示區域內,數字若被拆成「30」與「0」,速度數值就會變得難以閱讀。

只檢查版面配置例外,無法偵測到這個問題。

修正時,根據文字倍率調整收藏卡片的欄數。本次的小畫面 200% 顯示改為 1 欄,避免速度數字在中間拆行。

修正後畫面:顯示設定 320×568、德文 200%,首頁下方的收藏排列成 1 欄,300 BPM 的數字不再拆行
修正後的收藏顯示。長名稱末尾會省略,速度數字則不拆行。由於未保留此條件的修正前圖片,這不是同條件下修正前後的並列比較。

修正的同時,也在自動測試加入「三位數 BPM 應在同一行內」的檢查。以下為該檢查部分的程式碼摘錄。

for (var card = 0; card < 3; card++) {
  final paragraph = tester.renderObject<RenderParagraph>(
    find.text('300  BPM').at(card),
  );
  expect(
    paragraph.getBoxesForSelection(
      const TextSelection(baseOffset: 0, extentOffset: 3),
    ),
    hasLength(1),
    reason:
        '$locale/$size/$scale favorite BPM must stay on one line',
  );
}

檢查「300」字元範圍的繪製框是否只有一個。實際測試中,確認了 3 筆收藏各自的 BPM,以及首頁 1~12 的拍號。

這個方法針對的是本次以相同字型繪製的數字,並不是用來通用判斷混合不同語言或字型之文字換行的方法。

依據要呈現的資訊,將檢查項目具體化。 這次的收穫,是將從圖片發現的問題回饋至自動測試,讓下次修改時也能偵測到。

能否讀到錯誤說明並到達儲存按鈕

儲存畫面也重新檢視了錯誤訊息的行數限制。

原本最多限制 3 行,但在窄畫面放大文字時,較長的翻譯可能無法完整顯示。因此移除行數上限,讓使用者能透過捲動閱讀完整說明。

在窄畫面或大字設定下,將儲存、取消按鈕改為垂直排列。對話框標題與關閉按鈕,以及設定名稱與目前選項值,也改成能確保文字所需寬度的配置。

顯示設定 320×568、德文 200%,儲存畫面捲動至下方,顯示完整的名稱重複錯誤訊息及垂直排列的取消、儲存按鈕
名稱重複時的錯誤說明,以及取消、儲存按鈕。這是捲動後的畫面。

也加入輸入中的檢查。Widget 測試設定了相當於鍵盤的 280 邏輯像素底部留白,確認捲動後儲存按鈕能顯示於該區域上方。同時檢查錯誤訊息未因行數限制而遭截斷。

在 Android 模擬器上,實際讓輸入欄位取得焦點,檢查鍵盤顯示時的配置。不過,這時收集的 PNG 僅擷取 Flutter 畫面,並未包含 OS 鍵盤本身。因此將一般顯示與輸入中的圖片分開記錄。

修正後的結果與已確認範圍

2026 年 10 月 30 日的驗證結果如下。

專案結果
顯示 Widget 測試1,728 種條件均無版面配置例外。相關畫面的數字不拆行、錯誤訊息不截斷及儲存按鈕可達性檢查也全數通過
整體 Flutter 測試121 項通過
靜態分析無問題
Android 模擬器的畫面條件280 種條件。每次執行的 layoutErrors 均為 0 筆
收集的 PNG478 張,包含捲動後及輸入中的圖片

1,728 是顯示條件的組合數,121 則是整體 Flutter 測試的執行項目數。 由於測試內會反覆檢查多種顯示條件,因此將兩個數字分開記錄。

478 是收集到的 PNG 張數,並未逐張圖片給予人工目視通過或不通過的判定,而是挑選主要畫面檢查文字缺漏、重疊、數字拆行及捲動後的顯示。在已檢查的泰文與越南文主要畫面中,也未看到缺字方框或文字重疊。

模擬器除了使用 6 種合成尺寸,也套用了 Galaxy A16 5G、A06 4G、S24+、S23、Redmi 14C 4G 與 Pixel 9 共 6 種顯示設定。雖準備了 30 種機型設定,本次實際執行的是其中 6 種。

這些設定是在同一個 Android 14 模擬器上套用解析度與驗證用的近似密度,並未重現各廠商專屬 OS、字型或鏡頭挖孔,也未進行 30 款機型的實機驗證。Flutter 測試在 Windows 上執行,本次也未執行 Linux 用的逐像素一致 golden 比對。

本次記錄針對顯示驗證。畫面繪製、輸入欄位焦點與捲動的檢查結果,並不代表已驗證音訊播放或儲存處理成功。

下次 UI 修改也要確認的事

透過這次驗證,具體整理出希望保留在顯示測試中的項目。

  • 速度、拍號等數字不在中間拆行。
  • 讓錯誤說明可以完整閱讀。
  • 放大文字或輸入中,仍應能到達必要的操作按鈕。

依文字大小與翻譯調整欄數,或使用垂直排列與捲動,確保必要資訊與操作所需的空間。

透過自動測試反覆檢查各項條件,再將實際繪製圖片中發現的問題加入檢查項目。保留這個流程後,下次修改 UI 時,也能確認這次發現的問題是否再次出現。

← 前往開發部落格文章列表