本文正在参加「金石方案 . 瓜分6万现金大奖」

前言

开篇先吐槽一下,输入框和文本,一向都是官方每个版别改动的重点,先不说功能上全不全的问题,每次版别升级,必有 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();
  }

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

当然,怎么完成自界说键盘,能够依据自己的情况来决议,比方假如你的键盘需求顶起布局的话,你彻底能够写成下面的布局。

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小糖块QQ群:181398081

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

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