稀土掘金技术社区

Flutter: 天命人,来,送你个轮椅键盘!

点击 关注 公众号,“ 技术干货 ” 及时达!

序章

❝

“天命人,你终于来了。”

❞
❝

“你是谁?”天命人警觉地问道。

❞
❝

老猴子微微一笑:“我?只是个引路的老家伙罢了。至于你,是被选中的天命人,我将送你能够随心所欲定制的轮椅键盘,助你修行,随我来吧。”

❞
❝

老猴子熟练地打开了 p 站,天命人终于见到了传说中的能够随心所欲定制的轮椅键盘。它通体晶莹剔透,每一颗键都散发出独特的光芒,似乎可以感知使用者的意念。天命人轻轻一触,键盘立刻根据他的想法自动调整布局,符号和快捷键随着他的念头变幻莫测。

❞
❝ 天命人疑惑: 此物如此厉害,不会还需要 「解压」 吧。 ❞
❝ 老猴子笑到:“不不不,你只需要运行 flutter pub add extended_keyboard , 或者直接手动添加 extended_keyboard 到 pubspec.yaml 中的 dependencies 即可。” ❞
❝

天命人是懂非懂,终于忍不住问道:“老猴子,你为什么只引导我,却不告诉我如何做?”

❞
❝

老猴子笑了笑,缓缓离去,声音在风中飘来:“天命人,你的路还很长。前方的文档,需要你自己去读。”

❞

第一章(轮椅原理)

想在系统键盘和自定义键盘之间丝滑的切换,一个比较重要的点是,我们需要知道系统键盘的状态,让切换成自定义键盘的时候,动画不那么突兀。

系统键盘状态

在 Flutter 中想知道关于键盘的相关信息,我们必须先了解一下 WidgetsBinding.instance.window ( 在最近的 Flutter 版本中该 api 已经废弃,主要是为了适配多窗口。 后续正式删掉之后,再适配新的 api )。 它代表了 Flutter 应用运行的窗口。通过这个 window 对象,你可以访问到与窗口相关的各种属性和方法,比如窗口的尺寸、状态栏样式、导航栏样式等,以及执行一些与窗口相关的操作,比如设置窗口的标题、监听窗口的焦点变化等。

viewInsets

  /// 视图的填充区域,表示它与所在屏幕的 [Screen.viewInsets] 相交的部分。
  ///
  /// 例如,如果视图不与 [ScreenConfiguration.viewInsets] 区域重叠,[viewInsets]
  /// 将为 [WindowPadding.zero]。
  ///
  /// 该视图矩形每一边的物理像素数量,应用程序可以在这些区域内绘制内容,
  /// 但操作系统可能会在这些区域上放置系统 UI,例如键盘或系统菜单,
  /// 从而完全遮挡内容。
  final WindowPadding viewInsets;
window 的属性 viewInsets ,这部分表明,系统可能在这部分区域放置系统 ui ,比如键盘或者系统菜单,会挡住应用的内容。 WidgetsBinding.instance.window.viewInsets.bottom 即表明键盘的实时高度,当然,我们还需要除以设备像素与逻辑像素比例。 即键盘的实时高度等于 WidgetsBinding.instance.window.viewInsets.bottom / WidgetsBinding.instance.window.devicePixelRatio 。

viewPadding

  /// 视图的填充区域,表示它与所在屏幕的 [ScreenConfiguration.viewPadding] 相交的部分。
  ///
  /// 例如,如果视图不与 [ScreenConfiguration.viewPadding] 区域重叠,[viewPadding]
  /// 将为 [WindowPadding.zero]。
  ///
  /// 该屏幕矩形每一边的物理像素数量,应用程序可以在这些区域内放置视图,
  /// 但这些区域可能会被系统 UI(如系统通知栏)部分遮挡,或者被显示器的物理
  /// 侵入(例如电视屏幕的过扫描区域或手机传感器外壳)遮挡。
  final WindowPadding viewPadding;
