React Native —— TextInput 組件與鍵盤遮擋問題記錄

綜述

鍵盤遮擋問題,應該是 RN 中常見的了,網上有很多參考文章.但是這次開發的頁面中涉及到多行輸入框的問題。
鍵盤響應的幾種辦法

我個人寫的一個庫:功能區域輸入庫 react-native-FuncInpu

鑒于之前開發過一個處理鍵盤高度與自適應的組件庫,這里對若干個問題作一個記錄。記錄的內容包括以下幾塊:

  • 安卓與 ios 實現鍵盤彈出的區別
  • 鍵盤遮擋解決的思路
  • textInput props的坑
  • 理想的生命周期

安卓與 iOS實現鍵盤彈出的區別

iOS的情況

熟悉iOS開發的同學一定很清楚,在iOS中鍵盤的遮擋問題就是自己處理的。需要代碼中去監聽鍵盤事件的notification,然后自己處理對應的可能被遮擋控件的frame的變化。

UIKIT_EXTERN NSNotificationName const UIKeyboardWillShowNotification __TVOS_PROHIBITED;
UIKIT_EXTERN NSNotificationName const UIKeyboardDidShowNotification __TVOS_PROHIBITED;
UIKIT_EXTERN NSNotificationName const UIKeyboardWillHideNotification __TVOS_PROHIBITED;
UIKIT_EXTERN NSNotificationName const UIKeyboardDidHideNotification __TVOS_PROHIBITED;
UIKIT_EXTERN NSNotificationName const UIKeyboardWillChangeFrameNotification  NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED;
UIKIT_EXTERN NSNotificationName const UIKeyboardDidChangeFrameNotification   NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED;

另外對于iOS而言,第三方鍵盤存在著較為特別的情況。從iOS 8開始蘋果支持第三方鍵盤輸入,但是市場還會出現鍵盤無法彈出的問題。

本人使用的是搜狗的第三方鍵盤,從前經常出現無法彈出,必須要去 設置 -> 通用 -> 鍵盤 -> 搜狗鍵盤 -> 必須將允許完全訪問關閉再開啟,才能重新啟用。但是這個問題應該現在是不再會存在了。

從實現的效果來說,第三方鍵盤比起原生鍵盤,會多出類似于收起按鍵等等的功能(原生鍵盤沒有)。

32F3C45F7E8DEA06CE7FD57F905EE870.png

而且第三方鍵盤實質上是會先調用原生鍵盤,使得上文提到的notification生效,然后再讓自己的鍵盤生效的。以我調試時候遇到的情況為例,回調會收到鍵盤的占位高度,如果使用第三方鍵盤,高度會收到兩次。以上圖中的搜狗鍵盤為例,會收到242282兩個高度,最后固定的高度是282

安卓的情況

安卓的鍵盤彈出不存在 ios 的特殊情況。因為鍵盤的部分是獨立于我們頁面之外的。也就是說他會自己處理原先被遮擋部分的向上滾動。這里就不作贅述。


鍵盤遮擋解決的思路

通常的解決思路是,將所有的 TextInput組件的實例都包裹在一個scrollView中。當鍵盤起來的時候,整體的ScrollView做一個相應的滾動偏移。
文首中介紹的那個第三方庫 react-native-keyboard-aware-scroll-view 就是借鑒了這一點來實現.

但是有一點是不可避免的,就是使用了這套方案,從視覺上,一定是鍵盤先彈出,然后所有的內容頁面才會想上滾動。這和原生頁面情況下鍵盤的彈出和頁面的上移幾乎是同時發生的效果無法比擬。

對比圖.gif

這里閃動的有一點快,仔細看是可以看出二者的展示區別的。

產生這種情況的原因是因為rn必須要先從iOS原生獲取到鍵盤的時間之后,再去進行相關的響應。這其中的生命周期及調用順序,會在最后一個部分 生命周期 中講到。


TextInput props的坑

