Flutter 安全鍵盤

在 Flutter 中目前官方?jīng)]有提供快速自定義鍵盤的解決方案侦高。

但在項(xiàng)目中以及 ICP 測評需要用到安全鍵盤停巷,比如金額輸入师坎、安全密碼輸入恕酸、各種自定義快速輸入鍵盤。

這里以Packages的形式實(shí)現(xiàn)了一個(gè)自定義安全鍵盤胯陋,已上傳 Pub蕊温。

實(shí)現(xiàn)功能

  1. 適用原生輸入框控件
  2. 支持一個(gè)頁面多個(gè)安全鍵盤輸入框
  3. 支持系統(tǒng)鍵盤與安全鍵盤混合使用
  4. 支持自動定位到輸入框位置

實(shí)現(xiàn)思路

主要是通過攔截PlatformChannel實(shí)現(xiàn)的,可以無縫對接TextFiled等Flutter自帶的輸入框,監(jiān)聽輸入框各種狀態(tài)遏乔,以及內(nèi)容變化义矛。

關(guān)鍵代碼講解

  • KeyboardManager

自定義鍵盤管理類,包括輸入類型與鍵盤的管理盟萨,以及輸入框攔截凉翻、懸浮自定義鍵盤及生命周期管理等。
主要功能方法 init捻激。

///初始化鍵盤監(jiān)聽并且傳遞當(dāng)前頁面的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 方法中對輸入框交互進(jìn)行攔截制轰。

當(dāng)輸入框獲焦時(shí)會調(diào)用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中根據(jù)輸入框定義的鍵盤類型找出對應(yīng)鍵盤對應(yīng)的鍵盤配置艇挨、InputClient残炮,以及初始輸入變更監(jiān)聽KeyboardController韭赘。
在TextInput.show根據(jù)配置的鍵盤類型,調(diào)用 openKeyboard 生成鍵盤控件势就。

///顯示鍵盤
static openKeyboard() {
  ///鍵盤已經(jīng)打開
  if (_keyboardEntry != null) return;

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

  ///根據(jù)鍵盤高度泉瞻,使鍵盤滾動到輸入框位置
  KeyboardMediaQueryState queryState = _context
          .ancestorStateOfType(const TypeMatcher<KeyboardMediaQueryState>())
      as KeyboardMediaQueryState;
  queryState.update();

  ...

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

這里通過 Overlay顯示一個(gè)懸浮窗脉漏,并鍵盤彈出后,滾動到輸入框位置袖牙。

當(dāng)輸入框輸入時(shí)侧巨,會調(diào)用TextInput.setEditingState。

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

這里設(shè)置 KeyboardController 的TextEditingValue鞭达,就可以監(jiān)聽輸入框的輸入變化司忱。

當(dāng)切換焦點(diǎn)、或自定義鍵盤輸入完成關(guān)閉時(shí)畴蹭,會調(diào)用TextInput.clearClient坦仍、TextInput.hide。

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

      ///切換輸入框時(shí)叨襟,會調(diào)用該回調(diào),切換時(shí)鍵盤會隱藏繁扎。
      case 'TextInput.clearClient':
        hideKeyboard(animation: false);
        clearKeyboard();
        break

在 clearClient 中清除 InputClient,且隱藏鍵盤糊闽。

  • KeyboardController

鍵盤輸入變更監(jiān)聽,可以參考系統(tǒng)[TextEditingController]梳玫,主要通過ValueNotifier監(jiān)聽TextEditingValue的值變化,ValueNotifier是一個(gè)包含單個(gè)值的變更通知器右犹,當(dāng)它的值改變的時(shí)候提澎,會通知它的監(jiān)聽。
擴(kuò)展了 addText念链,deleteText虱朵,doneAction

///刪除一個(gè)字符,一般用于鍵盤的刪除鍵
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));
  }
}
/// 在光標(biāo)位置添加文字,一般用于鍵盤輸入
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

用于鍵盤彈出的時(shí)候控制頁面邊間,使輸入框不被擋住钓账,自動定位到輸入框位置碴犬。

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

在項(xiàng)目pubspec.yaml添加安全鍵盤依賴

dependencies:
  security_keyboard: ^1.0.2

  • Step2

根據(jù)項(xiàng)目需求編寫個(gè)性化鍵盤

