- 原文作者:Spencer Carli
- 譯文出自:掘金翻譯計划
- 譯者:rccoder
- 校對者:atuooo、ZiXYu

在使用 React Native 應用時,一個常見的問題是當你點擊文本輸入框時,鍵盤會彈出並且遮蓋住輸入框。就像這樣:

有幾種方式可以避免這種情況發生。一些方法比較簡單,另一些稍微復雜。一些是可以自定義的,一些是不能自定義的。今天,我將向你展示 3 種不同的方式來避免 React Native 應用中的鍵盤遮擋問題。
文章中所有的代碼都托管在 GitHub 上
KeyboardAvoidingView
最簡單、最容易安裝使用的方法是 KeyboardAvoidingView。這是一個核心組件,同時也非常簡單。
你可以使用這段存在鍵盤覆蓋輸入框問題的 代碼,然后更新它,使輸入框不再被覆蓋。你要做的第一件事是用 KeyboardAvoidView 替換 View,然后給它加一個 behavior 的 prop。查看文檔的話你會發現,他可以接收三個不同的值作為參數 —— height, padding, position。我發現 padding 的表現是最在我意料之內的,所以我將使用它。
import React from 'react'; import { View, TextInput, Image, KeyboardAvoidingView } from 'react-native'; import styles from './styles'; import logo from './logo.png'; const Demo = () => { return ( <KeyboardAvoidingView style={styles.container} behavior="padding" > <Image source={logo} style={styles.logo} /> <TextInput placeholder="Email" style={styles.input} /> <TextInput placeholder="Username" style={styles.input} /> <TextInput placeholder="Password" style={styles.input} /> <TextInput placeholder="Confirm Password" style={styles.input} /> <View style={{ height: 60 }} /> </KeyboardAvoidingView> ); }; export default Demo;
它的表現如下,雖然不是非常完美,但幾乎不需要任何工作量。這在我看來是相當好的。

需要注意的事,在上個實例代碼中的第 30 行,設置了一個高度為 60 的 View。我發現keyboardAvoidingView 對最后一個元素不適用,即使是添加了 padding/margin 屬性也不奏效。所以我添加了一個新的元素去 “撐開” 一些像素。
使用這個方法時,頂部的圖片會被推出到視圖之外。在后面我會告訴你如何解決這個問題。
針對 Android 開發者:我發現這種方法是處理這個問題最好,也是唯一的辦法。在 AndroidManifest.xml 中添加 android:windowSoftInputMode="adjustResize"。操作系統將為你解決大部分的問題,KeyboardAvoidingView 會為你解決剩下的問題。參見 這個。接下的部分可能不適用於你。
Keyboard Aware ScrollView
下一種解決辦法是使用 react-native-keyboard-aware-scroll-view,他會給你很大的沖擊。實際上它使用了 ScrollView 和 ListView 處理所有的事情(取決於你選擇的組件),讓滑動交互變得更加自然。它另外一個優點是它會自動將屏幕滾動到獲得焦點的輸入框處,這會帶來非常流暢的用戶體驗。
它的使用方法同樣非常簡單 —— 只需要替換 基礎代碼 的 View。下面是具體代碼,我會做一些相關的說明:
import React from 'react'; import { View, TextInput, Image } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' import styles from './styles'; import logo from './logo.png'; const Demo = () => { return ( <KeyboardAwareScrollView style={{ backgroundColor: '#4c69a5' }} resetScrollToCoords={{ x: 0, y: 0 }} contentContainerStyle={styles.container} scrollEnabled={false} > <Image source={logo} style={styles.logo} /> <TextInput placeholder="Email" style={styles.input} /> <TextInput placeholder="Username" style={styles.input} /> <TextInput placeholder="Password" style={styles.input} /> <TextInput placeholder="Confirm Password" style={styles.input} /> </KeyboardAwareScrollView> );
首先你需要設置 ScrollView 的 backgroundColor(如果你想使用滾動的話)。接下來你需要告訴默認組件在哪里,當你的鍵盤收起時,界面就會返回到默認的那個位置 —— 如果省略 View 的這個 prop,可能會導致鍵盤在關閉之后界面依舊停留在頂部。

在設置好 resetScrollToCoords 這個 prop 之后你需要設置 contentContainerStyle —— 這本質上會替換掉你之前給 View 設置的樣式。最后一件事是禁止掉從用戶產生的滾動交互。這可能並不是完全適合你的 UI 交互(比如對於用戶需要編輯很多字段的界面),但是在這里,允許用戶滾動沒有任何意義,因為並沒有其它的內容需要用戶來進行滾動操作。
把這些所有的 prop 放到一起就會產生下面的效果,看起來很不錯:

