Flutter?阻止系統(tǒng)鍵盤彈出的優(yōu)雅方式
前言
開(kāi)篇先吐槽一下,輸入框和文本,一直都是官方每個(gè)版本改動(dòng)的重點(diǎn),先不說(shuō)功能上全不全的問(wèn)題,每次版本升級(jí),必有 breaking change 。對(duì)于 extended_text_field | Flutter Package (flutter-io.cn) 和 extended_text | Flutter Package (flutter-io.cn) 來(lái)說(shuō),新功能都是基于官方的代碼,每次版本升級(jí),merge 代碼就一個(gè)字,頭痛,已經(jīng)有了躺平的想法了。(暫時(shí)不 merge 了,能運(yùn)行就行,等一個(gè)穩(wěn)定點(diǎn)的官方版本,準(zhǔn)備做個(gè)重構(gòu),重構(gòu)一個(gè)相對(duì)更好 merge 代碼的結(jié)構(gòu)。)
系統(tǒng)鍵盤彈出的原因
吐槽完畢,我們來(lái)看一個(gè)常見(jiàn)的場(chǎng)景,就是自定義鍵盤。要想顯示自己自定義的鍵盤,那么必然需要隱藏系統(tǒng)的鍵盤。方法主要有如下:
- 在合適的時(shí)機(jī)調(diào)用,
SystemChannels.textInput.invokeMethod<void>('TextInput.hide')。 - 系統(tǒng)鍵盤為啥會(huì)彈出來(lái),是因?yàn)槟承┐a調(diào)用了
SystemChannels.textInput.invokeMethod<void>('TextInput.show'),那么我們可以魔改官方代碼, 把TextField和EditableText的代碼復(fù)制出來(lái)。
EditableTextState 代碼中有一個(gè) TextInputConnection? _textInputConnection;,它會(huì)在有需要的時(shí)候調(diào)用 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');
}
那么問(wèn)題就簡(jiǎn)單了,把 TextInputConnection 調(diào)用 show 方法的地方全部注釋掉。這樣子確實(shí)系統(tǒng)鍵盤就不會(huì)再?gòu)棾鰜?lái)了。
在實(shí)際開(kāi)發(fā)過(guò)程中,兩種方法都有自身的問(wèn)題:
第一種方法會(huì)導(dǎo)致系統(tǒng)鍵盤上下,會(huì)造成布局閃爍,而且調(diào)用這個(gè)方法的時(shí)機(jī)也很容易造成額外的 bug 。
第二種方法,就跟我吐槽的一樣,復(fù)制官方代碼真的是吃力不討好的一件事情,版本遷移的時(shí)候,沒(méi)人愿意再去復(fù)制一堆代碼。如果你使用的是三方的組件,你可能還需要去維護(hù)三方組件的代碼。
攔截系統(tǒng)鍵盤彈出信息
實(shí)際上,系統(tǒng)鍵盤是否彈出,完全是因?yàn)?SystemChannels.textInput.invokeMethod<void>('TextInput.show') 的調(diào)用,但是我們不可能去每個(gè)調(diào)用該方法地方去做處理,那么這個(gè)方法執(zhí)行后續(xù),我們有辦法攔截嗎? 答案當(dāng)然是有的。
Flutter 的 Framework 層發(fā)送信息 TextInput.show 到 Flutter 引擎是通過(guò) MethodChannel, 而我們可以通過(guò)重載 WidgetsFlutterBinding 的 createBinaryMessenger 方法來(lái)處理Flutter 的 Framework 層通過(guò) MethodChannel 發(fā)送的信息。
mixin TextInputBindingMixin on WidgetsFlutterBinding {
@override
BinaryMessenger createBinaryMessenger() {
return TextInputBinaryMessenger(super.createBinaryMessenger());
}
}
在 main 方法中初始化這個(gè) binding 。
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
}
void main() {
YourBinding();
runApp(const MyApp());
}
BinaryMessenger 有 3 個(gè)方法需要重載.
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 層發(fā)送信息到 Flutter 引擎,會(huì)走這個(gè)方法,這也是我們需要的處理的方法。
- setMessageHandler
Flutter 引擎 發(fā)送信息到 Flutter 的 Framework 層的回調(diào)。在我們的場(chǎng)景中不用處理。
- handlePlatformMessage
把 send 和 setMessageHandler 二和一,看了下 注釋,似乎是服務(wù)于 test 的
static const MethodChannel platform = OptionalMethodChannel(
'flutter/platform',
JSONMethodCodec(),
);
對(duì)于不需要處理的方法,我們做以下處理。
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);
}
}
接下來(lái)我們可以根據(jù)我們的需求處理 send 方法了。當(dāng) channel 為 SystemChannels.textInput 的時(shí)候,根據(jù)方法名字來(lái)攔截 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':
// 處理是否需要濾過(guò)這次消息。
return SystemChannels.textInput.codec.encodeSuccessEnvelope(null);
default:
}
}
return origin.send(channel, message);
}
現(xiàn)在交給我們最后問(wèn)題就是怎么確定這次消息需要被攔截?當(dāng)需要發(fā)送 TextInput.show 消息的時(shí)候,必定有某個(gè) FocusNode 處于 Focus 的狀態(tài)。那么可以根據(jù)這個(gè) FocusNode 做區(qū)分。
我們定義個(gè)一個(gè)特別的 FocusNode,并且定義好一個(gè)屬性用于判斷(也有那種需要隨時(shí)改變是否需要攔截信息的需求)。
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;
}
這樣子,我們就可以根據(jù)以下代碼進(jìn)行判斷。
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 傳入這個(gè)特殊的 FocusNode。
final TextInputFocusNode _focusNode = TextInputFocusNode()..debugLabel = 'YourTextField';
@override
Widget build(BuildContext context) {
return TextField(
focusNode: _focusNode,
);
}
畫自己的鍵盤
這里主要講一下,彈出和隱藏鍵盤的時(shí)機(jī)。你可以通過(guò)當(dāng)前焦點(diǎn)的變化的時(shí)候,來(lái)顯示或者隱藏自定義的鍵盤。
當(dāng)你的自定義鍵盤能自己關(guān)閉,并且保存焦點(diǎn)不丟失的,你那還應(yīng)該在 [TextField] 的 onTap 事件中,再次判斷鍵盤是否顯示。比如我寫的例子中使用的是 showBottomSheet 方法,它是能通過(guò) drag 來(lái)關(guān)閉自己的。
下面為一個(gè)簡(jiǎn)單的例子,完整的例子在 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();
}