另外一个影响键盘布局的是 window 的属性 viewPadding ,这个就是我们平时说的安全距离。 手机的底部安全距离等于 WidgetsBinding.instance.window.viewPadding.bottom / WidgetsBinding.instance.window.devicePixelRatio 。

padding

  /// 视图的填充区域,表示它与所在屏幕的 [ScreenConfiguration.padding] 相交的部分。
  ///
  /// 例如,如果视图不与 [ScreenConfiguration.padding] 区域重叠,[padding]
  /// 将为 [WindowPadding.zero]。
  ///
  /// 该屏幕矩形每一边的物理像素数量,应用程序可以在这些区域内放置视图,
  /// 但这些区域可能会被系统 UI(如系统通知栏)部分遮挡,或者被显示器的物理
  /// 侵入(例如电视屏幕的过扫描区域或手机传感器外壳)遮挡。
  final WindowPadding padding;  
看描述,似乎很难区分 viewPadding 和 padding 的区别,我们这里暂时按下不表,我们试着调试一下它们的值。

调试

在 iphone 模拟器中,我们尝试打印如下值:
  • keyboardHeight: MediaQuery.of(context).viewInsets.bottom
  • viewPadding: MediaQuery.of(context).viewPadding.bottom
  • padding: MediaQuery.of(context).padding.bottom

日志统一删掉了一些不影响结果的线性过程。

键盘打开
flutter: keyboardHeight:0.0------viewPadding:34.0-----padding:34.0
flutter: keyboardHeight:0.03791859337070491------viewPadding:34.0-----padding:33.962081406629295

flutter: keyboardHeight:0.3559106355533004------viewPadding:34.0-----padding:33.6440893644467

flutter: keyboardHeight:16.945799253880978------viewPadding:34.0-----padding:17.054200746119022

flutter: keyboardHeight:48.595437318086624------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:86.29385042190552------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:124.68643552064896------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:160.79388678073883------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:193.20682454109192------viewPadding:34.0-----padding:0.0

...

flutter: keyboardHeight:345.5162001848221------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:346.0------viewPadding:34.0-----padding:0.0

键盘收起
flutter: keyboardHeight:346.0------viewPadding:34.0-----padding:0.0
flutter: keyboardHeight:345.62914845510386------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:329.01456036418676------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:297.3947924375534------viewPadding:34.0-----padding:0.0
...

flutter: keyboardHeight:64.36210083961487------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:51.02358794212341------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:40.236825704574585------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:31.612241744995117------viewPadding:34.0-----padding:2.387758255004883

flutter: keyboardHeight:24.739083647727966------viewPadding:34.0-----padding:9.260916352272034

flutter: keyboardHeight:19.29748547077179------viewPadding:34.0-----padding:14.70251452922821

flutter: keyboardHeight:15.003506898880005------viewPadding:34.0-----padding:18.996493101119995

...

flutter: keyboardHeight:0.4856146574020386------viewPadding:34.0-----padding:33.51438534259796

flutter: keyboardHeight:0.0------viewPadding:34.0-----padding:34.0
小结
  • 键盘打开

    viewInsets.bottom 从 0 到 346 viewPadding.bottom 从 34 到 34 padding.bottom 从 34 到 0
  • 键盘关闭

    viewInsets.bottom 从 346 到 0 viewPadding.bottom 从 34 到 34 padding.bottom 从 0 到 34
可以看出来, viewPadding.bottom 似乎是一个定值,而 padding.bottom 会根据键盘是否开启做变化。

SafeArea

在实际项目中,我们不会直接从 WidgetsBinding.instance.window 中直接获取相关信息,而是通过 context 获取,这样的好处是,当 WidgetsBinding.instance.window 中的这些值发生改变的时候, context 所在的组件会自动重新 build 。
  • MediaQuery.of(context).viewInsets.bottom
  • MediaQuery.of(context).viewPadding.bottom
  • MediaQuery.of(context).padding.bottom
