详解Flutter自定义应用程序内键盘的实现方法


Posted in Java/Android onJune 14, 2022

本文将向您展示如何创建自定义键盘小部件,用于在您自己的应用程序中的Flutter TextField中输入文本。使用案例包括特殊字符或语言的文本输入,其中系统键盘可能不存在或用户可能没有安装正确的键盘。

我们今天将制作一个更简单的版本:

详解Flutter自定义应用程序内键盘的实现方法

详解Flutter自定义应用程序内键盘的实现方法

注意 :本文不会告诉您如何构建用户在任何应用程序中安装和使用的系统键盘。这只是一种基于小部件的方法,可以在您自己的应用程序中使用。

完整的代码在文章的底部。

创建关键小部件

Flutter的优点是,通过组合更简单的小部件,可以轻松构建键盘等复杂布局。首先,您将创建几个简单的按键小部件。

文本键

我已经圈出了由您首先制作的TextKey小部件制作的键。

详解Flutter自定义应用程序内键盘的实现方法

显示文本键(包括空格键)的自定义写意红色圆圈

将以下TextKey小部件添加到您的项目中:

class TextKey extends StatelessWidget {
  const TextKey({
    Key key,
    @required this.text,
    this.onTextInput,
    this.flex = 1,
  }) : super(key: key);
  final String text;
  final ValueSetter<String> onTextInput;
  final int flex;
  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: Padding(
        padding: const EdgeInsets.all(1.0),
        child: Material(
          color: Colors.blue.shade300,
          child: InkWell(
            onTap: () {
              onTextInput?.call(text);
            },
            child: Container(
              child: Center(child: Text(text)),
            ),
          ),
        ),
      ),
    );
  }
}

以下是有趣的部分:

  • flex属性允许您的按键均匀分布在一行之间,甚至占据行的更大比例(如上图中的空格键)。
  • 按下按键后,它将以anonTextInput回调的形式将其值传递给键盘。

Backspace键

您还需要一个与TextKey小部件具有不同外观和功能的退格键。

详解Flutter自定义应用程序内键盘的实现方法

退格键

将以下小部件添加到您的项目中:

​
class BackspaceKey extends StatelessWidget {
  const BackspaceKey({
    Key? key,
    this.onBackspace,
    this.flex = 1,
  }) : super(key: key);
​
  final VoidCallback? onBackspace;
  final int flex;
​
  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: Padding(
        padding: const EdgeInsets.all(1.0),
        child: Material(
          color: Colors.blue.shade300,
          child: InkWell(
            onTap: () {
              onBackspace?.call();
            },
            child: Container(
              child: Center(
                child: Icon(Icons.backspace),
              ),
            ),
          ),
        ),
      ),
    );
  }

备注:

  • TextKey代码有点重复,因此一些重构是为了使其更加简介。
  • onBackspaceVoidCallback,因为不需要将任何文本传递回键盘。

将按键组成键盘

一旦有了按键,键盘就很容易布局,因为它们只是列中的行。

详解Flutter自定义应用程序内键盘的实现方法

包含三行的列

这是代码。我省略了一些重复的部分,以便简洁。不过,你可以在文章的末尾找到它。

​
class CustomKeyboard extends StatelessWidget {
  CustomKeyboard({
    Key? key,
    this.onTextInput,
    this.onBackspace,
  }) : super(key: key);
​
  final ValueSetter<String>? onTextInput;
  final VoidCallback? onBackspace;
​
  void _textInputHandler(String text) => onTextInput?.call(text);
​
  void _backspaceHandler() => onBackspace?.call();
​
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 160,
      color: Colors.blue,
      child: Column(
        children: [
          buildRowOne(),
          buildRowTwo(),
          buildRowThree(),
          buildRowFour(),
          buildRowFive()
        ],
      ),
    );
  }
​
  Expanded buildRowOne() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '坚',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '果',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '祝',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }
​
  Expanded buildRowTwo() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: 'I',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'n',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'f',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'o',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'Q',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }
​
  Expanded buildRowThree() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '十',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '五',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '周',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '年',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }
​
  Expanded buildRowFour() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '生',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '日',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '快',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '乐',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '!',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }
​
  Expanded buildRowFive() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: ' ?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: ' ?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          BackspaceKey(
            onBackspace: _backspaceHandler,
          ),
        ],
      ),
    );
  }
}

有趣的部分:

  • 键盘收集按键的回调并传递它们。这样,任何使用CustomKeyboard的人都会收到回调。
  • 您可以看到第三行如何使用flex。空格键的弯曲为4,而退格的默认弯曲为1。这使得空格键占用了后空键宽度的四倍。

在应用程序中使用键盘

现在,您可以像这样使用自定义键盘小部件:

详解Flutter自定义应用程序内键盘的实现方法

