使用 React Native 自定义安全键盘(iOS 和 Android 双适配)

5,772 阅读5分钟

自定义键盘的实现效果如下:

iOS端:

ios-keyboard.gif

Android端:
android-keyboard.gif

实现功能(iOS和Android):

  • 输入框没有自定义,直接使用RN的TextInput
  • 点击完成可以移除焦点
  • 长按回退键可以删除光标前所有字符
  • 字母和字符键盘点击会显示Tip
  • 字母、字符、数字键盘可相互快速切换(为视图做了懒加载和缓存)

大致思路

1、如何隐藏系统键盘
用RN为iOS和Android双平台共用一套自定义键盘,使用RN的TextInput作为输入框,这样TextInput该怎么使用就怎么使用,因此问题的关键就在于如何隐藏系统键盘。在iOS中使用inputView可以轻松实现自定义键盘,在Android中自定义键盘并非像iOS那么直接,需要手动隐藏系统键盘,然后监听输入框的焦点事件来控制自定义键盘的显示和隐藏。
可以看以下两个示例大致了解下:
iOS自定义键盘
Android 自定义键盘实现

Android中自定义键盘有一个让人容易误解的地方,就是KeyboardView。可以在Android 自定义键盘实现Demo中看到有hideSystemSoftKeyboard方法,用来把EditText的系统键盘给隐藏,然后再使用KeyboardView加入到视图的底部布局中,KeyboardView使用KeyboardView.OnKeyboardActionListener来监听键盘点击,使用Keyboard来给定键盘布局(使用XML描述布局),然后可以动态修改KeyboardKeyboardView.OnKeyboardActionListener来实现不同的自定义键盘。由此可见,使用RN做自定义键盘在hideSystemSoftKeyboard后就可以打止了,不需要使用KeyboardView,而是使用ReactRootView,然后在RN中绘制键盘。

2、自定义键盘与原生输入组件如何交互

  • RN -> Native
    • RN的TextInput要告知Native它需要什么类型的自定义键盘,因此需要把TextInput的ReactTag和键盘类型传给Native。
    • RN的键盘每次点击一个按钮需要通知Native,并且传入RN的Keyboard对应的TextInput组件(在Android中是EditText,在iOS中是TextFiled和TextView)的ReactTag,Native可以通过ReactTag来定位到具体的Native输入框,然后再根据RN中传来的命令来修改输入框的内容从而实现同步。
  • Native -> RN
    • iOS
      • Native需要创建一个RCTRootView,然后将一些初始化参数传入(如自定义键盘类型,TextView对应的ReactTag)
      • 将RCTRootView赋值给ReactTag对应的TextView的inputView。
    • Android
      • Native拿到ReactTag对应的EditText,禁止它的系统键盘弹出。
      • Native需要创建ReactRootView加入到Activity的底部布局中当做键盘使用。
      • 将ReactRootView加入到EditText的tag中,然后监听EditText的焦点改变来控制相对应的键盘的显示和隐藏。

时序图如下:

Paste_Image.png

RN中文网的自定义键盘的GitHub地址:github.com/reactnative…
关于ReactTag的基本原理(iOS版):awhisper.github.io/2016/07/02/…

绘制

将上面的流程和代码弄清后就只剩下RN的键盘界面问题了。

数字键盘的布局和绘制:
参考代码:github.com/beefe/react…

字母键盘的布局示意图:

image.png

字符键盘的布局示意图:

image.png

字母和符号键盘的Tip绘制:
最初想法:每个key都为一个单独的component,里面有个TouchableHighlight按钮和与按钮并级的Tip组件,在onPressIn和onPressOut之间控制Tip的显示与隐藏。

不可行:以上方案在Android端不可行,因为Android中RN不能使用overflow:visible属性,在0.41版本中可以使用FlatUIImplement实现overflow:visible属性,但是在0.42及以上版本使用会报错(不过现在解决了,我没试)。

曲线救国想法:使用measureLayout得到key相对keyboard的位置,然后在keyboard层级绘制Tip,而不在key组件中绘制Tip。这个方案基本可行,但是在Android端还是需要解决以下问题:
1、Tip可能会超过keyboard的边界,因此第一行还是会被截掉,所以需要把键盘的高度要加高一些以容纳第一行的Tip。
2、keyboard的背景色会遮挡下层视图,所以要对keyboard的背景做透明。
3、keyboard加高的部分会遮挡下层视图对手势的响应,所以对加高的部分要放弃事件响应。

解答参考了stackoverflow上的提问:Element overflow hidden in React-Native Android

绘制难点:
iOS端Tip外形的绘制:使用react-native-svg绘制,我的绘制代码
Android阴影的实现:使用以react-native-svg为基库做成的react-native-shadow

注:我并没有在Android中使用阴影样式,而是仿照f8app的写法在不同平台使用不同样式

性能优化

因为字母和字符键盘一次性绘制了30多个按钮在屏幕上,会导致在低端Android机能感觉到卡顿(调成Debug模式会很明显,在Dev下打开'Show Perf Monitor'会发现首屏渲染时JS线程掉帧很明显),我在这做了个测试:一次性渲染40个按钮,可以运行看一下卡顿情况。
因此渲染出的数字、字母和字符键盘的DOM分别要被缓存起来,这样才不会在频繁切换键盘时感觉到卡顿。
如果首屏出现的是数字键盘,那么字母键盘和字符键盘就不应该被放在渲染树中,做到用到特定键盘时再去加载这个键盘。

根据React 源码剖析系列 - 不可思议的 react diff,要做到高效的渲染尽量不能改变Dom树的结构,React并没有提供removeViewaddView之类的方法(当然你可以通过带 _ 的私有方法实现),Element(使用JSX或者React. createElement创建出来的)其实并不占多大空间,真正渲染的是DOM(可以通过ref获取),使用DisplayView可以控制DOM的隐藏和显示,让视图在第一次使用时才在DOM树创建,在隐藏和显示时不用改变DOM树的结构。

示例代码

customKeyboard

安利两个工具

react-native-storybook
UI组件的开发工具,可以浏览组件库,查看各个组件的各个状态,以及开发和测试组件。

react-devtools

image.png

欢迎关注我的简书主页:www.jianshu.com/u/b92ab7b3a… 文章同步更新^_^