一般 App 设计都不希望 App 的内容绘制到安全距离的部分,我们通常会使用 SafeArea 组件,并且将 bottom 设置成 true 。 当我们的页面套上一层 SafeArea 组件,并且将 bottom 设置成 true 。 在 iphone 模拟器中,我们尝试打印如下值:
  • keyboardHeight: MediaQuery.of(context).viewInsets.bottom
  • viewPadding: MediaQuery.of(context).viewPadding.bottom
  • padding: MediaQuery.of(context).padding.bottom
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SafeArea(
        bottom: true,
        child: Column(
          children: <Widget>[
            Text(
              '(viewInsets): ${MediaQuery.of(context).viewInsets.bottom}',
            ),
            Text(
              '(viewPadding): ${MediaQuery.of(context).viewPadding.bottom}',
            ),
            Text(
              '(padding): ${MediaQuery.of(context).padding.bottom}',
            ),
            const Spacer(),
            const TextField(
              decoration: InputDecoration(
                hintText: '请输入内容',
              ),
            ),
          ],
        ),
      ),
    );
  }
键盘打开
flutter: keyboardHeight:0.0------viewPadding:0.0-----padding:0.0
flutter: keyboardHeight:0.7043539783917367------viewPadding:0.7043539783917367-----padding:0.0

flutter: keyboardHeight:19.02943018078804------viewPadding:19.02943018078804-----padding:0.0

...

flutter: keyboardHeight:345.5308014154434------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:346.0------viewPadding:34.0-----padding:0.0

键盘收起
flutter: keyboardHeight:346------viewPadding:34.0-----padding:0.0
flutter: keyboardHeight:343.4928252743557------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:321.89208702743053------viewPadding:34.0-----padding:0.0
...

flutter: keyboardHeight:37.85125684738159------viewPadding:34.0-----padding:0.0

flutter: keyboardHeight:29.701666593551636------viewPadding:29.701666593551636-----padding:0.0

flutter: keyboardHeight:23.22051441669464------viewPadding:23.22051441669464-----padding:0.0

...

flutter: keyboardHeight:0.4466574192047119------viewPadding:0.4466574192047119-----padding:0.0

flutter: keyboardHeight:0.0------viewPadding:0.0-----padding:0.0
小结
  • 键盘打开

    MediaQuery.of(context).viewInsets.bottom 从 0 到 346 MediaQuery.of(context).viewPadding.bottom 从 0 到 34 MediaQuery.of(context).padding.bottom 永远为 0
  • 键盘关闭

    MediaQuery.of(context).viewInsets.bottom 从 346 到 0 MediaQuery.of(context).viewPadding.bottom 从 34 到 0 MediaQuery.of(context).padding.bottom 永远为 0
跟没有嵌套 SafeArea 的时候相比, viewPadding 和 padding 的值变化的很奇怪。那为什么会这样呢? 我们来一起看看 SafeArea 的实现方式。 SafeArea 的 build 方法体如下:
  1. 通过 context 获取了上一层的 MediaQuery
  2. 根据 maintainBottomViewPadding 属性判断 padding 是从上一层的 padding 还是 viewPadding 中获取 bottom 。(该属性设置为 true 的实际效果为, 键盘弹起来的时候, Flutter 绘制的部分,跟键盘之间依然有安全距离。 根据之前调试的数据来看,确实如此)
  3. 包装一个 Padding 来承接用户的设置,并且包裹一层 MediaQuery.removePadding (看名字像是移除 padding )
为什么要移除掉 padding , 因为这个组件已经通过 Padding 组件承接了安全距离,如果不移除,下一层组件中,如果又有人通过 MediaQuery.of(context) 或者 SafeArea 去设置的话,就会造成重复。
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    final MediaQueryData data = MediaQuery.of(context);
    EdgeInsets padding = data.padding;
    // Bottom padding has been consumed - i.e. by the keyboard
    if (maintainBottomViewPadding)
      padding = padding.copyWith(bottom: data.viewPadding.bottom);

    return Padding(
      padding: EdgeInsets.only(
        left: math.max(left ? padding.left : 0.0, minimum.left),
        top: math.max(top ? padding.top : 0.0, minimum.top),
        right: math.max(right ? padding.right : 0.0, minimum.right),
        bottom: math.max(bottom ? padding.bottom : 0.0, minimum.bottom),
      ),
      child: MediaQuery.removePadding(
        context: context,
        removeLeft: left,
        removeTop: top,
        removeRight: right,
        removeBottom: bottom,
        child: child,
      ),
    );
  }

