Flutter 阻止系统键盘弹出的优雅方式
作者:法的空间
前言
开篇先吐槽一下,输入框和文本,一直都是官方每个版本改动的重点,先不说功能上全不全的问题,每次版本升级,必有 breaking change
。对于 extended_text_field | Flutter Package (flutter-io.cn) 和 extended_text | Flutter Package (flutter-io.cn) 来说,新功能都是基于官方的代码,每次版本升级,merge
代码就一个字,头痛,已经有了躺平的想法了。(暂时不 merge
了,能运行就行,等一个稳定点的官方版本,准备做个重构,重构一个相对更好 merge
代码的结构。)
系统键盘弹出的原因
吐槽完毕,我们来看一个常见的场景,就是自定义键盘。要想显示自己自定义的键盘,那么必然需要隐藏系统的键盘。方法主要有如下:
- 在合适的时机调用,
SystemChannels.textInput.invokeMethod<void>('TextInput.hide')
。 - 系统键盘为啥会弹出来,是因为某些代码调用了
SystemChannels.textInput.invokeMethod<void>('TextInput.show')
,那么我们可以魔改官方代码, 把TextField
和EditableText
的代码复制出来。
EditableTextState
代码中有一个 TextInputConnection? _textInputConnection;
,它会在有需要的时候调用 show
方法。
TextInputConnection
中 show
,如下。
/// Requests that the text input control become visible. void show() { assert(attached); TextInput._instance._show(); }
TextInput
中 _show
,如下。
void _show() { _channel.invokeMethod<void>('TextInput.show'); }
那么问题就简单了,把 TextInputConnection
调用 show
方法的地方全部注释掉。这样子确实系统键盘就不会再弹出来了。
在实际开发过程中,两种方法都有自身的问题:
第一种方法会导致系统键盘上下,会造成布局闪烁,而且调用这个方法的时机也很容易造成额外的 bug
。
第二种方法,就跟我吐槽的一样,复制官方代码真的是吃力不讨好的一件事情,版本迁移的时候,没人愿意再去复制一堆代码。如果你使用的是三方的组件,你可能还需要去维护三方组件的代码。
拦截系统键盘弹出信息
实际上,系统键盘是否弹出,完全是因为 SystemChannels.textInput.invokeMethod<void>('TextInput.show')
的调用,但是我们不可能去每个调用该方法地方去做处理,那么这个方法执行后续,我们有办法拦截吗? 答案当然是有的。
Flutter
的 Framework
层发送信息 TextInput.show
到 Flutter
引擎是通过 MethodChannel
, 而我们可以通过重载 WidgetsFlutterBinding
的 createBinaryMessenger
方法来处理Flutter
的 Framework
层通过 MethodChannel
发送的信息。
mixin TextInputBindingMixin on WidgetsFlutterBinding { @override BinaryMessenger createBinaryMessenger() { return TextInputBinaryMessenger(super.createBinaryMessenger()); } }
在 main 方法中初始化这个 binding
。
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin { } void main() { YourBinding(); runApp(const MyApp()); }
BinaryMessenger
有 3
个方法需要重载.
class TextInputBinaryMessenger extends BinaryMessenger { TextInputBinaryMessenger(this.origin); final BinaryMessenger origin; @override Future<ByteData?>? send(String channel, ByteData? message) { // TODO: implement send throw UnimplementedError(); } @override void setMessageHandler(String channel, MessageHandler? handler) { // TODO: implement setMessageHandler } @override Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) { // TODO: implement handlePlatformMessage throw UnimplementedError(); } }
- send
Flutter
的 Framework
层发送信息到 Flutter
引擎,会走这个方法,这也是我们需要的处理的方法。
- setMessageHandler
Flutter
引擎 发送信息到 Flutter
的 Framework
层的回调。在我们的场景中不用处理。
- handlePlatformMessage
把 send
和 setMessageHandler
二和一,看了下 注释,似乎是服务于 test
的
static const MethodChannel platform = OptionalMethodChannel( 'flutter/platform', JSONMethodCodec(), );
对于不需要处理的方法,我们做以下处理。
class TextInputBinaryMessenger extends BinaryMessenger { TextInputBinaryMessenger(this.origin); final BinaryMessenger origin; @override Future<ByteData?>? send(String channel, ByteData? message) { // TODO: 处理我们自己的逻辑 return origin.send(channel, message); } @override void setMessageHandler(String channel, MessageHandler? handler) { origin.setMessageHandler(channel, handler); } @override Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) { return origin.handlePlatformMessage(channel, data, callback); } }
接下来我们可以根据我们的需求处理 send
方法了。当 channel
为 SystemChannels.textInput
的时候,根据方法名字来拦截 TextInput.show
。
static const MethodChannel textInput = OptionalMethodChannel( 'flutter/textinput', JSONMethodCodec(), );
@override Future<ByteData?>? send(String channel, ByteData? message) async { if (channel == SystemChannels.textInput.name) { final MethodCall methodCall = SystemChannels.textInput.codec.decodeMethodCall(message); switch (methodCall.method) { case 'TextInput.show': // 处理是否需要滤过这次消息。 return SystemChannels.textInput.codec.encodeSuccessEnvelope(null); default: } } return origin.send(channel, message); }
现在交给我们最后问题就是怎么确定这次消息需要被拦截?当需要发送 TextInput.show
消息的时候,必定有某个 FocusNode
处于 Focus
的状态。那么可以根据这个 FocusNode
做区分。
我们定义个一个特别的 FocusNode
,并且定义好一个属性用于判断(也有那种需要随时改变是否需要拦截信息的需求)。
class TextInputFocusNode extends FocusNode { /// no system keyboard show /// if it's true, it stop Flutter Framework send `TextInput.show` message to Flutter Engine bool ignoreSystemKeyboardShow = true; }
这样子,我们就可以根据以下代码进行判断。
Future<ByteData?>? send(String channel, ByteData? message) async { if (channel == SystemChannels.textInput.name) { final MethodCall methodCall = SystemChannels.textInput.codec.decodeMethodCall(message); switch (methodCall.method) { case 'TextInput.show': final FocusNode? focus = FocusManager.instance.primaryFocus; if (focus != null && focus is TextInputFocusNode && focus.ignoreSystemKeyboardShow) { return SystemChannels.textInput.codec.encodeSuccessEnvelope(null); } break; default: } } return origin.send(channel, message); }
最后我们只需要为 TextField
传入这个特殊的 FocusNode
。
final TextInputFocusNode _focusNode = TextInputFocusNode()..debugLabel = 'YourTextField'; @override Widget build(BuildContext context) { return TextField( focusNode: _focusNode, ); }
画自己的键盘
这里主要讲一下,弹出和隐藏键盘的时机。你可以通过当前焦点的变化的时候,来显示或者隐藏自定义的键盘。
当你的自定义键盘能自己关闭,并且保存焦点不丢失的,你那还应该在 [TextField] 的 onTap
事件中,再次判断键盘是否显示。比如我写的例子中使用的是 showBottomSheet
方法,它是能通过 drag
来关闭自己的。
下面为一个简单的例子,完整的例子在 extended_text_field/no_keyboard.dart at master · fluttercandies/extended_text_field (github.com)
PersistentBottomSheetController<void>? _bottomSheetController; final TextInputFocusNode _focusNode = TextInputFocusNode()..debugLabel = 'YourTextField'; @override void initState() { super.initState(); _focusNode.addListener(_handleFocusChanged); } @override Widget build(BuildContext context) { return Scaffold( body: TextField( // you must use TextInputFocusNode focusNode: _focusNode, ), ); } void _onTextFiledTap() { if (_bottomSheetController == null) { _handleFocusChanged(); } } void _handleFocusChanged() { if (_focusNode.hasFocus) { // just demo, you can define your custom keyboard as you want _bottomSheetController = showBottomSheet<void>( context: FocusManager.instance.primaryFocus!.context!, // set false, if don't want to drag to close custom keyboard enableDrag: true, builder: (BuildContext b) { // your custom keyboard return Container(); }); // maybe drag close _bottomSheetController?.closed.whenComplete(() { _bottomSheetController = null; }); } else { _bottomSheetController?.close(); _bottomSheetController = null; } } @override void dispose() { _focusNode.removeListener(_handleFocusChanged); super.dispose(); }
当然,怎么实现自定义键盘,可以根据自己的情况来决定,比如如果你的键盘需要顶起布局的话,你完全可以写成下面的布局。
Column( children: <Widget>[ // 你的页面 Expanded(child: Container()), // 你的自定义键盘 Container(), ], );
结语
通过对 createBinaryMessenger
的重载,我们实现对系统键盘弹出的拦截,避免我们对官方代码的依赖。其实 SystemChannels
当中,还有些其他的系统的 channel
,我们也能通过相同的方式去对它们进行拦截,比如可以拦截按键。
static const BasicMessageChannel<Object?> keyEvent = BasicMessageChannel<Object?>( 'flutter/keyevent', JSONMessageCodec(), );
本文相关代码都在 extended_text_field | Flutter Package (flutter-io.cn) 。
最最后放上 Flutter Candies 全家桶,真香。
以上就是Flutter 阻止系统键盘弹出的优雅方式的详细内容,更多关于Flutter 阻止系统键盘弹出的资料请关注脚本之家其它相关文章!