針對鍵盤輸入和遮擋的問題,主要是使用react native官方提供的TextInput組件。但是其中有若干屬性會影響到鍵盤遮擋輸入內容。下面一一來列舉。

  • underlineColorAndroid 僅安卓有效。默認是 true,也就是在安卓環境下,輸入框下面默認會加一根有顏色的線,需要將它設成 false 才會消失。

  • onContentSizeChange 在輸入框換行的時候被調用到的回調屬性。可以在 { nativeEvent: { contentSize: { width, height } } } 中拿到變化后的高度。這個屬性也只在multiLinetrue的時候有效。

    需要注意的是,如果我們打斷點會發現,在頁面初始化渲染,也就是在TextInput初始化的時候,onContentSizeChange 其實是會先被調用一次的。這時候返回的高度,是根據我們設定的 TextInput 中的顯示字體的大小來決定的。

  • onFocus 這個屬性顧名思義,就是點擊輸入框時候就會進入的回調。但是這里有一個小坑就是點擊輸入框后其實會進入好幾個回調,因為輸入框聚焦和鍵盤彈出的時間是同時發生的。而且是異步執行的,沒有固定的誰先誰后,這時候就需要在生命周期上做處理了。下面最后一個部分會進入探討。

  • onSubmitEditing 只在單行模式下生效(下面講 multiline會細說)。這時候需要把returnKey設成send之類的屬性,當我們點擊發送的時候,會進入到這個回調監聽操作。

  • blurOnSubmit —— 如果為true,文本框會在提交的時候失焦。對于單行輸入框默認值為true,多行則為false。注意:對于多行輸入框來說,如果將blurOnSubmit設為true,則在按下回車鍵時就會失去焦點同時觸發onSubmitEditing事件,而不會換行。
    這條有點繞,不太好理解,我先把文檔中的原文摘錄下來:

    If true, the text field will blur when submitted. The default value is true for single-line fields and false for multiline fields. Note that for multiline fields, setting blurOnSubmit to true means that pressing return will blur the field and trigger the onSubmitEditing event instead of inserting a newline into the field.

    可以理解為:如果我將blurOnSubmit 設為 true,那么可以在多行情況下使用虛擬鍵盤上的發送按鈕。并傳入點擊事件方法給他,但是這樣一來,光標會失焦,鍵盤會消失,而且不會換行。這顯然不是我想要的。但是不這樣做,就無法獲取到點擊事件。
    目前這個問題無法解決,我在react native的 git issues 上曾提過疑問,但是沒有解決的方案。textinput issues

  • multiline
    然后就是這個影響深遠的屬性,因為他是制約整個 TextInput 與鍵盤顯示之間關系最大的屬性。為什么這么說呢?因為換行和不換行導致的代碼量不是一個數量級的。

    • 單行的情況
      不換行的情況下,不存在切換滾動等等一系列復雜的操作。而且你可以使用上面提到的TextInput的另一個屬性 onSubmitEditting來讓鍵盤上的returnKey切換成我們想要的發送或別的按鈕并接管他按下時候的回調事件.(另一個屬性returnKeyType有提供)
      下面是接管前和接管后的對比
    接管前.png
    接管后.png
  • 多行的情況
    這是最復雜的情況。如上所述,因為換行勢必影響輸入框的高度,產生了高度偏移之后,你必須要通知外部包裹的scrollView做相應的偏移。每一次換行,TextInput都會進入onContentSizeChange回調,要去做對應的處理。

生命周期

這里要討論的主要就是針對3個情況下的生命周期及回調函數

  • 鍵盤彈出
  • 輸入框換行
  • 鍵盤回縮

鍵盤彈出

正如上文提到的,當我開始點擊一個輸入框時,其實實現的是有3兩件事:光標的聚焦、鍵盤的彈出、外部scrollView的滾動
但是這三件事各自的回調都是異步發生的,也就是說,他們的回調的時間順序可能和我們設想和期望的不一樣。

我們先來羅列這三個事情發生的時候,他們各自的回調。

  • 光標的聚焦 —— TextInputonFocus
  • 鍵盤的彈出
    • keyboardWillShow —— 僅iOS有效
    • keyboardDidShow
    • keyboardWillChangeFrame —— 僅iOS有效
    • keyboardDidChangeFrame —— 僅iOS有效
  • 外部scrollView
    • 滾動結束進入回調 onMomentumScrollEnd