接下来我们看看为什么数值是这样变化的呢?

查看 MediaQuery.removePadding 实现如下: 1. removeBottom 为 true 的时候, padding 的 bottom 值直接设置为了 0 . 2. viewPadding 则等于 removeBottom ? math.max(0.0, viewPadding.bottom - padding.bottom
  MediaQueryData removePadding({
    bool removeLeft = false,
    bool removeTop = false,
    bool removeRight = false,
    bool removeBottom = false,
  }) {
    if (!(removeLeft || removeTop || removeRight || removeBottom))
      return this;
    return MediaQueryData(
      size: size,
      devicePixelRatio: devicePixelRatio,
      textScaleFactor: textScaleFactor,
      platformBrightness: platformBrightness,
      padding: padding.copyWith(
        left: removeLeft ? 0.0 : null,
        top: removeTop ? 0.0 : null,
        right: removeRight ? 0.0 : null,
        bottom: removeBottom ? 0.0 : null,
      ),
      viewPadding: viewPadding.copyWith(
        left: removeLeft ? math.max(0.0, viewPadding.left - padding.left) : null,
        top: removeTop ? math.max(0.0, viewPadding.top - padding.top) : null,
        right: removeRight ? math.max(0.0, viewPadding.right - padding.right) : null,
        bottom: removeBottom ? math.max(0.0, viewPadding.bottom - padding.bottom) : null,
      ),
      viewInsets: viewInsets,
      alwaysUse24HourFormat: alwaysUse24HourFormat,
      highContrast: highContrast,
      disableAnimations: disableAnimations,
      invertColors: invertColors,
      accessibleNavigation: accessibleNavigation,
      boldText: boldText,
      gestureSettings: gestureSettings,
      displayFeatures: displayFeatures,
    );
  }

综合上述日志和代码:

总结:

  • 键盘打开

    MediaQuery.of(context).viewInsets.bottom 从 0 到 346 MediaQuery.of(context).viewPadding.bottom 从 34 - 34 到 34 - 0 即 0 到 34 MediaQuery.of(context).padding.bottom 永远为 0
  • 键盘关闭

    MediaQuery.of(context).viewInsets.bottom 从 346 到 0 MediaQuery.of(context).viewPadding.bottom 从 34 - 0 到 34 - 34 即 34 到 0 MediaQuery.of(context).padding.bottom 永远为 0

总结

通过上述的日志分析和代码分析,可以得出,系统键盘的高度包含了安全距离,并且键盘开关也会影响安全距离的值。

那么我们的自定义键盘如果需要等系统键盘一样高,那么自定义键盘的高度等于多少呢?

可能有人会说,这不是废话吗? 不就是等于系统键盘的高度 MediaQuery.of(context).viewInsets.bottom 吗? 但是考虑到 MediaQuery.of(context).viewPadding.bottom 值的变化,我们最终得出公式应该是: 自定义键盘高度=系统键盘高度-固定的安全距离高度+变化的安全距离高度
double customSystemHeight = MediaQuery.of(context).viewInsets.bottom 
- (WidgetsBinding.instance.window.viewInsets.bottom /
WidgetsBinding.instance.window.devicePixelRatio)
+ MediaQuery.of(context).viewPadding.bottom;

系统键盘开和关

当系统键盘开与关的时候, Flutter 框架内部做了什么事情呢?其实在 Flutter 如何优雅地阻止系统键盘弹出 一文中,我们已经初探一二。 SystemChannels.textInput 是掌握 Flutter 跟原生代码通信的通道。 packages/flutter/lib/src/services/text_input.dart 这个文件中包含了对其处理的部分。
  TextInput._() {
    _channel = SystemChannels.textInput;
        _channel.setMethodCallHandler(_loudlyHandleTextInputInvocation);
  }

里面的方法很多,我们主要应该关心的是下面几个方法。

  • TextInput.setClient

当输入框获得焦点的时候,输入框最终会调用这个方法,将输入框的一些配置传递给原生,并且建立连接。

  void attach(TextInputClient client, TextInputConfiguration configuration) {
    _channel.invokeMethod<void>(
      'TextInput.setClient',
      <Object>[
        TextInput._instance._currentConnection!._id,
        _configurationToJson(configuration),
      ],
    );
  }
TextInputConfiguration 是输入框的配置,我们可以利用它,来区分该输入框是否是一个我们自定义的键盘,比如我们可以利用 TextInputType , 创建一个不同于系统的 TextInputType 。
  const TextInputConfiguration({
    this.inputType = TextInputType.text,
    this.readOnly = false,
    this.obscureText = false,
    this.autocorrect = true,
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
    this.enableSuggestions = true,
    this.enableInteractiveSelection = true,
    this.actionLabel,
    this.inputAction = TextInputAction.done,
    this.keyboardAppearance = Brightness.light,
    this.textCapitalization = TextCapitalization.none,
    this.autofillConfiguration = AutofillConfiguration.disabled,
    this.enableIMEPersonalizedLearning = true,
    this.enableDeltaModel = false,
  })
  • TextInput.clearClient
  @override
  void detach(TextInputClient client) {
    _channel.invokeMethod<void>('TextInput.clearClient');
  }
  • TextInput.show
  @override
  void show() {
    _channel.invokeMethod<void>('TextInput.show');
  }
  • TextInput.hide
  @override
  void hide() {
    _channel.invokeMethod<void>('TextInput.hide');
  }

第二章(轮椅设计)

在分析完毕一些必要的原理之后,我们来做一些设计工作。首先是我们需要一个什么样的组件或者说 api 。
  • 希望能够尽量简单易用
  • 尽量不要引入原生的代码(即希望是一个纯 Flutter api ),这样如果有新平台,也无需做平台适配。
  • 确保系统键盘和自定义键盘之间切换丝滑不突兀
基于上面的需求和原理,这里准备了 2 套轮椅来适应不同的场景。

SystemKeyboard

系统键盘和自定义键盘的切换的关键是,提前知道系统键盘的高度,这样切换的时候,视觉上面不会造成动画突兀感。

SystemKeyboard 是用来存储管理系统键盘高度的,并且会缓存到本地,让下一次打开 App 的时候,直接能拿到不同键盘类型的高度信息。

KeyboardBuilder

如果我们想要关闭系统键盘,并且保持输入框的不丢失焦点,我们没法再使用 SystemChannels.textInput.invokeMethod<void>('TextInput.hide') 了. 相关问题 https://github.com/flutter/flutter/issues/16863

下面的代码是一种变通方案

TextField(
  showCursor: true,
  readOnly: true,
)
「该组件适用于在同一个输入框展示不同的键盘效果的场景。」

TextInputScope

我们可以通过拦截系统通信来阻止系统键盘的弹出,并且根据 TextInput.setClient 来判断当前是什么自定义键盘的类型,来绘制出来当前的自定义键盘。 由于输入框在键盘当打开的状态下, 动态改变 TextInputType ,并不能触发 TextInput.setClient 或者 TextInput.updateConfig 。即同一个输入框在键盘打开的情况下,你没法通过改变 keyboardType 来改变键盘的样式。

Change keyboardType of TextField not work, if the keyboard is showing · Issue #154154 · flutter/flutter (github.com)

「该组件适用于输入框的 keyboardType(TextInputType) 不会动态发生改变的场景。一个页面上可以有多个自定义键盘的输入框和系统键盘的输入框」

第三章(躺上轮椅)

道理大家都懂了,那么怎么使用呢?

安装

运行 flutter pub add extended_keyboard , 或者直接手动添加 extended_keyboard 到 pubspec.yaml 中的 dependencies.
dependencies:
  extended_keyboard: ^latest_version

使用

SystemKeyboard

用于管理系统键盘的高度并提供处理键盘布局更改的功能。

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemKeyboard().init();
  runApp(const MyApp());
}