代码看起来是这样的:

CustomKeyboard(
  onTextInput: (myText) {
    _insertText(myText);
  },
  onBackspace: () {
    _backspace();
  },
),

处理文本输入

以下是_insertText方法的样子:

void _insertText(String myText) {
  final text = _controller.text;
  final textSelection = _controller.selection;
  final newText = text.replaceRange(
    textSelection.start,
    textSelection.end,
    myText,
  );
  final myTextLength = myText.length;
  _controller.text = newText;
  _controller.selection = textSelection.copyWith(
    baseOffset: textSelection.start + myTextLength,
    extentOffset: textSelection.start + myTextLength,
  );
}

_controllerTextFieldTextEditingController。你必须记住,可能有一个选择,所以如果有的话,请用密钥传递的文本替换它。

感谢这个,以提供帮助。*

处理退格

您会认为退格很简单,但有一些不同的情况需要考虑:

  • 有一个选择(删除选择)
  • 光标在开头(什么都不要做)
  • 其他任何事情(删除之前的角色)

以下是_backspace方法的实现:

void _backspace() {
  final text = _controller.text;
  final textSelection = _controller.selection;
  final selectionLength = textSelection.end - textSelection.start;
  // There is a selection.
  if (selectionLength > 0) {
    final newText = text.replaceRange(
      textSelection.start,
      textSelection.end,
      '',
    );
    _controller.text = newText;
    _controller.selection = textSelection.copyWith(
      baseOffset: textSelection.start,
      extentOffset: textSelection.start,
    );
    return;
  }
  // The cursor is at the beginning.
  if (textSelection.start == 0) {
    return;
  }
  // Delete the previous character
  final previousCodeUnit = text.codeUnitAt(textSelection.start - 1);
  final offset = _isUtf16Surrogate(previousCodeUnit) ? 2 : 1;
  final newStart = textSelection.start - offset;
  final newEnd = textSelection.start;
  final newText = text.replaceRange(
    newStart,
    newEnd,
    '',
  );
  _controller.text = newText;
  _controller.selection = textSelection.copyWith(
    baseOffset: newStart,
    extentOffset: newStart,
  );
}
bool _isUtf16Surrogate(int value) {
  return value & 0xF800 == 0xD800;
}

即使删除之前的角色也有点棘手。如果您在有表情符号或其他代理对时只回退单个代码单元这将导致崩溃。作为上述代码中的变通办法,我检查了上一个字符是否是UFT-16代理,如果是,则后退了两个字符。(我从Flutter TextPainter源代码中获得了_isUtf16Surrogate方法。)然而,这仍然不是一个完美的解决方案,因为它不适用于像??或?‍?‍?这样的字素簇,它们由多个代理对组成。不过,至少它不会

以下是象形文字和表情符号键盘作为演示:

详解Flutter自定义应用程序内键盘的实现方法

???‍?‍

如果您对此有意见,请参阅此堆栈溢出问题

防止系统键盘显示

如果您想将自定义键盘与aTextField一起使用,但系统键盘不断弹出,那会有点烦人。这毕竟是默认行为。

防止系统键盘显示的方法是将TextFieldreadOnly属性设置为true

TextField(
  ...
  showCursor: true,
  readOnly: true,
),

此外,将showCursor设置为true,使光标在您使用自定义键盘时仍然可以工作。

在系统键盘和自定义键盘之间切换

如果您想让用户选择使用系统键盘或自定义键盘,您只需为readOnly使用不同的值进行重建。

详解Flutter自定义应用程序内键盘的实现方法

以下是演示应用程序中TextField的设置方式:

class _KeyboardDemoState extends State<KeyboardDemo> {
  TextEditingController _controller = TextEditingController();
  bool _readOnly = true;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: Column(
        children: [
          ...
          TextField(
            controller: _controller,
            decoration: ...,
            style: TextStyle(fontSize: 24),
            autofocus: true,
            showCursor: true,
            readOnly: _readOnly,
          ),
          IconButton(
            icon: Icon(Icons.keyboard),
            onPressed: () {
              setState(() {
                _readOnly = !_readOnly;
              });
            },
          ),

有趣的部分:

  • 当按下键盘IconButton时,更改_readOnly的值,然后重建布局。这会导致系统键盘隐藏或显示。
  • Scaffold上的resizeToAvoidBottomInset设置为false,允许系统键盘覆盖自定义键盘。另一个选项是在显示系统键盘时隐藏自定义键盘。然而,当我在实验中这样做时,我发现我必须使用单独的布尔值来隐藏自定义键盘,这样我就可以延迟显示它,直到系统键盘消失。否则,它会跳到系统键盘顶部一秒钟。

就这样!如您所见,制作自己的应用程序内键盘并不难。

完整代码

以下是我在本文中使用的演示应用程序的完整代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: KeyboardDemo(),
    );
  }
}

