網站首頁 編程語言 正文
前言
開篇先吐槽一下,輸入框和文本,一直都是官方每個版本改動的重點,先不說功能上全不全的問題,每次版本升級,必有 breaking change
。對于 extended_text_field | Flutter Package (flutter-io.cn) 和 extended_text | Flutter Package (flutter-io.cn) 來說,新功能都是基于官方的代碼,每次版本升級,merge
代碼就一個字,頭痛,已經有了躺平的想法了。(暫時不 merge
了,能運行就行,等一個穩定點的官方版本,準備做個重構,重構一個相對更好 merge
代碼的結構。)
系統鍵盤彈出的原因
吐槽完畢,我們來看一個常見的場景,就是自定義鍵盤。要想顯示自己自定義的鍵盤,那么必然需要隱藏系統的鍵盤。方法主要有如下:
- 在合適的時機調用,
SystemChannels.textInput.invokeMethod<void>('TextInput.hide')
。 - 系統鍵盤為啥會彈出來,是因為某些代碼調用了
SystemChannels.textInput.invokeMethod<void>('TextInput.show')
,那么我們可以魔改官方代碼, 把TextField
和EditableText
的代碼復制出來。
EditableTextState
代碼中有一個 TextInputConnection? _textInputConnection;
,它會在有需要的時候調用 show
方法。
TextInputConnection
中 show
,如下。
/// Requests that the text input control become visible. void show() { assert(attached); TextInput._instance._show(); }
TextInput
中 _show
,如下。
void _show() { _channel.invokeMethod<void>('TextInput.show'); }
那么問題就簡單了,把 TextInputConnection
調用 show
方法的地方全部注釋掉。這樣子確實系統鍵盤就不會再彈出來了。
在實際開發過程中,兩種方法都有自身的問題:
第一種方法會導致系統鍵盤上下,會造成布局閃爍,而且調用這個方法的時機也很容易造成額外的 bug
。
第二種方法,就跟我吐槽的一樣,復制官方代碼真的是吃力不討好的一件事情,版本遷移的時候,沒人愿意再去復制一堆代碼。如果你使用的是三方的組件,你可能還需要去維護三方組件的代碼。
攔截系統鍵盤彈出信息
實際上,系統鍵盤是否彈出,完全是因為 SystemChannels.textInput.invokeMethod<void>('TextInput.show')
的調用,但是我們不可能去每個調用該方法地方去做處理,那么這個方法執行后續,我們有辦法攔截嗎? 答案當然是有的。
Flutter
的 Framework
層發送信息 TextInput.show
到 Flutter
引擎是通過 MethodChannel
, 而我們可以通過重載 WidgetsFlutterBinding
的 createBinaryMessenger
方法來處理Flutter
的 Framework
層通過 MethodChannel
發送的信息。
mixin TextInputBindingMixin on WidgetsFlutterBinding { @override BinaryMessenger createBinaryMessenger() { return TextInputBinaryMessenger(super.createBinaryMessenger()); } }
在 main 方法中初始化這個 binding
。
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin { } void main() { YourBinding(); runApp(const MyApp()); }
BinaryMessenger
有 3
個方法需要重載.
class TextInputBinaryMessenger extends BinaryMessenger { TextInputBinaryMessenger(this.origin); final BinaryMessenger origin; @override Future<ByteData?>? send(String channel, ByteData? message) { // TODO: implement send throw UnimplementedError(); } @override void setMessageHandler(String channel, MessageHandler? handler) { // TODO: implement setMessageHandler } @override Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) { // TODO: implement handlePlatformMessage throw UnimplementedError(); } }
- send
Flutter
的 Framework
層發送信息到 Flutter
引擎,會走這個方法,這也是我們需要的處理的方法。
- setMessageHandler
Flutter
引擎 發送信息到 Flutter
的 Framework
層的回調。在我們的場景中不用處理。
- handlePlatformMessage
把 send
和 setMessageHandler
二和一,看了下 注釋,似乎是服務于 test
的
static const MethodChannel platform = OptionalMethodChannel( 'flutter/platform', JSONMethodCodec(), );
對于不需要處理的方法,我們做以下處理。
class TextInputBinaryMessenger extends BinaryMessenger { TextInputBinaryMessenger(this.origin); final BinaryMessenger origin; @override Future<ByteData?>? send(String channel, ByteData? message) { // TODO: 處理我們自己的邏輯 return origin.send(channel, message); } @override void setMessageHandler(String channel, MessageHandler? handler) { origin.setMessageHandler(channel, handler); } @override Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) { return origin.handlePlatformMessage(channel, data, callback); } }
接下來我們可以根據我們的需求處理 send
方法了。當 channel
為 SystemChannels.textInput
的時候,根據方法名字來攔截 TextInput.show
。
static const MethodChannel textInput = OptionalMethodChannel( 'flutter/textinput', JSONMethodCodec(), );
@override Future<ByteData?>? send(String channel, ByteData? message) async { if (channel == SystemChannels.textInput.name) { final MethodCall methodCall = SystemChannels.textInput.codec.decodeMethodCall(message); switch (methodCall.method) { case 'TextInput.show': // 處理是否需要濾過這次消息。 return SystemChannels.textInput.codec.encodeSuccessEnvelope(null); default: } } return origin.send(channel, message); }
現在交給我們最后問題就是怎么確定這次消息需要被攔截?當需要發送 TextInput.show
消息的時候,必定有某個 FocusNode
處于 Focus
的狀態。那么可以根據這個 FocusNode
做區分。
我們定義個一個特別的 FocusNode
,并且定義好一個屬性用于判斷(也有那種需要隨時改變是否需要攔截信息的需求)。
class TextInputFocusNode extends FocusNode { /// no system keyboard show /// if it's true, it stop Flutter Framework send `TextInput.show` message to Flutter Engine bool ignoreSystemKeyboardShow = true; }
這樣子,我們就可以根據以下代碼進行判斷。
Future<ByteData?>? send(String channel, ByteData? message) async { if (channel == SystemChannels.textInput.name) { final MethodCall methodCall = SystemChannels.textInput.codec.decodeMethodCall(message); switch (methodCall.method) { case 'TextInput.show': final FocusNode? focus = FocusManager.instance.primaryFocus; if (focus != null && focus is TextInputFocusNode && focus.ignoreSystemKeyboardShow) { return SystemChannels.textInput.codec.encodeSuccessEnvelope(null); } break; default: } } return origin.send(channel, message); }
最后我們只需要為 TextField
傳入這個特殊的 FocusNode
。
final TextInputFocusNode _focusNode = TextInputFocusNode()..debugLabel = 'YourTextField'; @override Widget build(BuildContext context) { return TextField( focusNode: _focusNode, ); }
畫自己的鍵盤
這里主要講一下,彈出和隱藏鍵盤的時機。你可以通過當前焦點的變化的時候,來顯示或者隱藏自定義的鍵盤。
當你的自定義鍵盤能自己關閉,并且保存焦點不丟失的,你那還應該在 [TextField] 的 onTap
事件中,再次判斷鍵盤是否顯示。比如我寫的例子中使用的是 showBottomSheet
方法,它是能通過 drag
來關閉自己的。
下面為一個簡單的例子,完整的例子在 extended_text_field/no_keyboard.dart at master · fluttercandies/extended_text_field (github.com)
PersistentBottomSheetController<void>? _bottomSheetController; final TextInputFocusNode _focusNode = TextInputFocusNode()..debugLabel = 'YourTextField'; @override void initState() { super.initState(); _focusNode.addListener(_handleFocusChanged); } @override Widget build(BuildContext context) { return Scaffold( body: TextField( // you must use TextInputFocusNode focusNode: _focusNode, ), ); } void _onTextFiledTap() { if (_bottomSheetController == null) { _handleFocusChanged(); } } void _handleFocusChanged() { if (_focusNode.hasFocus) { // just demo, you can define your custom keyboard as you want _bottomSheetController = showBottomSheet<void>( context: FocusManager.instance.primaryFocus!.context!, // set false, if don't want to drag to close custom keyboard enableDrag: true, builder: (BuildContext b) { // your custom keyboard return Container(); }); // maybe drag close _bottomSheetController?.closed.whenComplete(() { _bottomSheetController = null; }); } else { _bottomSheetController?.close(); _bottomSheetController = null; } } @override void dispose() { _focusNode.removeListener(_handleFocusChanged); super.dispose(); }
當然,怎么實現自定義鍵盤,可以根據自己的情況來決定,比如如果你的鍵盤需要頂起布局的話,你完全可以寫成下面的布局。
Column( children: <Widget>[ // 你的頁面 Expanded(child: Container()), // 你的自定義鍵盤 Container(), ], );
結語
通過對 createBinaryMessenger
的重載,我們實現對系統鍵盤彈出的攔截,避免我們對官方代碼的依賴。其實 SystemChannels
當中,還有些其他的系統的 channel
,我們也能通過相同的方式去對它們進行攔截,比如可以攔截按鍵。
static const BasicMessageChannel<Object?> keyEvent = BasicMessageChannel<Object?>( 'flutter/keyevent', JSONMessageCodec(), );
本文相關代碼都在 extended_text_field | Flutter Package (flutter-io.cn) 。
最最后放上 Flutter Candies 全家桶,真香。
原文鏈接:https://juejin.cn/post/7166046328609308685
相關推薦
- 2022-06-18 Elasticsearches之python使用及Django與Flask集成示例_python
- 2022-06-16 golang?gorm的預加載及軟刪硬刪的數據操作示例_Golang
- 2023-07-02 解密Python中的作用域與名字空間_python
- 2022-07-16 CMake下調用anaconda的pytorch及numpy傳參CV::Mat給python(多線程
- 2022-06-14 ASP.NET?Core?MVC中的標簽助手(TagHelper)用法_實用技巧
- 2022-06-08 記錄一次奇怪的springboot cache redis緩存報錯解決
- 2022-11-15 python中sample函數的介紹與使用_python
- 2022-06-09 ASP.NET?Core使用EF創建關系模型_實用技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支