KeyboardBuilder

如果我们想要关闭系统键盘,并且保持输入框的不丢失焦点,我们没法再使用 SystemChannels.textInput.invokeMethod<void>('TextInput.hide') 了. 相关问题 https://github.com/flutter/flutter/issues/16863 下面的代码是一种变通方案, KeyboardBuilder 基于这种方式来实现的。
TextField(
  showCursor: true,
  readOnly: true,
)
KeyboardTypeBuilder
用于监听 KeyboardType 改变的组件,并且提供 CustomKeyboardController 来控制自定义键盘的开关。
   KeyboardTypeBuilder(
     builder: (
       BuildContext context,
       CustomKeyboardController controller,
     ) =>
         ToggleButton(
       builder: (bool active) => Icon(
         Icons.sentiment_very_satisfied,
         color: active ? Colors.orange : null,
       ),
       activeChanged: (bool active) {
         _keyboardPanelType = KeyboardPanelType.emoji;
         if (active) {
           controller.showCustomKeyboard();
           if (!_focusNode.hasFocus) {
             SchedulerBinding.instance
                 .addPostFrameCallback((Duration timeStamp) {
               _focusNode.requestFocus();
             });
           }
         } else {
           controller.showSystemKeyboard();
         }
       },
       active: controller.isCustom &&
           _keyboardPanelType == KeyboardPanelType.emoji,
     ),
   ),
