課程: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 的支援其實相當有限:
- 數值支援度不一:很多 Android 系統字體只認識
400(normal) 和700(bold)。如果你設定了500或600,Android 往往會直接回退到400。 - 解決之道:如果你需要精確的字重控制(例如 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) vsshadowColor(iOS Only)。
backfaceVisibility在某些平台上行為不一致。- 對策:如果一個屬性只在單一平台生效,你必須尋找對應平台的等價屬性(如用
elevation去補足 Android 的陰影)。
第四步:隔離實驗 (Isolation Layer) —— 排除變數與解決方案
當你確定了問題點,接下來是修復。
- 邏輯分支:使用
Platform.select()針對特定平台給予不同的值。 - 架構分流:如果一個元件的渲染邏輯在兩邊差異巨大,不要在一個檔案裡寫滿
if (isIOS),應該直接使用副檔名分流:MyButton.ios.tsx
MyButton.android.tsx- Metro 打包器會根據當前平台自動選擇正確的檔案。
建立防禦性的開發心態
跨平台開發最忌諱的是「在 iOS 上開發完,最後一刻才去開 Android 模擬器」。這會導致差異累積到無法收拾的地步。
我的建議是:
- 同步開發:開發每一個 UI 元件時,左右兩邊各開一個模擬器。每改幾行代碼,就兩邊都看一眼。
- 封裝基礎元件:不要直接在專案各處使用
Text或View。建立一個你自己的AppText元件,把includeFontPadding: false這種通用的避雷邏輯封裝進去。 - 接受差異:完美的 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或副檔名分流解決問題。 - 開發心態:防禦性開發(同步測試雙平台)與元件化封裝(集中管理平台補償邏輯)是維持專案健康的長久之計。