跳至主要内容

課程:RN 跨平台開發基礎 第 9 堂:跨平台差異根源

字型渲染與偵錯框架

在上一節中,我們處理了鍵盤與觸控這類「動態」的互動差異。這些行為雖然棘手,但通常具有明顯的因果關係。然而,當我們進入「字型渲染」的世界時,問題會變得更加隱晦且令人沮喪。你可能在 iOS 上調出了一個完美的 UI,但在 Android 上卻發現文字偏上、文字被莫名截斷,或是加粗效果完全無效。

這種「靜態細節」的差異,往往是區分一個「堪用 APP」與「精品 APP」的關鍵。本節作為 Topic 5 的總結,我們不僅要拆解字型渲染的底層陷阱,更要建立一套系統性的偵錯框架,讓你在未來面對任何未知的跨平台 Bug 時,都能擁有一套冷靜診斷的方法論。

為什麼同樣的字體,在兩邊看起來不一樣?

在 Web 開發中,我們習慣了瀏覽器會盡可能抹平系統差異。但在 React Native 中,我們是直接呼叫原生的渲染引擎。這意味著,即使你設定了相同的 fontSize,最終呈現的效果也會受到作業系統預設行為的深度影響。

系統預設字體的哲學差異

iOS 預設使用 San Francisco (SF),而 Android 預設使用 Roboto。這兩套字體在設計之初的「字幹寬度」、「字間距」以及「基準線(Baseline)」定義就完全不同。

  • iOS (SF):設計上非常強調精緻感與垂直空間的平衡,在渲染時通常會給予文字較寬鬆的行間距。
  • Android (Roboto):為了在早期較低解析度的螢幕上維持清晰度,Roboto 的渲染往往更偏向「緊湊」,這導致它在相同的 fontSize 下,視覺感受通常比 iOS 稍微「擠」一點。

這就是為什麼當你使用預設字體開發時,總覺得 Android 的視覺重心稍微偏上的原因。

文字截斷與 numberOfLines

在處理標題或摘要時,我們常用 numberOfLines 配合 ellipsizeMode。但在 Android 上,如果你沒有給予足夠的容器高度(或是設定了過小的 lineHeight),Android 的原生渲染器有時會直接把最後一行切掉,甚至連省略號(...)都不顯示。這是因為 Android 對於文字邊界(Text Bounds)的判定比 iOS 嚴格許多。

垂直居中的頭號戰犯:includeFontPadding

如果你曾在 Android 上嘗試將一段文字在一個圓形或矩形框內水平垂直居中,你一定遇過這個噩夢:儘管 alignItems: 'center'justifyContent: 'center',文字在 Android 上看起來就是會「往下偏移」幾個像素。

這不是 Flexbox 的錯,而是 Android 原生 TextView 的一個歷史遺留屬性:includeFontPadding

什麼是字體填充(Font Padding)?

在 Android 的設計中,為了確保一些特殊的重音符號(如某些歐系語言字母上方的點或鉤)不會被截斷,系統預設會在文字的頂部和底部保留額外的空間。這個空間對於純中文或純英文來說是多餘的,但它會直接破壞垂直置中的邏輯。

解決方案: 在 React Native 的 Text 樣式中,你可以明確關閉它:

const styles = StyleSheet.create({
text: {
fontSize: 16,
// 解決 Android 垂直置中偏移的關鍵
includeFontPadding: false,
// 配合 textAlignVertical 確保在 Android 上的精準控制
textAlignVertical: 'center',
},
});

請注意,includeFontPadding 是一個 Android-only 的屬性。這是一個典型的例子:為了達成「視覺上的一致」,我們必須對特定平台採取不同的樣式策略。

行高與字重:隱藏的渲染陷阱

LineHeight 的計算邏輯

在網頁 CSS 中,line-height: 1.5 是一個比例。但在 React Native 中,lineHeight 必須是一個 絕對數值(像素/點)

  • iOS:會將 lineHeight 均勻地分配在基準線的上下方。
  • Android:處理 lineHeight 的方式較為僵硬,如果 lineHeight 設定得太接近 fontSize,在 Android 上非常容易發生文字頂部被削掉的情況。

實務建議:始終確保 lineHeight 至少比 fontSize 大 20% 到 30%。例如 fontSize: 16,則 lineHeight 建議設定為 20 或以上。

FontWeight 的數字遊戲

在 iOS 上,你可以從 100 設定到 900(視字體支援度而定),每一階都有細微的變化。但在 Android 的原生環境中,對於 fontWeight 的支援其實相當有限:

  1. 數值支援度不一:很多 Android 系統字體只認識 400 (normal) 和 700 (bold)。如果你設定了 500600,Android 往往會直接回退到 400
  2. 解決之道:如果你需要精確的字重控制(例如 Medium 或 Semibold),在 Android 上最穩定的做法是引入自定義字體檔案(如 PingFang-Medium.ttf),並在樣式中直接指定 fontFamily

系統性偵錯框架:遇到平台差異該怎麼辦?

當你的 APP 越做越大,你不可能記住所有的跨平台陷阱。這時候,你需要一套穩定的「思考路徑」來診斷並解決問題。與其盲目地調整數值,不如遵循以下這套 四步驟診斷流程