CustomKeyboardController
用于通知 KeyboardType 改变,并且控制自定义键盘的开关。
  • KeyboardType : 当前键盘的类型
  • isCustom :  是否是自定义键盘
  • showCustomKeyboard : 打开自定义键盘
  • hideCustomKeyboard : 关闭自定义键盘
  • showSystemKeyboard : 打开系统键盘 (通过将 readOnly 设置成 false)
  • unfocus : 使输入框失去焦点, 并且关闭系统和自定义键盘
KeyboardBuilder
如果使用 Scaffold ,请确保将 Scaffold.resizeToAvoidBottomInset 设置为 false 。 使用 KeyboardBuilder 小部件来封装包含输入字段的区域,允许在其 builder 回调中创建自定义键盘布局。 builder 函数接收一个名为 systemKeyboardHeight 的参数,该参数表示最后显示的系统键盘的高度。此参数可用于为您的自定义键盘设置适当的高度,从而确保无缝且直观的用户体验。
parameter description default
builder 一个构建器函数,它根据系统键盘高度返回一个小部件。 required
bodyBuilder 一个带 readOnly 参数的组件回调 required
resizeToAvoidBottomInset 跟 Scaffold.resizeToAvoidBottomInset 作用一致 true
controller 自定义键盘控制器 null
  return Scaffold(
    resizeToAvoidBottomInset: false,
    appBar: AppBar(title: const Text('ChatDemo(KeyboardBuilder)')),
    body: SafeArea(
      bottom: true,
      child: KeyboardBuilder(
        resizeToAvoidBottomInset: true,
        builder: (BuildContext context, double? systemKeyboardHeight) {
          return Container();
        },
        bodyBuilder: (bool readOnly) => Column(children: <Widget>[
          Row(
            children: <Widget>[
              Expanded(
                child: TextField(
                  readOnly: readOnly, 
                  showCursor: true,
                  onTap: () {
                    _customKeyboardController.showSystemKeyboard();
                  },
                ),
              ),
              KeyboardTypeBuilder(
                builder: (
                  BuildContext context,
                  CustomKeyboardController controller,
                ) =>
                    ToggleButton(
                  builder: (bool active) => Icon(
                    Icons.sentiment_very_satisfied,
                    color: active ? Colors.orange : null,
                  ),
                  activeChanged: (bool active) {
                    _keyboardPanelType = KeyboardPanelType.emoji;
                    if (active) {
                      controller.showCustomKeyboard();
                      if (!_focusNode.hasFocus) {
                        SchedulerBinding.instance
                            .addPostFrameCallback((Duration timeStamp) {
                          _focusNode.requestFocus();
                        });
                      }
                    } else {
                      controller.showSystemKeyboard();
                    }
                  },
                  active: controller.isCustom &&
                      _keyboardPanelType == KeyboardPanelType.emoji,
                ),
              ),
            ],
          ),
        ]),
      ),
    ),
  );

