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

5,453 阅读5分钟

序章

image

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

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

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

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

天命人疑惑: 此物如此厉害,不会还需要解压吧。

DM_20240826100807_001.JPEG

老猴子笑到:“不不不,你只需要运行 flutter pub add extended_keyboard, 或者直接手动添加 extended_keyboardpubspec.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;  

看描述,似乎很难区分 viewPaddingpadding 的区别,我们这里暂时按下不表,我们试着调试一下它们的值。

调试

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.bottom0346 viewPadding.bottom3434 padding.bottom340

  • 键盘关闭

    viewInsets.bottom3460 viewPadding.bottom3434 padding.bottom034

可以看出来,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.bottom0346 MediaQuery.of(context).viewPadding.bottom034 MediaQuery.of(context).padding.bottom 永远为 0

  • 键盘关闭

    MediaQuery.of(context).viewInsets.bottom3460 MediaQuery.of(context).viewPadding.bottom340 MediaQuery.of(context).padding.bottom 永远为 0

跟没有嵌套 SafeArea 的时候相比,viewPaddingpadding 的值变化的很奇怪。那为什么会这样呢? 我们来一起看看 SafeArea 的实现方式。

SafeAreabuild 方法体如下:

  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.removeBottomtrue 的时候,paddingbottom 值直接设置为了 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.bottom0346 MediaQuery.of(context).viewPadding.bottom34 - 3434 - 0034 MediaQuery.of(context).padding.bottom 永远为 0

  • 键盘关闭

    MediaQuery.of(context).viewInsets.bottom3460 MediaQuery.of(context).viewPadding.bottom34 - 034 - 34340 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') 了. 相关问题 github.com/flutter/flu…

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

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') 了. 相关问题 github.com/flutter/flu…

下面的代码是一种变通方案,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 的参数,该参数表示最后显示的系统键盘的高度。此参数可用于为您的自定义键盘设置适当的高度,从而确保无缝且直观的用户体验。

parameterdescriptiondefault
builder一个构建器函数,它根据系统键盘高度返回一个小部件。required
bodyBuilder一个带 readOnly 参数的组件回调required
resizeToAvoidBottomInsetScaffold.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,
                ),
              ),
            ],
          ),
        ]),
      ),
    ),
  );

c4603683-d52b-4b3e-88a0-22a1cfc3e58b.gif

Full Demo

TextInputScope

KeyboardBinding / KeyboardBindingMixin

你可以直接使用 KeyboardBinding ,或者将 KeyboardBindingMixin 混入到你的 WidgetsFlutterBinding 中。

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

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

parameterdescriptiondefault
getKeyboardHeight返回自定义键盘的高度required
builder包含输入框的主体required
keyboardName自定义键盘的名字required
showDuration自定义键盘打开的时间const Duration(milliseconds: 200)
hideDuration自定义键盘隐藏的时间const Duration(milliseconds: 200)
resizeToAvoidBottomInsetScaffold.resizeToAvoidBottomInset 一样的意思. 如果它不设置,将和 TextInputScope.resizeToAvoidBottomInset 的值相同null
  KeyboardConfiguration(
    getKeyboardHeight: (double? systemKeyboardHeight) =>
        systemKeyboardHeight ?? 346,
    builder: () {
      return Container();
    },
    keyboardName: 'custom_number1',
    resizeToAvoidBottomInset: true,
  ),
TextInputScope

如果使用 Scaffold,请确保将 Scaffold.resizeToAvoidBottomInset 设置为 false

parameterdescriptiondefault
body包含输入框的主体required
configurations自定义键盘配置required
keyboardHeight默认的自定义键盘高度346
resizeToAvoidBottomInsetScaffold.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,
        ),
      ),
    );
  }

text_input_demo.gif

Full Demo

扩展方法

TextEditingController 的扩展方法

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

终章

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

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

image.png

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

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