class KeyboardDemo extends StatefulWidget {
  @override
  _KeyboardDemoState createState() => _KeyboardDemoState();
}

class _KeyboardDemoState extends State<KeyboardDemo> {
  TextEditingController _controller = TextEditingController();
  bool _readOnly = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("大前端之旅的自定义键盘"),
      ),
      resizeToAvoidBottomInset: false,
      body: Column(
        children: [
          Text("微信:xjg13690"),
          SizedBox(height: 50),
          TextField(
            controller: _controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(3),
              ),
            ),
            style: TextStyle(fontSize: 24),
            autofocus: true,
            showCursor: true,
            readOnly: _readOnly,
          ),
          IconButton(
            icon: Icon(Icons.keyboard),
            onPressed: () {
              setState(() {
                _readOnly = !_readOnly;
              });
            },
          ),
          Spacer(),
          CustomKeyboard(
            onTextInput: (myText) {
              _insertText(myText);
            },
            onBackspace: () {
              _backspace();
            },
          ),
        ],
      ),
    );
  }

  void _insertText(String myText) {
    final text = _controller.text;
    final textSelection = _controller.selection;
    final newText = text.replaceRange(
      textSelection.start,
      textSelection.end,
      myText,
    );
    final myTextLength = myText.length;
    _controller.text = newText;
    _controller.selection = textSelection.copyWith(
      baseOffset: textSelection.start + myTextLength,
      extentOffset: textSelection.start + myTextLength,
    );
  }

  void _backspace() {
    final text = _controller.text;
    final textSelection = _controller.selection;
    final selectionLength = textSelection.end - textSelection.start;

    // There is a selection.
    if (selectionLength > 0) {
      final newText = text.replaceRange(
        textSelection.start,
        textSelection.end,
        '',
      );
      _controller.text = newText;
      _controller.selection = textSelection.copyWith(
        baseOffset: textSelection.start,
        extentOffset: textSelection.start,
      );
      return;
    }

    // The cursor is at the beginning.
    if (textSelection.start == 0) {
      return;
    }

    // Delete the previous character
    final previousCodeUnit = text.codeUnitAt(textSelection.start - 1);
    final offset = _isUtf16Surrogate(previousCodeUnit) ? 2 : 1;
    final newStart = textSelection.start - offset;
    final newEnd = textSelection.start;
    final newText = text.replaceRange(
      newStart,
      newEnd,
      '',
    );
    _controller.text = newText;
    _controller.selection = textSelection.copyWith(
      baseOffset: newStart,
      extentOffset: newStart,
    );
  }

  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class CustomKeyboard extends StatelessWidget {
  CustomKeyboard({
    Key? key,
    this.onTextInput,
    this.onBackspace,
  }) : super(key: key);

  final ValueSetter<String>? onTextInput;
  final VoidCallback? onBackspace;

  void _textInputHandler(String text) => onTextInput?.call(text);

  void _backspaceHandler() => onBackspace?.call();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 160,
      color: Colors.blue,
      child: Column(
        children: [
          buildRowOne(),
          buildRowTwo(),
          buildRowThree(),
          buildRowFour(),
          buildRowFive()
        ],
      ),
    );
  }

  Expanded buildRowOne() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '坚',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '果',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '祝',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }

  Expanded buildRowTwo() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: 'I',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'n',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'f',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'o',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: 'Q',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }

  Expanded buildRowThree() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '十',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '五',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '周',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '年',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }

  Expanded buildRowFour() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: '生',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '日',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '快',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '乐',
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '!',
            onTextInput: _textInputHandler,
          ),
        ],
      ),
    );
  }

  Expanded buildRowFive() {
    return Expanded(
      child: Row(
        children: [
          TextKey(
            text: ' ?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: ' ?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          TextKey(
            text: '?',
            flex: 2,
            onTextInput: _textInputHandler,
          ),
          BackspaceKey(
            onBackspace: _backspaceHandler,
          ),
        ],
      ),
    );
  }
}

class TextKey extends StatelessWidget {
  const TextKey({
    Key? key,
    @required this.text,
    this.onTextInput,
    this.flex = 1,
  }) : super(key: key);

  final String? text;
  final ValueSetter<String>? onTextInput;
  final int flex;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: Padding(
        padding: const EdgeInsets.all(1.0),
        child: Material(
          color: Colors.blue.shade300,
          child: InkWell(
            onTap: () {
              onTextInput?.call(text!);
            },
            child: Container(
              child: Center(child: Text(text!)),
            ),
          ),
        ),
      ),
    );
  }
}

class BackspaceKey extends StatelessWidget {
  const BackspaceKey({
    Key? key,
    this.onBackspace,
    this.flex = 1,
  }) : super(key: key);

