综述
键盘遮挡问题,应该是 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
开始苹果支持第三方键盘输入,但是市场还会出现键盘无法弹出的问题。
本人使用的是搜狗的第三方键盘,从前经常出现无法弹出,必须要去 设置 -> 通用 -> 键盘 -> 搜狗键盘 -> 必须将允许完全访问关闭再开启,才能重新启用。但是这个问题应该现在是不再会存在了。
从实现的效果来说,第三方键盘比起原生键盘,会多出类似于收起按键等等的功能(原生键盘没有)。
而且第三方键盘实质上是会先调用原生键盘,使得上文提到的notification
生效,然后再让自己的键盘生效的。以我调试时候遇到的情况为例,回调会收到键盘的占位高度,如果使用第三方键盘,高度会收到两次。以上图中的搜狗键盘为例,会收到242
和282
两个高度,最后固定的高度是282
安卓的情况
安卓的键盘弹出不存在 ios 的特殊情况。因为键盘的部分是独立于我们页面之外的。也就是说他会自己处理原先被遮挡部分的向上滚动。这里就不作赘述。
键盘遮挡解决的思路
通常的解决思路是,将所有的 TextInput
组件的实例都包裹在一个scrollView
中。当键盘起来的时候,整体的ScrollView
做一个相应的滚动偏移。
文首中介绍的那个第三方库 react-native-keyboard-aware-scroll-view
就是借鉴了这一点来实现.
但是有一点是不可避免的,就是使用了这套方案,从视觉上,一定是键盘先弹出,然后所有的内容页面才会想上滚动。这和原生页面情况下键盘的弹出和页面的上移几乎是同时发生的效果无法比拟。
这里闪动的有一点快,仔细看是可以看出二者的展示区别的。
产生这种情况的原因是因为rn
必须要先从iOS
原生获取到键盘的时间之后,再去进行相关的响应。这其中的生命周期及调用顺序,会在最后一个部分 生命周期 中讲到。
TextInput
props
的坑
针对键盘输入和遮挡的问题,主要是使用react native
官方提供的TextInput
组件。但是其中有若干属性会影响到键盘遮挡输入内容。下面一一来列举。
underlineColorAndroid
仅安卓有效。默认是true
,也就是在安卓环境下,输入框下面默认会加一根有颜色的线,需要将它设成false
才会消失。-
onContentSizeChange
在输入框换行的时候被调用到的回调属性。可以在{ nativeEvent: { contentSize: { width, height } } }
中拿到变化后的高度。这个属性也只在multiLine
为true
的时候有效。需要注意的是,如果我们打断点会发现,在页面初始化渲染,也就是在
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
有提供)
下面是接管前和接管后的对比
- 单行的情况
- 多行的情况
这是最复杂的情况。如上所述,因为换行势必影响输入框的高度,产生了高度偏移之后,你必须要通知外部包裹的scrollView
做相应的偏移。每一次换行,TextInput
都会进入onContentSizeChange
回调,要去做对应的处理。
生命周期
这里要讨论的主要就是针对3个情况下的生命周期及回调函数
- 键盘弹出
- 输入框换行
- 键盘回缩
键盘弹出
正如上文提到的,当我开始点击一个输入框时,其实实现的是有3两件事:光标的聚焦、键盘的弹出、外部scrollView
的滚动
但是这三件事各自的回调都是异步发生的,也就是说,他们的回调的时间顺序可能和我们设想和期望的不一样。
我们先来罗列这三个事情发生的时候,他们各自的回调。
- 光标的聚焦 ——
TextInput
的onFocus
- 键盘的弹出
-
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
,那么点击发送以后,会先响应点击了空白区域
这一设定,然后再执行button
的onPress
回调。我这里的处理方法是,不使用按键性质的组件,直接采用<View>
的onTouchStart
回调,规避了这个问题的发生
另外,在
ios
下调用Keyboard.dismiss()
且当前输入键盘为第三方键盘时,有时候会消失失败。我做的方法是键盘调用Keyboard.dismiss()
之后隔50ms 再调用一次。
在频繁的切换弹出和回缩的过程中,如果没有采用标志位去对这些凌乱的回调进行管控,让他们跑在我们想要的顺序中,就会达不到我们预期想要实现的效果。
鉴于如何实现一个理想化的输入框切换不是本篇想要讨论的内容,这里只讨论如果你要按顺序执行你想要执行的时序,该如何避免其中可能导致的坑。
想了解的读者可以参看 我的一个 git 开源库 有文档说明和参考代码。这了不赘述
- 首先,光标的聚焦和键盘的弹出显而易见是在调用外部滚动之前需要做的事,这一点毋庸置疑。因为只有你知道了键盘的高度之后,才会让外部去做滚动相应的高度。(这里再一次解释了上面那张 gif 对比图的原因)
- 其次,输入框高度的切换,一定是会搭配
setState
操作使用的。也就是说,除了我们上面提到的几个必定会进入的回调外,我们还要考虑到componentDidUpdate
回调。 - 最后,调用外部滚动相当于修改了
scrollView
的contentOffset
,换行相当于修改了整个scrollView
的contentSize
,而这一切都会调用渲染并且会有一定的生效时间,如果在生效之前我们调用了setState
去做渲染,势必会造成最后页面的偏移结果和我们想象中的不一致。
解决上面这几个问题的方法其实说简单也简单。
一个是要采用标志位来避免进入一些重复的回调,(例如 keyboardDismiss
我在上头都会执行两次,第一次会进入到willChangeFrame
,但是我进去之后设置了一个标志位,第二次再进去,因为这个标志位,我就不会再执行一次后续的代码了)
第二就是在需要做一些调用的时候加上几十毫秒的延时,让另一个回调先执行。
流程图
下面是两张图借用我之前开发过的一个带功能区域的输入组件
(效果参考微信聊天页面底下的功能输入区域)中的流程图,可以感受下生命周期对开发过程中的影响。