第一步:確認意圖 (OS Layer) —— 這是平台規範嗎?

在改程式碼之前,先問自己:「這個差異是 Bug,還是平台的預設行為?」

  • 情境:Android 的頁面轉場動畫是從下往上浮現,而 iOS 是從右往左推入。
  • 判斷:這是作業系統的設計語言(Material vs HIG)。
  • 對策:除非產品經理強烈要求兩端 1:1,否則應尊重原生規範。不要試圖把 Android 改成跟 iOS 一模一樣,那樣通常會讓 Android 使用者感到違和。

第二步:檢查容器 (Layout Layer) —— 是佈局限制造成的嗎?

如果確認是視覺 Bug,先檢查外層容器。

  • 檢查清單
    • 是否有父容器設定了 overflow: 'hidden' 導致子元素被截斷?
  • Flexbox 的 alignItems: 'stretch' (RN 預設值) 是否撐開了你不想要的寬度?
  • 是否漏掉了 SafeAreaView 導致 UI 鑽進了劉海區?
  • 技巧:給出問題的元件加上一個鮮豔的背景顏色(如 backgroundColor: 'red'),這是定位佈局問題最快速的方法。

第三步:檢查屬性 (Prop Layer) —— 屬性支援度與平台標記

查閱 React Native 的官方文件,檢查你使用的屬性。

  • 觀察標記:官方文件在屬性旁邊通常會標註 [iOS][Android]
  • 常見案例
    • elevation (Android Only) vs shadowColor (iOS Only)。
  • backfaceVisibility 在某些平台上行為不一致。
  • 對策:如果一個屬性只在單一平台生效,你必須尋找對應平台的等價屬性(如用 elevation 去補足 Android 的陰影)。

第四步:隔離實驗 (Isolation Layer) —— 排除變數與解決方案

當你確定了問題點,接下來是修復。

  • 邏輯分支:使用 Platform.select() 針對特定平台給予不同的值。
  • 架構分流:如果一個元件的渲染邏輯在兩邊差異巨大,不要在一個檔案裡寫滿 if (isIOS),應該直接使用副檔名分流:
    • MyButton.ios.tsx
  • MyButton.android.tsx
  • Metro 打包器會根據當前平台自動選擇正確的檔案。

建立防禦性的開發心態

跨平台開發最忌諱的是「在 iOS 上開發完,最後一刻才去開 Android 模擬器」。這會導致差異累積到無法收拾的地步。

我的建議是:

  1. 同步開發:開發每一個 UI 元件時,左右兩邊各開一個模擬器。每改幾行代碼,就兩邊都看一眼。
  2. 封裝基礎元件:不要直接在專案各處使用 TextView。建立一個你自己的 AppText 元件,把 includeFontPadding: false 這種通用的避雷邏輯封裝進去。
  3. 接受差異:完美的 1:1 往往是代價高昂且不必要的。只要視覺權重一致、操作流暢,微小的間距差異(1-2px)通常是可以接受的。

課程總結:理解差異、尊重原生、系統診斷

在 Topic 5 中,我們走過了一段漫長的旅程。我們從設計語言的哲學出發,理解了為什麼 iOS 追求層次與毛玻璃感,而 Android 追求物理紙張與海拔高度。我們拆解了 SafeArea 的安全區間、StatusBar 的顏色控制、鍵盤彈出的遮擋邏輯,以及剛剛完成的字型渲染細節。

這堂課最重要的價值不在於解決了那幾個特定的 Bug,而在於建立了一個認知:跨平台開發不是要消滅平台差異,而是要「有意識地管理」差異

當你不再害怕看到 Android 上的排版錯亂,而是能冷靜地啟動「四步驟診斷流程」時,你已經從一個只是在寫程式碼的開發者,進階成為一個具備系統架構思維的工程師了。

接下來我們去哪裡?

掌握了 UI 的靜態與動態細節後,我們終於要踏入 APP 開發中最令人興奮的部分:Topic 6:原生功能整合

在接下來的主題中,我們將離開單純的 UI 佈局,深入探討 JS 層如何與手機的原生硬體溝通。我們將學習:

  • 如何在 Expo 環境中優雅地處理相機與圖片選取。
  • 如何規劃一套能同時搞定 iOS APNs 與 Android FCM 的推播通知架構。
  • 最重要的是,如何系統性地處理「權限請求」——這是許多內容類 APP 上架被拒的首要原因。

準備好後,我們就開始探索原生模組的奧祕。

關鍵要點與回顧

  • 字體渲染的核心差異:Android 預設的字體填充(includeFontPadding)是導致垂直置中失效的元兇,必須手動關閉。
  • 行高與字重lineHeight 應始終大於 fontSize 以避免 Android 文字截斷;數值型字重在 Android 上支援有限,關鍵場景應使用自定義字體。
  • 系統性偵錯框架
    • 確認意圖:是否符合平台設計語言?
  • 檢查容器:是否受外層佈局限制?
  • 檢查屬性:屬性是否支援當前平台?
  • 隔離實驗:利用 Platform.select 或副檔名分流解決問題。
  • 開發心態:防禦性開發(同步測試雙平台)與元件化封裝(集中管理平台補償邏輯)是維持專案健康的長久之計。