- 原文作者:Spencer Carli
- 譯文出自:掘金翻譯計劃
- 譯者:rccoder
- 校對者:atuooo、ZiXYu
如何讓你的 React Native 應(yīng)用在鍵盤彈出時優(yōu)雅地響應(yīng)
在使用 React Native 應(yīng)用時,一個常見的問題是當(dāng)你點(diǎn)擊文本輸入框時,鍵盤會彈出并且遮蓋住輸入框。就像這樣:
有幾種方式可以避免這種情況發(fā)生。一些方法比較簡單,另一些稍微復(fù)雜。一些是可以自定義的,一些是不能自定義的。今天,我將向你展示 3 種不同的方式來避免 React Native 應(yīng)用中的鍵盤遮擋問題。
文章中所有的代碼都托管在 GitHub 上
KeyboardAvoidingView
最簡單、最容易安裝使用的方法是 KeyboardAvoidingView。這是一個核心組件,同時也非常簡單。
你可以使用這段存在鍵盤覆蓋輸入框問題的 代碼,然后更新它,使輸入框不再被覆蓋。你要做的第一件事是用 KeyboardAvoidView 替換 View,然后給它加一個 behavior 的 prop。查看文檔的話你會發(fā)現(xiàn),他可以接收三個不同的值作為參數(shù) ——?height, padding, position。我發(fā)現(xiàn) padding 的表現(xiàn)是最在我意料之內(nèi)的,所以我將使用它。
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;
它的表現(xiàn)如下,雖然不是非常完美,但幾乎不需要任何工作量。這在我看來是相當(dāng)好的。
需要注意的事,在上個實例代碼中的第 30 行,設(shè)置了一個高度為 60 的 View。我發(fā)現(xiàn) keyboardAvoidingView 對最后一個元素不適用,即使是添加了 padding/margin 屬性也不奏效。所以我添加了一個新的元素去 “撐開” 一些像素。
使用這個方法時,頂部的圖片會被推出到視圖之外。在后面我會告訴你如何解決這個問題。
針對 Android 開發(fā)者:我發(fā)現(xiàn)這種方法是處理這個問題最好,也是唯一的辦法。在
AndroidManifest.xml中添加android:windowSoftInputMode="adjustResize"。操作系統(tǒng)將為你解決大部分的問題,KeyboardAvoidingView 會為你解決剩下的問題。參見 這個。接下的部分可能不適用于你。
Keyboard Aware ScrollView
下一種解決辦法是使用 react-native-keyboard-aware-scroll-view,他會給你很大的沖擊。實際上它使用了 ScrollView 和 ListView 處理所有的事情(取決于你選擇的組件),讓滑動交互變得更加自然。它另外一個優(yōu)點(diǎn)是它會自動將屏幕滾動到獲得焦點(diǎn)的輸入框處,這會帶來非常流暢的用戶體驗。
它的使用方法同樣非常簡單 —— 只需要替換 基礎(chǔ)代碼 的 View。下面是具體代碼,我會做一些相關(guān)的說明:
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>
);
首先你需要設(shè)置 ScrollView 的 backgroundColor(如果你想使用滾動的話)。接下來你需要告訴默認(rèn)組件在哪里,當(dāng)你的鍵盤收起時,界面就會返回到默認(rèn)的那個位置 —— 如果省略 View 的這個 prop,可能會導(dǎo)致鍵盤在關(guān)閉之后界面依舊停留在頂部。
在設(shè)置好 resetScrollToCoords 這個 prop 之后你需要設(shè)置 contentContainerStyle —— 這本質(zhì)上會替換掉你之前給 View 設(shè)置的樣式。最后一件事是禁止掉從用戶產(chǎn)生的滾動交互。這可能并不是完全適合你的 UI 交互(比如對于用戶需要編輯很多字段的界面),但是在這里,允許用戶滾動沒有任何意義,因為并沒有其它的內(nèi)容需要用戶來進(jìn)行滾動操作。
把這些所有的 prop 放到一起就會產(chǎn)生下面的效果,看起來很不錯:
Keyboard Module
這是迄今為止最為手動的方式,但也同時給開發(fā)者最大的控制權(quán)。你可以使用一些動畫庫來幫助實現(xiàn)之前看到的那種平滑滾動。
React Native 在官方文檔是沒有說 Keyboard Module 可以監(jiān)聽從設(shè)備上產(chǎn)生的鍵盤事件。你使用的事件是 keyboardWillShow 和 keyboardWillHide,來產(chǎn)生一個鍵盤展開的動畫(或者其他信息)。
當(dāng) keyboardWillShow 事件產(chǎn)生時,需要設(shè)置一個動畫變量到鍵盤的最終高度,并使其與鍵盤彈出滑動時間保持一致。然后你可以用這個動畫變量的值在容器的底部設(shè)置 padding,將所有的內(nèi)容上移。
我會在后面展示具體代碼,先展示一下上面所說的內(nèi)容會產(chǎn)生的效果:
這次我將修復(fù) UI 中的那個圖片。為此,需要使用動畫變量的值來管理圖片的高度,你可以在彈出鍵盤的同時調(diào)整圖片的高度。下面是具體代碼:
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,它們會改變動畫變量的參數(shù)。
這里用兩個動畫同時并行驅(qū)動 UI 的改變。會給你留下下面的印象:
雖然寫了非常多的代碼,但好歹讓整個操作看上去非常流暢。你有很大的余地去選擇你要做什么,真正的自定義與你所關(guān)心內(nèi)容的互動。
Combining Options
如果想提煉一些代碼,我傾向于結(jié)合幾種情況在一起。例如: 通選方案 1 和方案 3,你就只需要關(guān)心和圖像高度相關(guān)的動畫。
隨著 UI 復(fù)雜性的增加,使用下面代碼會比方案 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;
每種實現(xiàn)都有它的優(yōu)點(diǎn)和缺點(diǎn) —— 你必須選擇最適合給定用戶體驗的方案。
|</task-lists>
<details class="details-overlay details-reset position-relative float-left reaction-popover-container js-reaction-popover-container"></details>
[
@rccoder
](https://github.com/rccoder) rccoder added 翻譯 React Native labels <relative-time datetime="2017-03-14T10:40:55Z" title="2017年3月14日 GMT+8 下午6:40">on 14 Mar 2017</relative-time>
<details class="details-overlay details-reset position-relative d-inline-block js-socket-channel js-updatable-content js-dropdown-details js-reaction-popover-container js-comment-header-reaction-button" data-channel="reaction:issue-comment:286388880" data-url="/_render_node/MDEyOklzc3VlQ29tbWVudDI4NjM4ODg4MA==/comments/comment_header_reaction_button"></details>
<details class="details-overlay details-reset position-relative d-inline-block js-socket-channel js-updatable-content js-dropdown-details js-reaction-popover-container js-comment-header-reaction-button" data-channel="reaction:issue-comment:286388880" data-url="/_render_node/MDEyOklzc3VlQ29tbWVudDI4NjM4ODg4MA==/comments/comment_header_reaction_button"></details><details class="details-overlay details-reset position-relative d-inline-block js-dropdown-details " id="details-issuecomment-286388880"></details>
<details class="details-overlay details-reset position-relative d-inline-block js-dropdown-details " id="details-issuecomment-286388880"></details>
Owner
rccoder commented <relative-time datetime="2017-03-14T11:03:37Z" title="2017年3月14日 GMT+8 下午7:03">on 14 Mar 2017</relative-time><include-fragment class="js-comment-edit-history d-inline"></include-fragment>
<task-lists disabled="" sortable="">|
題外話
其他參考
我的實踐
場景
類似于 QQ 聊天窗的場景
<View>
<View></View> // 輸入框
<ScrollView></Scrollview> // 對話 View
<View></View> // TextInput
<View>
解決方案
前提
采用 flex 布局,兩個 View 是固定高度,ScrollView 在中間占滿剩余空間
監(jiān)聽鍵盤談起事件,計算鍵盤高度。
在最后一個 View 后面填充 空間占用View,高度由 state 控制,取自鍵盤高度。
<View>
<View></View> // 輸入框
<ScrollView></Scrollview> // 對話 View
<View><TextInput /></View> // TextInput
<View></View> // 空間占用 View
<View>
問題
ScrollView 長度變短之后,里面的內(nèi)容并沒有發(fā)生滾動。產(chǎn)生鍵盤覆蓋 ScrollView 內(nèi)容的效果
解決方案
過程中自定義 ScrollView 的滾動。滾到最底部:
componentDidUpdate() {
const {
// 鍵盤高度
keyboardHeight,
// 鍵盤是否展開
keyboardStatus
} = this.props
const {
scrollViewHeight
} = this.state;
// 鍵盤展開
// 鍵盤展開時 scrollview 變小,內(nèi)容位置不會發(fā)生變化,所以減去鍵盤高度滑動到最底
// 鍵盤關(guān)閉時 scrollView 變大,所以再減一個鍵盤高度
if (keyboardStatus === 1) {
this.scrollView.scrollTo({
y: scrollViewHeight - keyboardHeight,
animated: true
})
} else {
this.scrollView.scrollTo({
y: scrollViewHeight - 2*keyboardHeight,
animated: true
})
}
}
如何如果 ScrollView 的 scrollViewHeight ?
<ScrollView
ref={ref => this.scrollView = ref}
>
<View style={styles.lineView}>
<EasySpeak
pos='right'
text='今天天氣怎么樣'
/>
</View>
<View style={styles.lineView}>
<EasySpeak
pos='left'
text='你所在地區(qū)是哈爾濱,今天溫度 -40 度,注意身體別凍死自己'
/>
</View>
<View style={styles.lineView}>
<MultiImgText />
</View>
<View style={styles.lineView}>
<MultiImgText />
</View>
<View onLayout={e => this.setState({scrollViewHeight: e.nativeEvent.layout.y})}/>
</ScrollView>
即 下面鋪一層 View,計算 onLayout 時的位置
尾語
只在 IOS 模擬器上做了簡單測試,目前尚不清楚是否有其他問題。這里只做一些記錄