在 Flutter 中目前官方没有提供快速自定义键盘的解决方案。
但在项目中以及 ICP 测评需要用到安全键盘,比如金额输入、安全密码输入、各种自定义快速输入键盘。
这里以Packages的形式实现了一个自定义安全键盘,已上传 Pub。
实现功能
- 适用原生输入框控件
- 支持一个页面多个安全键盘输入框
- 支持系统键盘与安全键盘混合使用
- 支持自动定位到输入框位置
实现思路
主要是通过拦截PlatformChannel实现的,可以无缝对接TextFiled等Flutter自带的输入框,监听输入框各种状态,以及内容变化。
关键代码讲解
- KeyboardManager
自定义键盘管理类,包括输入类型与键盘的管理,以及输入框拦截、悬浮自定义键盘及生命周期管理等。
主要功能方法 init。
///初始化键盘监听并且传递当前页面的context
static init(BuildContext context) {
_context = context;
interceptorInput();
}
///拦截键盘交互
static interceptorInput() {
if (isInterceptor) return;
isInterceptor = true;
BinaryMessages.setMockMessageHandler("flutter/textinput",
(ByteData data) async {
var methodCall = _codec.decodeMethodCall(data);
switch (methodCall.method) {
...
return response;
});
}
在init 方法中对输入框交互进行拦截。
当输入框获焦时会调用TextInput.setClient、TextInput.show。
case 'TextInput.show':
if (_currentKeyboard != null) {
openKeyboard();
return _codec.encodeSuccessEnvelope(null);
} else {
return await _sendPlatformMessage("flutter/textinput", data);
}
break
case 'TextInput.setClient':
...
brea
在TextInput.setClient中根据输入框定义的键盘类型找出对应键盘对应的键盘配置、InputClient,以及初始输入变更监听KeyboardController。
在TextInput.show根据配置的键盘类型,调用 openKeyboard 生成键盘控件。
///显示键盘
static openKeyboard() {
///键盘已经打开
if (_keyboardEntry != null) return;
_pageKey = GlobalKey<KeyboardPageState>();
_keyboardHeight = _currentKeyboard.getHeight();
///根据键盘高度,使键盘滚动到输入框位置
KeyboardMediaQueryState queryState = _context
.ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
as KeyboardMediaQueryState;
queryState.update();
...
///往Overlay中插入插入OverlayEntry
Overlay.of(_context).insert(_keyboardEntry);
}
这里通过 Overlay显示一个悬浮窗,并键盘弹出后,滚动到输入框位置。
当输入框输入时,会调用TextInput.setEditingState。
case 'TextInput.setEditingState':
var editingState = TextEditingValue.fromJSON(methodCall.arguments);
if (editingState != null && _keyboardController != null) {
_keyboardController.value = editingState;
return _codec.encodeSuccessEnvelope(null);
}
break
这里设置 KeyboardController 的TextEditingValue,就可以监听输入框的输入变化。
当切换焦点、或自定义键盘输入完成关闭时,会调用TextInput.clearClient、TextInput.hide。
case 'TextInput.hide':
if (_currentKeyboard != null) {
hideKeyboard();
return _codec.encodeSuccessEnvelope(null);
} else {
return await _sendPlatformMessage("flutter/textinput", data);
}
break;
///切换输入框时,会调用该回调,切换时键盘会隐藏。
case 'TextInput.clearClient':
hideKeyboard(animation: false);
clearKeyboard();
break
在 clearClient 中清除 InputClient,且隐藏键盘。
- KeyboardController
键盘输入变更监听,可以参考系统[TextEditingController],主要通过ValueNotifier监听TextEditingValue的值变化,ValueNotifier是一个包含单个值的变更通知器,当它的值改变的时候,会通知它的监听。
扩展了 addText,deleteText,doneAction
///删除一个字符,一般用于键盘的删除键
deleteOne() {
if (selection.baseOffset == 0) return;
String newText = '';
if (selection.baseOffset != selection.extentOffset) {
newText = selection.textBefore(text) + selection.textAfter(text);
value = TextEditingValue(
text: newText,
selection: selection.copyWith(
baseOffset: selection.baseOffset,
extentOffset: selection.baseOffset));
} else {
newText = text.substring(0, selection.baseOffset - 1) +
selection.textAfter(text);
value = TextEditingValue(
text: newText,
selection: selection.copyWith(
baseOffset: selection.baseOffset - 1,
extentOffset: selection.baseOffset - 1));
}
}
/// 在光标位置添加文字,一般用于键盘输入
addText(String insertText) {
String newText =
selection.textBefore(text) + insertText + selection.textAfter(text);
value = TextEditingValue(
text: newText,
selection: selection.copyWith(
baseOffset: selection.baseOffset + insertText.length,
extentOffset: selection.baseOffset + insertText.length));
}
/// 完成
doneAction() {
KeyboardManager.sendPerformAction(TextInputAction.done);
}
- KeyboardMediaQuery
用于键盘弹出的时候控制页面边间,使输入框不被挡住,自动定位到输入框位置。
class KeyboardMediaQuery extends StatefulWidget {
final Widget child;
KeyboardMediaQuery({this.child}) : assert(child != null);
@override
State<StatefulWidget> createState() => KeyboardMediaQueryState();
}
class KeyboardMediaQueryState extends State<KeyboardMediaQuery> {
@override
Widget build(BuildContext context) {
var data = MediaQuery.of(context);
///消息传递,更新控件边距
return MediaQuery(
child: widget.child,
data: data.copyWith(
viewInsets: data.viewInsets
.copyWith(bottom: KeyboardManager.keyboardHeight)));
}
///通知更新
void update() {
setState(() => {});
}
}
使用方法
- Step1
在项目pubspec.yaml添加安全键盘依赖
dependencies:
security_keyboard: ^1.0.2
- Step2
根据项目需求编写个性化键盘
typedef KeyboardSwitch = Function(SecurityKeyboardType type);
enum SecurityKeyboardType {
text,
textUpperCase,
number,
numberOnly,
numberSimple,
symbol
}
class SecurityKeyboard extends StatefulWidget {
///用于控制键盘输出的Controller
final KeyboardController controller;
///键盘类型,默认文本
final SecurityKeyboardType keyboardType;
///定义InputType类型
static const SecurityTextInputType inputType =
const SecurityTextInputType(name: 'SecurityKeyboardInputType');
SecurityKeyboard({this.controller, this.keyboardType});
///文本输入类型
static SecurityTextInputType text =
SecurityKeyboard._inputKeyboard(SecurityKeyboardType.text);
///数字输入类型
static SecurityTextInputType number =
SecurityKeyboard._inputKeyboard(SecurityKeyboardType.number);
///仅数字输入类型
static SecurityTextInputType numberOnly =
SecurityKeyboard._inputKeyboard(SecurityKeyboardType.numberOnly);
///仅数字输入类型,且没有键盘提示
static SecurityTextInputType numberSimple =
SecurityKeyboard._inputKeyboard(SecurityKeyboardType.numberSimple);
///初始化键盘类型,返回输入框类型
static SecurityTextInputType _inputKeyboard(
SecurityKeyboardType securityKeyboardType) {
///注册键盘的方法
String inputType = securityKeyboardType.toString();
SecurityTextInputType securityTextInputType =
SecurityTextInputType(name: inputType);
KeyboardManager.addKeyboard(
securityTextInputType,
KeyboardConfig(
builder: (context, controller) {
return SecurityKeyboard(
controller: controller,
keyboardType: securityKeyboardType,
);
},
getHeight: () {
return SecurityKeyboard.getHeight(securityKeyboardType);
},
),
);
return securityTextInputType;
}
///键盘类型
SecurityKeyboardType get _keyboardType => keyboardType;
///编写获取高度的方法
static double getHeight(SecurityKeyboardType securityKeyboardType) {
return securityKeyboardType == SecurityKeyboardType.numberSimple
? LcfarmSize.dp(192)
: LcfarmSize.dp(232);
}
@override
_SecurityKeyboardState createState() => _SecurityKeyboardState();
}
class _SecurityKeyboardState extends State<SecurityKeyboard> {
///当前键盘类型
SecurityKeyboardType currentKeyboardType;
@override
void initState() {
super.initState();
currentKeyboardType = widget._keyboardType;
}
@override
Widget build(BuildContext context) {
Widget keyboard;
switch (currentKeyboardType) {
case SecurityKeyboardType.number:
keyboard = NumberKeyboard(
widget.controller,
currentKeyboardType,
keyboardSwitch: (SecurityKeyboardType keyboardType) {
setState(() {
currentKeyboardType = keyboardType;
});
},
);
break;
case SecurityKeyboardType.numberOnly:
case SecurityKeyboardType.numberSimple:
keyboard = NumberKeyboard(widget.controller, currentKeyboardType);
break;
case SecurityKeyboardType.symbol:
keyboard = SymbolKeyboard(widget.controller,
(SecurityKeyboardType keyboardType) {
setState(() {
currentKeyboardType = keyboardType;
});
});
break;
case SecurityKeyboardType.text:
case SecurityKeyboardType.textUpperCase:
keyboard = AlphabetKeyboard(widget.controller, currentKeyboardType,
(SecurityKeyboardType keyboardType) {
setState(() {
currentKeyboardType = keyboardType;
});
});
break;
}
return keyboard;
}
}
将以下代码添加到要使用安全键盘的页面:
class PasswordVerify extends StatelessWidget {
@override
Widget buildScaffold(BuildContext context) {
//构建包含安全键盘视图,用于键盘弹出的时候页面可以滚动到输入框的位置
return KeyboardMediaQuery(
child: Builder(builder: (ctx) {
//初始化键盘监听并且传递当前页面的context
KeyboardManager.init(ctx);
return super.buildScaffold(context);
}),
);
}
- Step4
在TextField keyboardType中设置自定义安全性键盘类型。
只需传递Step1中编写的inputType,就像通常设置键盘输入类型一样。
TextField(
...
keyboardType: SecurityKeyboard.text,
...
)
最后
如果在使用过程遇到问题,欢迎下方留言交流。