Full Demo

TextInputScope

KeyboardBinding / KeyboardBindingMixin
你可以直接使用 KeyboardBinding ,或者将 KeyboardBindingMixin 混入到你的 WidgetsFlutterBinding 中。
Future<void> main() async {
  KeyboardBinding();
  await SystemKeyboard().init();
  runApp(const MyApp());
}
KeyboardConfiguration

这个配置包括键盘应该如何构建,它的动画持续时间,它的名字。

parameter description default
getKeyboardHeight 返回自定义键盘的高度 required
builder 包含输入框的主体 required
keyboardName 自定义键盘的名字 required
showDuration 自定义键盘打开的时间 const Duration(milliseconds: 200)
hideDuration 自定义键盘隐藏的时间 const Duration(milliseconds: 200)
resizeToAvoidBottomInset 跟 Scaffold.resizeToAvoidBottomInset 一样的意思. 如果它不设置,将和 TextInputScope.resizeToAvoidBottomInset 的值相同 null
  KeyboardConfiguration(
    getKeyboardHeight: (double? systemKeyboardHeight) =>
        systemKeyboardHeight ?? 346,
    builder: () {
      return Container();
    },
    keyboardName: 'custom_number1',
    resizeToAvoidBottomInset: true,
  ),
TextInputScope
如果使用 Scaffold ,请确保将 Scaffold.resizeToAvoidBottomInset 设置为 false 。
parameter description default
body 包含输入框的主体 required
configurations 自定义键盘配置 required
keyboardHeight 默认的自定义键盘高度 346
resizeToAvoidBottomInset 跟 Scaffold.resizeToAvoidBottomInset 的意思一样. true
  late List<KeyboardConfiguration> _configurations;
  @override
  void initState() {
    super.initState();
    _configurations = <KeyboardConfiguration>[
      KeyboardConfiguration(
        getKeyboardHeight: (double? systemKeyboardHeight) =>
            systemKeyboardHeight ?? 346,
        builder: () {
          return Container();
        },
        keyboardName: 'custom_number',
      ),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextInputDemo'),
      ),
      resizeToAvoidBottomInset: false,
      body: SafeArea(
        bottom: true,
        child: TextInputScope(
          body: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 5),
            child: Column(
              children: <Widget>[
                TextField(
                  keyboardType: _configurations[0].keyboardType,
                  controller: _controller,
                  decoration: InputDecoration(
                    hintText:
                        'The keyboardType is ${_configurations[0].keyboardType.name}',
                  ),
                ),
              ],
            ),
          ),
          configurations: _configurations,
        ),
      ),
    );
  }

Full Demo

扩展方法

TextEditingController 的扩展方法

  • void insertText(String text) 在当前位置插入文本
  • void delete() 删除一个字符
  • TextEditingValue deleteText() 删除一个字符并且返回删除之后的值,可以根据自己的情况再处理
  • void performAction(TextInputAction action) 跟 TextInputClient.performAction 一样的作用

终章

❝ “天命人,别忘了, Flutter 只是工具,真正的力量依然在你心中。” ❞
❝

“原来,这一切的力量,从未真正离开过我。”天命人低声自语。

❞
爱 Flutter ,爱 糖果 ,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果。

点击 关注 公众号,“ 技术干货 ” 及时达!