如何讓你的 React Native 應(yīng)用在鍵盤彈出時優(yōu)雅地響應(yīng)

如何讓你的 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ù) ——?heightpadding, 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,他會給你很大的沖擊。實際上它使用了 ScrollViewListView 處理所有的事情(取決于你選擇的組件),讓滑動交互變得更加自然。它另外一個優(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è)置 ScrollViewbackgroundColor(如果你想使用滾動的話)。接下來你需要告訴默認(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)生的鍵盤事件。你使用的事件是 keyboardWillShowkeyboardWillHide,來產(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.ViewAnimated.Image 而非 ViewImage,以便可以使用動畫變量的值。有趣的部分是 keyboardWillShowkeyboardWillHide,它們會改變動畫變量的參數(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 模擬器上做了簡單測試,目前尚不清楚是否有其他問題。這里只做一些記錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容