typedef KeyboardSwitch = Function(SecurityKeyboardType type);

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

class SecurityKeyboard extends StatefulWidget {
  ///用于控制鍵盤輸出的Controller
  final KeyboardController controller;

  ///鍵盤類型,默認(rèn)文本
  final SecurityKeyboardType keyboardType;

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

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

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

  ///數(shù)字輸入類型
  static SecurityTextInputType number =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.number);

  ///僅數(shù)字輸入類型
  static SecurityTextInputType numberOnly =
      SecurityKeyboard._inputKeyboard(SecurityKeyboardType.numberOnly);

  ///僅數(shù)字輸入類型,且沒有鍵盤提示
  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> {
  ///當(dāng)前鍵盤類型
  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) {
    //構(gòu)建包含安全鍵盤視圖,用于鍵盤彈出的時(shí)候頁面可以滾動到輸入框的位置
    return KeyboardMediaQuery(
      child: Builder(builder: (ctx) {
        //初始化鍵盤監(jiān)聽并且傳遞當(dāng)前頁面的context
        KeyboardManager.init(ctx);
        return super.buildScaffold(context);
      }),
    );
  }

  • Step4

在TextField keyboardType中設(shè)置自定義安全性鍵盤類型服协。
只需傳遞Step1中編寫的inputType,就像通常設(shè)置鍵盤輸入類型一樣啦粹。

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

最后

??如果在使用過程遇到問題偿荷,歡迎下方留言交流。

??Pub類庫地址

學(xué)習(xí)資料

請大家不吝點(diǎn)贊唠椭!因?yàn)槟狞c(diǎn)贊是對我最大的鼓勵(lì)跳纳,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贪嫂,一起剝皮案震驚了整個(gè)濱河市寺庄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖斗塘,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赢织,死亡現(xiàn)場離奇詭異,居然都是意外死亡馍盟,警方通過查閱死者的電腦和手機(jī)于置,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贞岭,“玉大人八毯,你說我怎么就攤上這事∶榻埃” “怎么了宪彩?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長讲婚。 經(jīng)常有香客問我尿孔,道長,這世上最難降的妖魔是什么筹麸? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任活合,我火速辦了婚禮,結(jié)果婚禮上物赶,老公的妹妹穿的比我還像新娘白指。我一直安慰自己,他們只是感情好酵紫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布告嘲。 她就那樣靜靜地躺著,像睡著了一般奖地。 火紅的嫁衣襯著肌膚如雪橄唬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天参歹,我揣著相機(jī)與錄音仰楚,去河邊找鬼。 笑死犬庇,一個(gè)胖子當(dāng)著我的面吹牛僧界,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播臭挽,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捂襟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了欢峰?” 一聲冷哼從身側(cè)響起葬荷,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤涨共,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后闯狱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抛计,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年哄孤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吹截。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瘦陈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出波俄,到底是詐尸還是另有隱情晨逝,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布懦铺,位于F島的核電站捉貌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏冬念。R本人自食惡果不足惜趁窃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望急前。 院中可真熱鬧醒陆,春花似錦、人聲如沸裆针。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽世吨。三九已至澡刹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間耘婚,已是汗流浹背像屋。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留边篮,地道東北人己莺。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像戈轿,于是被迫代替她去往敵國和親凌受。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • ??JavaScript 與 HTML 之間的交互是通過事件實(shí)現(xiàn)的挠进。 ??事件,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,490評論 1 11
  • 設(shè)想你在看一部期待已久的電影誊册。再設(shè)想電影院里不關(guān)燈领突,允許打電話,隨意進(jìn)出案怯,銀幕角落時(shí)不時(shí)還會彈廣告君旦。你不會喜歡這樣...
    Schuke閱讀 965評論 6 24
  • 因?yàn)閯儇?fù)已分! AlphaGo和李世石這次對決的難度嘲碱,較之19年前“深藍(lán)”擊敗卡斯帕羅夫金砍,只能用兩個(gè)字描述:秒殺。...
    liufan閱讀 731評論 0 51
  • 我想要寫作的初衷僅僅在于我想記錄我曾經(jīng)和正在經(jīng)歷的人和事麦锯。有一天你突然意識到生命如此的寶貴恕稠,流失的光陰將不再重返。...
    雷姐如是說閱讀 989評論 2 3