美文网首页
Flutter 如何优雅地阻止系统键盘弹出

Flutter 如何优雅地阻止系统键盘弹出

作者: 我爱田Hebe | 来源:发表于2022-11-22 10:43 被阅读0次

    前言

    开篇先吐槽一下,输入框和文本,一直都是官方每个版本改动的重点,先不说功能上全不全的问题,每次版本升级,必有 breaking change 。对于 extended_text_field | Flutter Package (flutter-io.cn)extended_text | Flutter Package (flutter-io.cn) 来说,新功能都是基于官方的代码,每次版本升级,merge 代码就一个字,头痛,已经有了躺平的想法了。(暂时不 merge 了,能运行就行,等一个稳定点的官方版本,准备做个重构,重构一个相对更好 merge 代码的结构。)

    系统键盘弹出的原因

    吐槽完毕,我们来看一个常见的场景,就是自定义键盘。要想显示自己自定义的键盘,那么必然需要隐藏系统的键盘。方法主要有如下:

    1. 在合适的时机调用,SystemChannels.textInput.invokeMethod<void>('TextInput.hide')
    2. 系统键盘为啥会弹出来,是因为某些代码调用了 SystemChannels.textInput.invokeMethod<void>('TextInput.show'),那么我们可以魔改官方代码, 把 TextFieldEditableText 的代码复制出来。

    EditableTextState 代码中有一个 TextInputConnection? _textInputConnection;,它会在有需要的时候调用 show 方法。

    TextInputConnectionshow,如下。

      /// 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') 的调用,但是我们不可能去每个调用该方法地方去做处理,那么这个方法执行后续,我们有办法拦截吗? 答案当然是有的。

    FlutterFramework 层发送信息 TextInput.showFlutter 引擎是通过 MethodChannel, 而我们可以通过重载 WidgetsFlutterBindingcreateBinaryMessenger 方法来处理FlutterFramework 层通过 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());
     }
    

    BinaryMessenger3 个方法需要重载.

    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

    FlutterFramework 层发送信息到 Flutter 引擎,会走这个方法,这也是我们需要的处理的方法。

    • setMessageHandler

    Flutter 引擎 发送信息到 FlutterFramework 层的回调。在我们的场景中不用处理。

    • handlePlatformMessage

    sendsetMessageHandler 二和一,看了下注释,似乎是服务于 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 方法了。当 channelSystemChannels.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,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果[[图片上传失败...(image-7f602a-1669171322207)]

    QQ群:181398081](https://link.juejin.cn?target=https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3D5bcc0gy "https://jq.qq.com/?_wv=1027&k=5bcc0gy")

    最最后放上 Flutter Candies 全家桶,真香。

    作者:法的空间
    链接:https://juejin.cn/post/7166046328609308685

    相关文章

      网友评论

          本文标题:Flutter 如何优雅地阻止系统键盘弹出

          本文链接:https://www.haomeiwen.com/subject/gppoxdtx.html