Keyboard Module
這是迄今為止最為手動的方式,但也同時給開發者最大的控制權。你可以使用一些動畫庫來幫助實現之前看到的那種平滑滾動。
React Native 在官方文檔是沒有說 Keyboard Module 可以監聽從設備上產生的鍵盤事件。你使用的事件是 keyboardWillShow 和 keyboardWillHide,來產生一個鍵盤展開的動畫(或者其他信息)。
當 keyboardWillShow 事件產生時,需要設置一個動畫變量到鍵盤的最終高度,並使其與鍵盤彈出滑動時間保持一致。然后你可以用這個動畫變量的值在容器的底部設置 padding,將所有的內容上移。
我會在后面展示具體代碼,先展示一下上面所說的內容會產生的效果:


這次我將修復 UI 中的那個圖片。為此,需要使用動畫變量的值來管理圖片的高度,你可以在彈出鍵盤的同時調整圖片的高度。下面是具體代碼:
import React, { Component } from 'react';
import { View, TextInput, Image, Animated, Keyboard } from 'react-native';
import styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL} from './styles';
import logo from './logo.png';
class Demo extends Component {
constructor(props) {
super(props);
this.keyboardHeight = new Animated.Value(0);
this.imageHeight = new Animated.Value(IMAGE_HEIGHT);
}
componentWillMount () {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.parallel([
Animated.timing(this.keyboardHeight, {
duration: event.duration,
toValue: event.endCoordinates.height,
}),
Animated.timing(this.imageHeight, {
duration: event.duration,
toValue: IMAGE_HEIGHT_SMALL,
}),
]).start();
};
keyboardWillHide = (event) => {
Animated.parallel([
Animated.timing(this.keyboardHeight, {
duration: event.duration,
toValue: 0,
}),
Animated.timing(this.imageHeight, {
duration: event.duration,
toValue: IMAGE_HEIGHT,
}),
]).start();
};
render() {
return (
<Animated.View style={[styles.container, { paddingBottom: this.keyboardHeight }]}>
<Animated.Image source={logo} style={[styles.logo, { height: this.imageHeight }]} />
<TextInput
placeholder="Email"
style={styles.input}
/>
<TextInput
placeholder="Username"
style={styles.input}
/>
<TextInput
placeholder="Password"
style={styles.input}
/>
<TextInput
placeholder="Confirm Password"
style={styles.input}
/>
</Animated.View>
);
}
};
export default Demo;
它確實是一個和其他解決方案不一樣的方案。使用 Animated.View 和 Animated.Image 而非 View 和 Image,以便可以使用動畫變量的值。有趣的部分是 keyboardWillShow 和 keyboardWillHide,它們會改變動畫變量的參數。
這里用兩個動畫同時並行驅動 UI 的改變。會給你留下下面的印象:

雖然寫了非常多的代碼,但好歹讓整個操作看上去非常流暢。你有很大的余地去選擇你要做什么,真正的自定義與你所關心內容的互動。
Combining Options
如果想提煉一些代碼,我傾向於結合幾種情況在一起。例如: 通選方案 1 和方案 3,你就只需要關心和圖像高度相關的動畫。
隨着 UI 復雜性的增加,使用下面代碼會比方案 3 精簡很多:
import React, { Component } from 'react';
import { View, TextInput, Image, Animated, Keyboard, KeyboardAvoidingView } from 'react-native';
import styles, { IMAGE_HEIGHT, IMAGE_HEIGHT_SMALL } from './styles';
import logo from './logo.png';
class Demo extends Component {
constructor(props) {
super(props);
this.imageHeight = new Animated.Value(IMAGE_HEIGHT);
}
componentWillMount () {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.timing(this.imageHeight, {
duration: event.duration,
toValue: IMAGE_HEIGHT_SMALL,
}).start();
};
keyboardWillHide = (event) => {
Animated.timing(this.imageHeight, {
duration: event.duration,
toValue: IMAGE_HEIGHT,
}).start();
};
render() {
return (
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<Animated.Image source={logo} style={[styles.logo, { height: this.imageHeight }]} />
<TextInput
placeholder="Email"
style={styles.input}
/>
<TextInput
placeholder="Username"
style={styles.input}
/>
<TextInput
placeholder="Password"
style={styles.input}
/>
<TextInput
placeholder="Confirm Password"
style={styles.input}
/>
</KeyboardAvoidingView>
);
}
};
export default Demo;

每種實現都有它的優點和缺點 —— 你必須選擇最適合給定用戶體驗的方案。