  final VoidCallback? onBackspace;
  final int flex;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: Padding(
        padding: const EdgeInsets.all(1.0),
        child: Material(
          color: Colors.blue.shade300,
          child: InkWell(
            onTap: () {
              onBackspace?.call();
            },
            child: Container(
              child: Center(
                child: Icon(Icons.backspace),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

以上就是详解Flutter自定义应用程序内键盘的实现方法的详细内容,更多关于Flutter自定义键盘的资料请关注三水点靠木其它相关文章!


Tags in this post...

Java/Android 相关文章推荐
解决Maven项目中 Invalid bound statement 无效的绑定问题
Jun 15 Java/Android
JVM入门之类加载与字节码技术(类加载与类的加载器)
Jun 15 Java/Android
Feign调用传输文件异常的解决
Jun 24 Java/Android
Springboot集成阿里云OSS上传文件系统教程
Jun 28 Java/Android
SpringBoot整合RabbitMQ的5种模式实战
Aug 02 Java/Android
深入解读Java三大集合之map list set的用法
Nov 11 Java/Android
springboot+zookeeper实现分布式锁
Mar 21 Java/Android
剑指Offer之Java算法习题精讲二叉树专项训练
Mar 21 Java/Android
Java后端 Dubbo retries 超时重试机制的解决方案
Apr 14 Java/Android
Java 写一个简单的图书管理系统
Apr 26 Java/Android
利用Java连接Hadoop进行编程
Jun 28 Java/Android
Spring Boot优化后启动速度快到飞起技巧示例
Jul 23 Java/Android
ConditionalOnProperty配置swagger不生效问题及解决
Jun 14 #Java/Android
Java异常体系非正常停止和分类
Android开发手册TextInputLayout样式使用示例
Jun 10 #Java/Android
Java实现简单小画板
Android开发EditText禁止输入监听及InputFilter字符过滤
Jun 10 #Java/Android
详解Spring Bean的配置方式与实例化
Jun 10 #Java/Android
Spring JPA 增加字段执行异常问题及解决
Jun 10 #Java/Android
You might like
一个很不错的PHP翻页类
2009/06/01 PHP
PHP 递归效率分析
2009/11/24 PHP
php文件上传、下载和删除示例
2020/08/28 PHP
PHP编程文件处理类SplFileObject和SplFileInfo用法实例分析
2017/07/22 PHP
JavaScript Timer实现代码
2010/02/17 Javascript
关于jQuery object and DOM element
2013/04/15 Javascript
使用firebug进行调试javascript的示例
2013/12/16 Javascript
javascript基于HTML5 canvas制作画箭头组件
2014/06/25 Javascript
3kb jQuery代码搞定各种树形选择的实现方法
2016/06/10 Javascript
js插件Jcrop自定义截取图片功能
2016/10/14 Javascript
原生js实现倒计时功能(多种格式调用)
2017/01/12 Javascript
分享19个JavaScript 有用的简写写法
2017/07/07 Javascript
vue移动端下拉刷新和上拉加载的实现代码
2018/09/08 Javascript
Vue.js的动态组件模板的实现
2018/11/26 Javascript
vue中如何实现后台管理系统的权限控制的方法步骤
2019/09/05 Javascript
vue-路由精讲 二级路由和三级路由的作用
2020/08/06 Javascript
vue v-on:click传递动态参数的步骤
2020/09/11 Javascript
[06:45]DOTA2-DPC中国联赛 正赛 Magma vs LBZS 选手采访
2021/03/11 DOTA
如何用canvas实现在线签名的示例代码
2018/07/10 HTML / CSS
英国婚礼商城:Wedding Mall
2019/11/02 全球购物
澳大利亚著名的纺织品品牌:Canningvale
2020/05/05 全球购物
大学生创业计划书的格式要求
2013/12/29 职场文书
元旦联欢会主持词
2014/03/26 职场文书
安全生产专项整治方案
2014/05/06 职场文书
运动会广播稿诗歌版
2014/09/12 职场文书
工商局领导班子存在的问题整改措施思想汇报
2014/10/05 职场文书
安全先进个人材料
2014/12/29 职场文书
2015年出纳个人工作总结
2015/04/02 职场文书
大学生实习证明
2015/06/16 职场文书
小学运动会通讯稿
2015/07/18 职场文书
中考百日冲刺决心书
2015/09/22 职场文书
致男子1500米运动员的广播稿
2019/11/08 职场文书
Python破解极验滑动验证码详细步骤
2021/05/21 Python
Redis 限流器
2022/05/15 Redis
Pandas实现批量拆分与合并Excel的示例代码
2022/05/30 Python
教你使用Ubuntu搭建DNS服务器
2022/09/23 Servers