Flutter 安全键盘

在 Flutter 中目前官方没有提供快速自定义键盘的解决方案。

但在项目中以及 ICP 测评需要用到安全键盘,比如金额输入、安全密码输入、各种自定义快速输入键盘。

这里以Packages的形式实现了一个自定义安全键盘,已上传 Pub。

实现功能

  1. 适用原生输入框控件
  2. 支持一个页面多个安全键盘输入框
  3. 支持系统键盘与安全键盘混合使用
  4. 支持自动定位到输入框位置

实现思路

主要是通过拦截PlatformChannel实现的,可以无缝对接TextFiled等Flutter自带的输入框,监听输入框各种状态,以及内容变化。

关键代码讲解

  • KeyboardManager

自定义键盘管理类,包括输入类型与键盘的管理,以及输入框拦截、悬浮自定义键盘及生命周期管理等。
主要功能方法 init。

///初始化键盘监听并且传递当前页面的context
static init(BuildContext context) {
  _context = context;
  interceptorInput();
}

///拦截键盘交互
static interceptorInput() {
  if (isInterceptor) return;
  isInterceptor = true;
  BinaryMessages.setMockMessageHandler("flutter/textinput",
      (ByteData data) async {
    var methodCall = _codec.decodeMethodCall(data);
    switch (methodCall.method) {
    ...
    
    return response;
  });
}


在init 方法中对输入框交互进行拦截。

当输入框获焦时会调用TextInput.setClient、TextInput.show。


 case 'TextInput.show':
        if (_currentKeyboard != null) {
          openKeyboard();
          return _codec.encodeSuccessEnvelope(null);
        } else {
          return await _sendPlatformMessage("flutter/textinput", data);
        }
        break
 case 'TextInput.setClient':
        ...
        brea

在TextInput.setClient中根据输入框定义的键盘类型找出对应键盘对应的键盘配置、InputClient,以及初始输入变更监听KeyboardController。
在TextInput.show根据配置的键盘类型,调用 openKeyboard 生成键盘控件。

///显示键盘
static openKeyboard() {
  ///键盘已经打开
  if (_keyboardEntry != null) return;

  _pageKey = GlobalKey<KeyboardPageState>();
  _keyboardHeight = _currentKeyboard.getHeight();

  ///根据键盘高度,使键盘滚动到输入框位置
  KeyboardMediaQueryState queryState = _context
          .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
      as KeyboardMediaQueryState;
  queryState.update();

  ...

  ///往Overlay中插入插入OverlayEntry
  Overlay.of(_context).insert(_keyboardEntry);
}

这里通过 Overlay显示一个悬浮窗,并键盘弹出后,滚动到输入框位置。

当输入框输入时,会调用TextInput.setEditingState。

case 'TextInput.setEditingState':
        var editingState = TextEditingValue.fromJSON(methodCall.arguments);
        if (editingState != null && _keyboardController != null) {
          _keyboardController.value = editingState;
          return _codec.encodeSuccessEnvelope(null);
        }
        break

这里设置 KeyboardController 的TextEditingValue,就可以监听输入框的输入变化。

当切换焦点、或自定义键盘输入完成关闭时,会调用TextInput.clearClient、TextInput.hide。

     case 'TextInput.hide':
        if (_currentKeyboard != null) {
          hideKeyboard();
          return _codec.encodeSuccessEnvelope(null);
        } else {
          return await _sendPlatformMessage("flutter/textinput", data);
        }
        break;

      ///切换输入框时,会调用该回调,切换时键盘会隐藏。
      case 'TextInput.clearClient':
        hideKeyboard(animation: false);
        clearKeyboard();
        break

在 clearClient 中清除 InputClient,且隐藏键盘。

  • KeyboardController

键盘输入变更监听,可以参考系统[TextEditingController],主要通过ValueNotifier监听TextEditingValue的值变化,ValueNotifier是一个包含单个值的变更通知器,当它的值改变的时候,会通知它的监听。
扩展了 addText,deleteText,doneAction

///删除一个字符,一般用于键盘的删除键
deleteOne() {
  if (selection.baseOffset == 0) return;
  String newText = '';
  if (selection.baseOffset != selection.extentOffset) {
    newText = selection.textBefore(text) + selection.textAfter(text);
    value = TextEditingValue(
        text: newText,
        selection: selection.copyWith(
            baseOffset: selection.baseOffset,
            extentOffset: selection.baseOffset));
  } else {
    newText = text.substring(0, selection.baseOffset - 1) +
        selection.textAfter(text);
    value = TextEditingValue(
        text: newText,
        selection: selection.copyWith(
            baseOffset: selection.baseOffset - 1,
            extentOffset: selection.baseOffset - 1));
  }
}
/// 在光标位置添加文字,一般用于键盘输入
addText(String insertText) {
  String newText =
      selection.textBefore(text) + insertText + selection.textAfter(text);
  value = TextEditingValue(
      text: newText,
      selection: selection.copyWith(
          baseOffset: selection.baseOffset + insertText.length,
          extentOffset: selection.baseOffset + insertText.length));
}
/// 完成
doneAction() {
  KeyboardManager.sendPerformAction(TextInputAction.done);
}

  • KeyboardMediaQuery

用于键盘弹出的时候控制页面边间,使输入框不被挡住,自动定位到输入框位置。

class KeyboardMediaQuery extends StatefulWidget {
  final Widget child;

  KeyboardMediaQuery({this.child}) : assert(child != null);

  @override
  State<StatefulWidget> createState() => KeyboardMediaQueryState();
}

class KeyboardMediaQueryState extends State<KeyboardMediaQuery> {
  @override
  Widget build(BuildContext context) {
    var data = MediaQuery.of(context);

    ///消息传递,更新控件边距
    return MediaQuery(
        child: widget.child,
        data: data.copyWith(
            viewInsets: data.viewInsets
                .copyWith(bottom: KeyboardManager.keyboardHeight)));
  }

  ///通知更新
  void update() {
    setState(() => {});
  }
}

使用方法

  • Step1

在项目pubspec.yaml添加安全键盘依赖

dependencies:
  security_keyboard: ^1.0.2

  • Step2

根据项目需求编写个性化键盘

typedef KeyboardSwitch = Function(SecurityKeyboardType type);

enum SecurityKeyboardType {
  text,
  textUpperCase,
  number,
  numberOnly,
  numberSimple,
  symbol
}

class SecurityKeyboard extends StatefulWidget {
  ///用于控制键盘输出的Controller
  final KeyboardController controller;

  ///键盘类型,默认文本
  final SecurityKeyboardType keyboardType;

  ///定义InputType类型
  static const SecurityTextInputType inputType =
      const SecurityTextInputType(name: 'SecurityKeyboardInputType');

  SecurityKeyboard({this.controller, this.keyboardType});

  ///文本输入类型
  static SecurityTextInputType text =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.text);

  ///数字输入类型
  static SecurityTextInputType number =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.number);

  ///仅数字输入类型
  static SecurityTextInputType numberOnly =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.numberOnly);

  ///仅数字输入类型,且没有键盘提示
  static SecurityTextInputType numberSimple =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.numberSimple);

  ///初始化键盘类型,返回输入框类型
  static SecurityTextInputType _inputKeyboard(
      SecurityKeyboardType securityKeyboardType) {
    ///注册键盘的方法
    String inputType = securityKeyboardType.toString();
    SecurityTextInputType securityTextInputType =
        SecurityTextInputType(name: inputType);
    KeyboardManager.addKeyboard(
      securityTextInputType,
      KeyboardConfig(
        builder: (context, controller) {
          return SecurityKeyboard(
            controller: controller,
            keyboardType: securityKeyboardType,
          );
        },
        getHeight: () {
          return SecurityKeyboard.getHeight(securityKeyboardType);
        },
      ),
    );

    return securityTextInputType;
  }

  ///键盘类型
  SecurityKeyboardType get _keyboardType => keyboardType;

  ///编写获取高度的方法
  static double getHeight(SecurityKeyboardType securityKeyboardType) {
    return securityKeyboardType == SecurityKeyboardType.numberSimple
        ? LcfarmSize.dp(192)
        : LcfarmSize.dp(232);
  }


  @override
  _SecurityKeyboardState createState() => _SecurityKeyboardState();
}

