跳至主要内容

課程:RN 跨平台開發基礎 第 4 堂:樣式的跨平台收尾

跨平台視覺陷阱

想像一下:你在 iOS 模擬器上花了一個下午,精雕細琢了一個內容卡片。那細膩的擴散陰影、完美置中的標題文字、以及優雅的圖片比例,讓你對這個 UI 感到無比自豪。然而,當你滿懷期待地在 Android 實機上打開 APP 時,卻發現陰影消失了,或者變成了一種生硬的黑邊;標題文字稍微偏上,甚至底部被切掉了一點;原本深灰色的圖示顏色也顯得有些怪異。

這種「明明程式碼一樣,看起來卻不一樣」的挫折感,是每個 React Native 開發者都會經歷的洗禮。

這並非 React Native 的 bug,而是因為 iOS 與 Android 在底層渲染引擎、系統字型設計以及 UI 哲學上的本質差異。React Native 的目標是調用原生元件,因此它也繼承了這些原生系統的「個性」。在這一部分,我們將深入拆解陰影、字型與圖片在跨平台開發中最常遇到的視覺陷阱,並學習如何透過「視覺一致性」而非「程式碼一致性」來解決問題。

陰影的兩套邏輯:shadow* 屬性 vs elevation

陰影是 APP 增加層次感最直覺的方式,但在 React Native 中,陰影的處理方式在 iOS 和 Android 上幾乎是完全平行的兩套系統。

iOS:基於 Core Graphics 的自由畫筆

在 iOS 上,React Native 提供的陰影屬性(shadowColor, shadowOffset, shadowOpacity, shadowRadius)是直接對應到 iOS 底層 CALayer 的屬性。這意味著你可以像在設計軟體中一樣,精確控制陰影的 X/Y 偏移、模糊程度(Radius)以及透明度。

這種方式非常靈活,你可以創造出極其柔和、擴散範圍很大的「現代感」陰影。

Android:基於物理海拔的 elevation

Android 的陰影邏輯則完全不同。它遵循 Material Design 的「海拔」(Elevation)概念。在 Android 中,陰影不是「畫」上去的,而是因為該元件在 Z 軸上比背景「高」,所以系統自動計算並投射出陰影。

當你在樣式中寫下 elevation: 5 時,Android 系統會根據這個數值決定陰影的大小和擴散程度。這帶來了幾個嚴重的侷限:

  1. 無法自訂顏色:在舊版的 React Native 中,Android 陰影只能是灰黑色,你無法像 iOS 那樣設定 shadowColor(雖然新版本已開始部分支援,但效果依然受限)。
  2. 無法自訂偏移與模糊度:你不能控制陰影往左偏或往右偏,也不能單獨調整它的模糊範圍。
  3. 強制性的灰階效果:它總是帶著一種 Material Design 特有的硬派感,很難達成 iOS 那種輕盈的空氣感。

預測與發現:如果我把兩套屬性寫在一起會發生什麼?

如果你在一個 View 的樣式中同時寫了 shadow*elevation

const styles = StyleSheet.create({
card: {
backgroundColor: 'white',
// iOS 屬性
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android 屬性
elevation: 5,
}
});

結果是: 它們會各自在所屬的平台上生效,互不干擾。這看起來是個解決方案,但視覺上兩端會非常不統一。iOS 上可能是柔和的淺灰色陰影,Android 上卻是深沈的重陰影。

現代對策:boxShadow 與第三方庫

React Native 在最近的版本中(特別是新架構 Fabric 推出後)開始引入了 boxShadow 屬性,試圖統一這兩者的體驗。然而,在目前的主流開發環境中,如果你追求極致的跨平台視覺一致性,通常有兩個選擇:

  1. 接受差異:遵循各平台的原生慣例,iOS 輕盈,Android 紮實。
  2. 使用 react-native-shadow-2:這是一個非常受歡迎的庫,它不使用原生陰影,而是透過 SVG 繪製陰影。雖然會稍微增加一點點渲染負擔,但它能保證你的陰影在 iOS 和 Android 上長得一模一樣。

字型渲染的陷阱:為什麼文字會「歪掉」?

字型是內容類 APP 的靈魂。你可能認為設定了 fontSizelineHeight 就能高枕無憂,但在 Android 上,文字往往會給你帶來意外驚喜。

lineHeight 的垂直偏移與 Clipping

在網頁開發中,lineHeight 通常會讓文字在行內垂直居中。但在 React Native 的 Android 實作中,lineHeight 的計算邏輯與 iOS 不同,這常導致以下兩個問題:

  1. 文字偏上或偏下:在同樣的 lineHeight 設定下,iOS 的文字通常能完美居中,但 Android 的文字可能會往上靠,導致視覺重心不穩。
  2. 文字被切除 (Clipping):如果你的 lineHeight 設定得太過緊湊,Android 系統有時候會為了節省渲染空間,把文字的頂部或底部(特別是有上升部或下降部的英文字母,如 g, j, p, q, y)切掉。