輸入框換行

onContentSizeChange 回調。這里再強調一次,當TextInput 被初始化渲染的時候,onContentSizeChange 也會執行,回調的高度根據設定的font大小來決定

鍵盤回縮

鍵盤回縮有3種情況會觸發到:

  • 用戶點擊了空白的非鍵盤輸入區部分;
  • 點擊了鍵盤上的回縮按鈕(ios 原生鍵盤沒有這個按鈕)
  • 代碼中調用了Keyboard.dismiss()

而且會和上面類似產生兩大類回調:

  • 鍵盤的彈出
    • keyboardWillHide —— 僅iOS有效
    • keyboardDidHide
    • keyboardWillChangeFrame —— 僅iOS有效
    • keyboardDidChangeFrame —— 僅iOS有效
  • 外部scrollView
    • 滾動結束進入回調 onMomentumScrollEnd

這里還有一個小坑,如果我們設置的發送按鈕是一個 button,或者是一個TouchOpacity,那么點擊發送以后,會先響應點擊了空白區域這一設定,然后再執行 buttononPress 回調。我這里的處理方法是,不使用按鍵性質的組件,直接采用 <View>onTouchStart 回調,規避了這個問題的發生

send.png

另外,在 ios 下調用 Keyboard.dismiss() 且當前輸入鍵盤為第三方鍵盤時,有時候會消失失敗。我做的方法是鍵盤調用 Keyboard.dismiss() 之后隔50ms 再調用一次。

在頻繁的切換彈出和回縮的過程中,如果沒有采用標志位去對這些凌亂的回調進行管控,讓他們跑在我們想要的順序中,就會達不到我們預期想要實現的效果。
鑒于如何實現一個理想化的輸入框切換不是本篇想要討論的內容,這里只討論如果你要按順序執行你想要執行的時序,該如何避免其中可能導致的坑。

想了解的讀者可以參看 我的一個 git 開源庫 有文檔說明和參考代碼。這了不贅述

  • 首先,光標的聚焦和鍵盤的彈出顯而易見是在調用外部滾動之前需要做的事,這一點毋庸置疑。因為只有你知道了鍵盤的高度之后,才會讓外部去做滾動相應的高度。(這里再一次解釋了上面那張 gif 對比圖的原因)
  • 其次,輸入框高度的切換,一定是會搭配setState操作使用的。也就是說,除了我們上面提到的幾個必定會進入的回調外,我們還要考慮到 componentDidUpdate 回調。
  • 最后,調用外部滾動相當于修改了scrollViewcontentOffset,換行相當于修改了整個scrollViewcontentSize,而這一切都會調用渲染并且會有一定的生效時間,如果在生效之前我們調用了setState去做渲染,勢必會造成最后頁面的偏移結果和我們想象中的不一致。

解決上面這幾個問題的方法其實說簡單也簡單。
一個是要采用標志位來避免進入一些重復的回調,(例如 keyboardDismiss我在上頭都會執行兩次,第一次會進入到willChangeFrame,但是我進去之后設置了一個標志位,第二次再進去,因為這個標志位,我就不會再執行一次后續的代碼了)
第二就是在需要做一些調用的時候加上幾十毫秒的延時,讓另一個回調先執行。

流程圖

下面是兩張圖借用我之前開發過的一個帶功能區域的輸入組件(效果參考微信聊天頁面底下的功能輸入區域)中的流程圖,可以感受下生命周期對開發過程中的影響。

功能輸入——喚起鍵盤流程.png
功能輸入 —— 鍵盤消失流程.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數據庫組件 SD...
    陽明AGI閱讀 16,018評論 3 119
  • 第一條時時刻刻都離不開空調 而且被老板坑了 說好的今天碰面 結果又是無音信 害得我改變計劃 好多事都被擱置 果然老...
    Admire佩佩閱讀 200評論 0 0
  • 多圖預警!文末有pdf版鏈接,pdf版閱讀效果更佳。 PDF版下載鏈接:http://pan.baidu.com/...
    妖葉秋閱讀 1,389評論 9 22