當(dāng)然,怎么實(shí)現(xiàn)自定義鍵盤,可以根據(jù)自己的情況來(lái)決定,比如如果你的鍵盤需要頂起布局的話,你完全可以寫成下面的布局。
Column(
children: <Widget>[
// 你的頁(yè)面
Expanded(child: Container()),
// 你的自定義鍵盤
Container(),
],
);
結(jié)語(yǔ)
通過(guò)對(duì) createBinaryMessenger 的重載,我們實(shí)現(xiàn)對(duì)系統(tǒng)鍵盤彈出的攔截,避免我們對(duì)官方代碼的依賴。其實(shí) SystemChannels 當(dāng)中,還有些其他的系統(tǒng)的 channel,我們也能通過(guò)相同的方式去對(duì)它們進(jìn)行攔截,比如可以攔截按鍵。
static const BasicMessageChannel<Object?> keyEvent = BasicMessageChannel<Object?>(
'flutter/keyevent',
JSONMessageCodec(),
);
本文相關(guān)代碼都在 extended_text_field | Flutter Package (flutter-io.cn) 。
最最后放上 Flutter Candies 全家桶,真香。
以上就是Flutter 阻止系統(tǒng)鍵盤彈出的優(yōu)雅方式的詳細(xì)內(nèi)容,更多關(guān)于Flutter 阻止系統(tǒng)鍵盤彈出的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)歌詞滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)歌詞滾動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11
Android Build Variants 為項(xiàng)目設(shè)置變種版本的方法
下面小編就為大家分享一篇Android Build Variants 為項(xiàng)目設(shè)置變種版本的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
AndroidStudio不自動(dòng)添加新創(chuàng)建的文件到VCS的解決辦法
這篇文章主要介紹了AndroidStudio不自動(dòng)添加新創(chuàng)建的文件到VCS的解決辦法的相關(guān)資料,需要的朋友可以參考下2017-03-03
Android自定義控件實(shí)現(xiàn)按鈕滾動(dòng)選擇效果
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)按鈕滾動(dòng)選擇效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
如何正確實(shí)現(xiàn)Android啟動(dòng)屏畫面的方法(避免白屏)
本篇文章主要介紹了如何正確實(shí)現(xiàn)Android啟動(dòng)屏畫面的方法(避免白屏),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
Android網(wǎng)絡(luò)編程之UDP通信模型實(shí)例
這篇文章主要介紹了Android網(wǎng)絡(luò)編程之UDP通信模型實(shí)例,本文給出了服務(wù)端代碼和客戶端代碼,需要的朋友可以參考下2014-10-10
Android滑動(dòng)刪除數(shù)據(jù)功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android滑動(dòng)刪除功能2017-01-01
記錄Android studio JNI開(kāi)發(fā)的三種方式(推薦)
JNI (Java Native Interface)是一套編程接口,用來(lái)實(shí)現(xiàn)Java代碼和其他語(yǔ)言(c、C++或匯編)進(jìn)行交互。下面通過(guò)本文給大家講解Android studio JNI開(kāi)發(fā)的三種方式,需要的朋友參考下吧2017-12-12