class _SecurityKeyboardState extends State<SecurityKeyboard> {
  ///当前键盘类型
  SecurityKeyboardType currentKeyboardType;

  @override
  void initState() {
    super.initState();
    currentKeyboardType = widget._keyboardType;
  }

  @override
  Widget build(BuildContext context) {
    Widget keyboard;
    switch (currentKeyboardType) {
      case SecurityKeyboardType.number:
        keyboard = NumberKeyboard(
          widget.controller,
          currentKeyboardType,
          keyboardSwitch: (SecurityKeyboardType keyboardType) {
            setState(() {
              currentKeyboardType = keyboardType;
            });
          },
        );
        break;
      case SecurityKeyboardType.numberOnly:
      case SecurityKeyboardType.numberSimple:
        keyboard = NumberKeyboard(widget.controller, currentKeyboardType);
        break;
      case SecurityKeyboardType.symbol:
        keyboard = SymbolKeyboard(widget.controller,
            (SecurityKeyboardType keyboardType) {
          setState(() {
            currentKeyboardType = keyboardType;
          });
        });
        break;
      case SecurityKeyboardType.text:
      case SecurityKeyboardType.textUpperCase:
        keyboard = AlphabetKeyboard(widget.controller, currentKeyboardType,
            (SecurityKeyboardType keyboardType) {
          setState(() {
            currentKeyboardType = keyboardType;
          });
        });
        break;
    }
    return keyboard;
  }
}
代码示例
安全键盘.jpg

将以下代码添加到要使用安全键盘的页面:

class PasswordVerify extends StatelessWidget {
  @override
  Widget buildScaffold(BuildContext context) {
    //构建包含安全键盘视图,用于键盘弹出的时候页面可以滚动到输入框的位置
    return KeyboardMediaQuery(
      child: Builder(builder: (ctx) {
        //初始化键盘监听并且传递当前页面的context
        KeyboardManager.init(ctx);
        return super.buildScaffold(context);
      }),
    );
  }

  • Step4

在TextField keyboardType中设置自定义安全性键盘类型。
只需传递Step1中编写的inputType,就像通常设置键盘输入类型一样。

TextField(
   ...
   keyboardType: SecurityKeyboard.text,
   ...
 )

最后

  如果在使用过程遇到问题,欢迎下方留言交流。

  Pub类库地址

学习资料

请大家不吝点赞!因为您的点赞是对我最大的鼓励,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,473评论 1 11
  • 设想你在看一部期待已久的电影。再设想电影院里不关灯,允许打电话,随意进出,银幕角落时不时还会弹广告。你不会喜欢这样...
    Schuke阅读 958评论 6 24
  • 因为胜负已分! AlphaGo和李世石这次对决的难度,较之19年前“深蓝”击败卡斯帕罗夫,只能用两个字描述:秒杀。...
    liufan阅读 722评论 0 51
  • 我想要写作的初衷仅仅在于我想记录我曾经和正在经历的人和事。有一天你突然意识到生命如此的宝贵,流失的光阴将不再重返。...
    雷姐如是说阅读 978评论 2 3