課程:RN 跨平台開發基礎 第 9 堂:跨平台差異根源
鍵盤與觸控互動
在上一節中,我們處理了螢幕邊緣那些「靜態」的邊界,例如劉海與手勢列。但在行動裝置開發中,最令人頭痛的往往是那種「動態」侵入螢幕的邊界——鍵盤。當使用者點擊輸入框,鍵盤從底部彈起時,它會直接遮蓋住你的 UI,甚至把原本正在輸入的文字框擋得死死的。
這不僅是排版問題,更是 iOS 與 Android 在作業系統層級處理邏輯的根本差異。接著,我們還會深入探討「觸控」,了解為什麼 React Native 團隊放棄了舊有的 TouchableOpacity,轉而推崇功能更強大的 Pressable。
鍵盤行為:當 UI 遇到「不速之客」
在網頁開發中,鍵盤彈起通常會自動縮短視窗高度,但在原生開發(Native)中,兩個平台的哲學完全不同:
1. iOS:鍵盤是一個「覆蓋層」
在 iOS 中,系統預設鍵盤是直接「蓋」在原本的視圖(View)之上的。系統不會主動幫你把內容往上推。這意味著如果你有一個按鈕在螢幕底部,鍵盤彈起後,那個按鈕就會從視覺上消失,藏在鍵盤後面。身為開發者,你必須「手動」去監聽鍵盤高度,並自己把 UI 抬高。
2. Android:系統主動介入
Android 預設會嘗試處理鍵盤遮擋問題。它通常會透過「推擠」整個視窗(Window)或「縮放」視窗高度來確保焦點元素可見。然而,這種自動行為在 React Native 的複雜 Flexbox 佈局中,往往會導致意想不到的排版崩壞。
KeyboardAvoidingView 的核心邏輯
為了統一這兩種行為,React Native 提供了 KeyboardAvoidingView。它的工作原理很簡單:當鍵盤彈起時,它會根據鍵盤的高度,自動調整自身的 padding 或 height。
最關鍵的參數是 behavior,它有三種模式:
**padding**** (推薦 iOS 使用):** 當鍵盤出現時,元件會在底部增加一個等於鍵盤高度的內邊距。這是目前處理 iOS 佈局最穩健的方式,因為它不會改變視圖本身的尺寸計算,只是把內容往上頂。**height**** (推薦 Android 使用):** 它會直接改變元件的高度。在 Android 上,如果系統沒有自動處理好縮放,這個設定通常能派上用場。**position**: 它會像絕對定位一樣移動整個視圖。這在簡單的登入頁面或許有用,但在複雜的列表或分層 UI 中,經常會導致頂部組件被推出螢幕外,因此較少使用。
實戰建議:Platform.select 的妙用
你經常會看到經驗豐富的開發者這樣寫:
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
keyboardVerticalOffset={Platform.select({ ios: 0, android: 20 })}
>
{/* 你的輸入框與內容 */}
</KeyboardAvoidingView>
為什麼需要 **keyboardVerticalOffset**?
這是另一個大坑。如果你的頁面頂部有 Header(例如使用 React Navigation),鍵盤高度的計算可能會出現偏差。keyboardVerticalOffset 允許你補償這段誤差。通常 offset 的數值會等於 Header 的高度。
Android 特有的隱藏設定:windowSoftInputMode
如果你發現 Android 的鍵盤行為怎麼調都不對勁,那可能不是 React Native 程式碼的問題,而是 AndroidManifest.xml 的預設值在搞鬼。
在 Android 中,有兩個主要的設定模式:
**adjustPan**** (平移模式):** 系統不會調整視窗大小,而是直接把整個視窗往上移,直到輸入框露出來。這會導致 Header 被擠出螢幕外。**adjustResize**** (調整大小模式):** 這是現代 APP 的主流做法。系統會縮小可用空間,讓你的 Flexbox 重新佈局。
在 Expo 專案中,你可以透過 app.json 來控制這個行為:
"android": {
"softwareKeyboardLayoutMode": "adjustResize"
}
通常我們建議使用 adjustResize,因為它能讓 KeyboardAvoidingView 更精準地與 RN 的佈局系統協作。
觸控互動:從「堪用」到「精準」
處理完鍵盤後,我們來談談使用者的手指。在 React Native 的發展史中,觸控元件經歷了一次重大的典範轉移。
演進史:為什麼舊的 Touchable 系列不夠好?
在早期,我們習慣使用 TouchableOpacity、TouchableHighlight 或 TouchableWithoutFeedback。這些元件雖然簡單好用,但有幾個致命缺點:
- 行為固定:
TouchableOpacity強制綁定了透明度變化,TouchableHighlight強制綁定了底色變化。如果你想要一個「按下去縮小 5%」的特效,這幾個元件都很難優化。 - 效能限制: 它們的內部實作較為老舊,難以精確處理複雜的觸控狀態(例如:手指滑入、滑出、長按的瞬間反饋)。
- 平台不一致: 它們很難實現 Android 系統那種標誌性的「漣漪效果」(Ripple Effect)。
現代首選:Pressable
Pressable 是 React Native 推出用來取代所有 Touchable 元件的「未來方案」。它不再預設任何動畫,而是提供了一個強大的狀態監聽機制。
Pressable 最酷的地方在於,它的 style 與 children 可以接受一個「函數」。
範例:實作跨平台一致的按鈕
<Pressable
// Android 特有的漣漪效果
android_ripple={{ color: '#ccc', borderless: false }}
// 利用函數動態決定樣式
style={({ pressed }) => [
{
backgroundColor: pressed ? '#005bb5' : '#007aff',
padding: 15,
borderRadius: 8,
opacity: Platform.OS === 'ios' && pressed ? 0.7 : 1, // iOS 手動補償透明度
},
]}
>
{({ pressed }) => (
<Text style={{ color: 'white' }}>
{pressed ? '正在點擊...' : '送出評論'}
</Text>
)}
</Pressable>
為什麼你該換成 Pressable?
- Hit Slop (擴大觸控區域): 這是內容類 APP 的救星。如果你的關閉按鈕(X)很小,你可以設定
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }},讓使用者的手指即便沒精準點到 X,也能觸發事件,這對 UX 提升極大。 - 精細的狀態控制: 除了
pressed,它還支援hovered(滑鼠經過,對 iPad/Web 有用) 和focused(焦點進入)。 - 預測性行為:
Pressable對於「手指按住後滑離按鈕範圍」的處理比傳統元件更加穩定。
綜合應用場景:媒體 APP 的「評論輸入區」
讓我們把這兩個概念結合在一起。想像你正在開發一個影音 APP 的評論功能。底端有一個輸入框(TextInput)和一個發送按鈕(Pressable)。
這是一個常見的「地雷區」,因為:
- 鍵盤彈起時,輸入框必須被頂上去。
- 輸入框頂上去後,不能蓋住最後一則評論。
- 發送按鈕在 Android 上要有漣漪回饋,在 iOS 上要有透明度回饋。
結構拆解:
- 最外層:
KeyboardAvoidingView。設定behavior="padding"。 - 中間層:
FlatList或ScrollView。記得加上keyboardShouldPersistTaps="handled"。這是一個關鍵設定,如果不加,當鍵盤彈起時,使用者點擊列表想收回鍵盤會失效。 - 底層: 一個
View包裹著TextInput與Pressable按鈕。
關於 Pressable 的 Android 漣漪小技巧:
如果你想要漣漪效果超出按鈕邊界(例如一個圓形的 Icon 按鈕),可以設定 android_ripple={{ borderless: true }}。但要注意,這需要父容器留有足夠空間,否則漣漪會被裁切。
橋接:通往字型與偵錯的最後一哩路
處理完「邊界」與「觸控」後,你已經掌握了行動端互動最核心的物理行為。鍵盤的彈起與手指的點擊,是使用者感知 APP 「流暢度」與「原生感」最直接的管道。
然而,即便佈局正確、觸控反應靈敏,如果文字在 iOS 上看起來很完美,到了 Android 卻發生行高(Line Height)偏移,或者字體突然變得很擠,整個 APP 的「質感」依然會大打折扣。這不僅是視覺美觀問題,更涉及到作業系統對字型渲染的底層差異。
在下一個部分,我們將進入跨平台開發的最後一塊拼圖:字型渲染的陷阱。我們將解釋為什麼相同的 fontSize 在不同平台看起來不一樣大,並總結出一套系統性的偵錯框架,讓你在未來遇到任何詭異的平台差異時,都能不再依賴通靈,而是有一套科學的思考流程。