這背後的原因之一是 Android 字型預設會帶有一個 includeFontPadding。這個屬性會為了容納某些特殊語系的重音符號而預留空間,卻常導致中文或普通英文在垂直居中時失效。

解決方案: 在 Android 的 Text 樣式中,嘗試將 includeFontPadding: false 設為常態,這通常能讓 Android 的文字位置更接近 iOS。

fontWeight 的「數字失蹤案」

在 iOS 上,系統字型(San Francisco)支援非常細緻的 fontWeight,從 '100''900' 幾乎都有對應的粗細變化。

但在 Android 上,預設系統字型(Roboto)通常只內建了 normal (400) 和 bold (700) 兩種主要的粗細。如果你設定了 fontWeight: '600'

  • iOS:會顯示 Semibold 效果。
  • Android:因為找不到對應的 600 字型檔,它可能會直接退回到 normal,或者暴力切換到 bold。這會導致你的 UI 在 Android 上看起來不是太單薄,就是太粗獷。

跨平台字型策略

為了避免這些麻煩,內容類 APP 的最佳實踐通常是:

  • 自訂字型 (Custom Fonts):不要依賴系統字型。透過 Expo 或 RN CLI 載入一套統一的字型檔(如 Noto Sans 或 Open Sans),並在載入時明確定義每個重量對應的字型名稱。例如,在 Android 上直接呼叫 fontFamily: 'Brand-Semibold' 而不是 fontFamily: 'Brand', fontWeight: '600'

圖片樣式與色彩的邊界

圖片展示是媒體 APP 的核心,雖然 Image 元件看起來很單純,但在細節處理上仍有跨平台的差異。

resizeMode 的微小偏差

resizeMode 決定了圖片如何適應容器。雖然 cover, contain, stretch 在兩端定義一致,但在處理圓角 (borderRadius) 時,Android 有時會出現邊緣鋸齒(aliasing)或圖片內容溢出圓角的問題。

在 iOS 上,給 Image 加上 borderRadius 通常就能完美運作;但在 Android 上,有時你需要在外層包裹一個 View 並設定 overflow: 'hidden' 才能確保圖片被正確裁切。

tintColor:圖示開發者的神器

tintColor 是一個非常強大的屬性,它可以將透明 PNG 或向量圖形的非透明部分直接染成指定的顏色。這在處理底部選單列(Tab Bar)的圖示切換時極其好用。

然而,需要注意的陷阱是:

  • iOStintColorImage 元件上表現非常穩定。
  • Android:在某些舊版本的 Android 或特定的圖片格式下,tintColor 可能會失效,或者導致圖片渲染變色異常。
  • 限制tintColor 會覆蓋圖片原本的所有色彩資訊。如果你有一張彩色圖示想在選單中使用,tintColor 會把它變成一個單色的剪影。

總結:視覺一致性並不等於程式碼一致性

在處理這些陷阱時,開發者最容易犯的錯誤就是強求「同一套數值」。例如,為了修復 Android 的文字偏移,你調整了 padding,結果 iOS 反而歪了。

核心心智模型:Platform-Specific Tweaks

我們必須接受一個事實:有時候為了讓兩個平台「看起來」一樣,你必須給它們「不一樣」的參數。

這就是為什麼我們在上一節學習了 Platform.select。當你發現某個視覺效果在兩端不統一時,不要試圖找一個中庸的數值,而是應該果斷拆分:

const styles = StyleSheet.create({
headerText: {
fontSize: 18,
...Platform.select({
ios: {
fontWeight: '600',
lineHeight: 22,
},
android: {
fontWeight: 'bold', // 針對 Android 的字型特性
lineHeight: 24, // 補償 Android 的渲染差異
includeFontPadding: false,
},
}),
},
});

這種做法雖然會增加樣式表的長度,但它能確保你的使用者無論拿起 iPhone 還是 Android 手機,都能獲得你心目中那個「最高質感」的體驗。視覺一致性是品牌專業感的來源,而理解這些底層陷阱,正是你從「能開發 APP」進階到「能開發精緻 APP」的關鍵。

鞏固與銜接

這部分我們深入探討了陰影、字型與圖片的各種「坑」。你已經理解了為什麼同樣的程式碼在不同設備上會產生視覺落差,以及如何利用 Platform 工具來進行精準的微調。

在下一部分中,我們將學習如何將這些瑣碎的微調經驗「系統化」。我們不希望在全專案的每個角落都寫滿 Platform.select。相反地,我們會探討如何建立一套可複用的樣式系統與元件封裝策略。我們將學習如何定義主題 Token,並封裝出像 MyTextMyCard 這樣的共用元件,讓這些惱人的平台差異在底層被優雅地處理掉,而你在開發業務功能時,只需要關注 UI 的語意。