相關API解讀
- 背景
- 1.基礎 API
- 2.Window
- 3.BindingBase
- 4.ServicesBinding
- 5.Flutter 中如何彈出系統鍵盤?
- 6.InsightBank 如何彈出自定義鍵盤
- 7.如何解決自定義鍵盤與原生鍵盤互相切換問題圈膏?
背景
這是關于 bug 的故事梭灿。
為了解決自定義鍵盤與原生鍵盤互相切換bottom計算不準確的問題曹铃,研究了系統鍵盤與自定義鍵盤是如何工作的秸仙。
- didChangeMetrics() 如何計算鍵盤的高度
- debug 模式切換鍵盤沒有問題飞蚓,到 relase 版本下卻不行了踱稍?
一弟跑、基礎 API
-
TextInputType:
定義各種鍵盤的類型灾前,與 Native中接口相同(number、phone窖认、email...) -
CustomTextInputType:
目前只有Android平臺使用豫柬。繼承 TextInputType 自定義鍵盤類型告希。 -
FocusNode
: StatefullWidget 可以使用一個對象來獲取鍵盤焦點和處理鍵盤事件。 -
TextEditingController:
增刪改查烧给。輸入的信息(需要在 dispos 中回收資源)燕偶。 -
InputController:
validate用于檢測文本信息
二、 window
它公開了顯示的大小础嫡,核心調度程序API指么,輸入事件回調(語言改變、 textScaleFactor 改變回調 )榴鼎,圖形繪制API以及其他此類核心服務伯诬。
MediaQuery 的 MediaQueryData 來源于 WidgetsBinding.instance.window
https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4
-
Window.viewInsets:
系統為系統UI(例如鍵盤)保留的物理像素,它將完全遮蓋該區(qū)域中繪制的所有內容巫财。onMetricsChanged 可以監(jiān)聽到發(fā)生改變 -
Window.viewPadding:
是顯示器每一側的物理像素盗似,可能會由于系統UI或顯示器的物理侵入而被部分遮擋 -
Window.padding:
它將允許viewInsets插圖消耗視圖填充
Window.viewInsets 更新流程
-
Engine
通過_updateWindowMetrics()
更新window
-
window
通過handleMetricsChanged()
調用RendererBingding
和widgetsBinding
去更新UI,回調didChangeMetrics()
三平项、BindingBase
- 各種Binding服務 minxin 的基類
- 提供window
- 提供鎖事件處理赫舒,用于熱加載。熱加載時會把所有輸入事件鎖起來闽瓢,等熱加載完成再執(zhí)行接癌。
/// 提供單例服務的mixin們的基類
///
/// 使用on繼承此類并實現initInstances方法 這個mixin在app的生命周期內只能被構建一次,在checked
/// 模式下會對此斷言
///
/// 用于編寫應用程序的最頂層將具有一個繼承自[BindingBase]并使用所有各種[BindingBase]
/// mixins(例如[ServicesBinding])的具體類扣讼。
/// 比如Flutter中的Widgets庫引入了一個名為[WidgetsFlutterBinding]的binding缺猛,定義了如何綁定
/// 可以隱性(例如,[WidgetsFlutterBinding]從[runApp]啟動)椭符,或者需要應用程序顯式調用構造函數
abstract class BindingBase {
BindingBase() {
initInstances();//在構造函數里進行初始化
initServiceExtensions();//初始化擴展服務
}
ui.Window get window => ui.window;//提供window
@protected
@mustCallSuper
void initInstances() {//初始化荔燎,其他binding mixin可以重寫此類
}
@protected
@mustCallSuper
void initServiceExtensions() {}//用于子類重寫該方法,用于注冊一些擴展服務艰山。
@protected
bool get locked => _lockCount > 0;
int _lockCount = 0;
/// /鎖定異步事件和回調的分派湖雹,直到回調的未來完成為止咏闪。
/// 這會導致輸入滯后曙搬,因此應盡可能避免。 它主要用于非用戶交互時間
@protected
Future<void> lockEvents(Future<void> callback()) {
developer.Timeline.startSync('Lock events');
_lockCount += 1;
final Future<void> future = callback();
future.whenComplete(() {
_lockCount -= 1;
if (!locked) {
unlocked();
}
});
return future;
}
@protected
@mustCallSuper
void unlocked() {//解鎖
assert(!locked);
}
/// 開發(fā)過程中使用
///
/// 使整個應用程序重新繪制鸽嫂,例如熱裝后纵装。
///通過發(fā)送`ext.flutter.reassemble`服務擴展信號來手動進行
/// 在此方法運行時,事件被鎖定(例如据某,指針事件未鎖定)
Future<void> reassembleApplication() {
return lockEvents(performReassemble);
}
/// 不會繼續(xù)指向舊代碼橡娄,并刷新以前計算的值的所有緩存,以防新代碼對它們進行不同的計算癣籽。
/// 例如挽唉,渲染層在調用時觸發(fā)整個應用程序重新繪制滤祖。
@mustCallSuper
@protected
Future<void> performReassemble() {//重繪方法,需要重寫
return Future<void>.value();
}
···省略一些服務擴展方法
}
其他 Bingding
-
GestureBinding:
提供了window.onPointerDataPacket 回調瓶籽,綁定Framework手勢子系統匠童,是Framework事件模型與底層事件的綁定入口。 -
ServicesBinding:
提供了window.onPlatformMessage 回調塑顺, 用于綁定平臺消息通道(message channel)汤求,主要處理原生和Flutter通信。 -
SchedulerBinding:
提供了window.onBeginFrame和window.onDrawFrame回調严拒,監(jiān)聽刷新事件扬绪,綁定Framework繪制調度子系統。 -
PaintingBinding:
綁定繪制庫裤唠,主要用于處理圖片緩存挤牛。 -
SemanticsBinding:
語義化層與Flutter engine的橋梁,主要是輔助功能的底層支持种蘸。 -
RendererBinding:
提供了window.onMetricsChanged 赊颠、window.onTextScaleFactorChanged 等回調。它是渲染樹與Flutter engine的橋梁劈彪。 -
WidgetsBinding:
提供了window.onLocaleChanged竣蹦、onBuildScheduled 等回調。它是Flutter widget層與engine的橋梁沧奴。
四痘括、ServicesBinding mixin
偵聽平臺消息,并將其定向到defaultBinaryMessenger
滔吠。另注冊了 LicenseEntryCollector
做證書相關處理纲菌。
DefaultBinaryMessenger 繼承自 BinaryMessenger,BinaryMessenger 是一個信使疮绷,它通過Flutter平臺發(fā)送二進制數據翰舌。
DefaultBinaryMessenger 繼承自 BinaryMessenger
/// 一個信使,它通過Flutter平臺發(fā)送二進制數據冬骚。
abstract class BinaryMessenger {
/// A const constructor to allow subclasses to be const.
const BinaryMessenger();
/// 設置一個回調椅贱,以在給定通道上接收來自平臺插件的消息,而無需對其進行解碼只冻。
/// 給定的回調將替換當前注冊的回調頻道(如果有)庇麦。 要刪除處理程序,請將null作為[handler]參數傳遞喜德。
void setMessageHandler(String channel, Future<ByteData> handler(ByteData message));
/// 與上述類似山橄,但不會回調native端
void setMockMessageHandler(String channel, Future<ByteData> handler(ByteData message));
/// 將二進制消息發(fā)送到給定通道上的平臺插件。返回一個二進制的Future
Future<ByteData> send(String channel, ByteData message);
/// 處理 native 端發(fā)來的消息
Future<void> handlePlatformMessage(String channel, ByteData data, ui.PlatformMessageResponseCallback callback);
}
DefaultBinaryMessenger
另外
PlatformBinaryMessenger
與 DefaultBinaryMessenger 實現類似舍悯,不同點在于 PlatformBinaryMessenger 不支持 setMockMessageHandler() 航棱。
-
setMessageHandler
:以 channel 為 key睡雇,MessageHandler 為 value ,放入 ChannelBuffer 中饮醇。 -
setMockMessageHandler:
以 channel 為 key入桂,MessageHandler 為 value 。實現自定義 channel 方法驳阎。 -
ChannelBuffer
: 允許在 Engine 和 Framework 之間存儲消息抗愁。在 Framework 處理之前,消息會一直存儲呵晚。
/// The default implementation of [BinaryMessenger].
/// 發(fā)送消息到 native蜘腌,并處理 native 返回的消息
class _DefaultBinaryMessenger extends BinaryMessenger {
const _DefaultBinaryMessenger._();
static final Map<String, MessageHandler> _handlers =
<String, MessageHandler>{};
static final Map<String, MessageHandler> _mockHandlers =
<String, MessageHandler>{};
Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
final Completer<ByteData> completer = Completer<ByteData>();
/// ui.window.sendPlatformMessage 調用 C++ 的方法。
ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
try {
completer.complete(reply);
} catch (exception, stack) {
...
}
});
return completer.future;
}
@override
Future<void> handlePlatformMessage(
String channel,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) async {
ByteData response;
try {
final MessageHandler handler = _handlers[channel];
if (handler != null) {
response = await handler(data);
} else {
/// 存儲通道消息饵隙,直到通道被完全路由為止撮珠,即,當消息處理程序附加到框架側的通道時金矛。
ui.channelBuffers.push(channel, data, callback);
callback = null;
}
} catch (exception, stack) {
...
} finally {
if (callback != null) {
callback(response);
}
}
}
@override
Future<ByteData> send(String channel, ByteData message) {
final MessageHandler handler = _mockHandlers[channel];
if (handler != null)
return handler(message);
return _sendPlatformMessage(channel, message);
}
@override
void setMessageHandler(String channel, MessageHandler handler) {
if (handler == null)
_handlers.remove(channel);
else
_handlers[channel] = handler;
/// 允許在引擎和框架之間存儲消息芯急。
ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
await handlePlatformMessage(channel, data, callback);
});
}
@override
void setMockMessageHandler(String channel, MessageHandler handler) {
if (handler == null)
_mockHandlers.remove(channel);
else
_mockHandlers[channel] = handler;
}
}
五、Flutter 中如何彈出系統鍵盤驶俊?
- 1.從
FocusManager
開始娶耍,requestFocus() 獲得 inputWidget 輸入的焦點
FocusScope.of(context).requestFocus(focusNode)
-
FocusManager
調用內部的_listeners
列表, 執(zhí)行EditableText._handleFocusChanged()
饼酿,EditableText 會在 initState() 和 didUpdateWidget() 中去注冊監(jiān)聽榕酒。
-
/// 解決自定義鍵盤與原生鍵盤提供方案。
@override
void didChangeMetrics() {
// window.viewInsets.bottom 用來獲得鍵盤的高度
if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
_showCaretOnScreen();// 計算屏幕中位置
}
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
}
/// EditableText 1706
void _handleFocusChanged() {
/// 打開和關閉輸入連接
_openOrCloseInputConnectionIfNeeded();
// 播放 Cursor 動畫
_startOrStopCursorTimerIfNeeded();
// SelectionOverlay 復制粘貼相關
_updateOrDisposeSelectionOverlayIfNeeded();
if (_hasFocus) {
// Listen for changing viewInsets, which indicates keyboard showing up.
// 監(jiān)聽viewInsets的改變故俐,當鍵盤出現的時候
WidgetsBinding.instance.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
_showCaretOnScreen();
if (!_value.selection.isValid) {
// 如果我們收到焦點時選擇無效想鹰,請將光標放在末尾。
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
// 如果失去焦點药版,則清除選擇和合成狀態(tài)辑舷。
_value = TextEditingValue(text: _value.text);
}
}
-
_openOrCloseInputConnectionIfNeeded()
中會建立與 Native 鍵盤的連接。同時通過TextInput.attach
關聯鍵盤事件槽片。
-
/// EditableText 1391的構造方法
void _openInputConnection() {
if (!_hasInputConnection) { // 注冊與原生的通信
final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue;
_textInputConnection = TextInput.attach(//
this,
TextInputConfiguration(
inputType: widget.keyboardType,
...
),
);
_textInputConnection.show(); // 展示原生鍵盤
_updateSizeAndTransform();
final TextStyle style = widget.style;
_textInputConnection
..setStyle(
fontFamily: style.fontFamily,
...
)
..setEditingState(localValue);
} else {
_textInputConnection.show();
}
}
(1)_hasInputConnection
方法會初始化 TextInput()
對象何缓,調用 binaryMessenger.setMethodCallHandler(),創(chuàng)建與原生的通信筐乳。_handleTextInputInvocation() 注冊了交互的事件歌殃,比如:更新光標的位置乔妈、給默認值蝙云、關閉連接.
/// TextInput 828 的構造方法
TextInput._() {
_channel = OptionalMethodChannel(
'flutter/textinput',
JSONMethodCodec(),
);
_channel.setMethodCallHandler(_handleTextInputInvocation);
}
/// 監(jiān)聽鍵盤輸入中事件
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
// The incoming message was for a different client.
if (client != _currentConnection._id)
return;
switch (method) {
case 'TextInputClient.updateEditingState':
// 輸入事件,更新輸入焦點路召、暫停勃刨,開始光標動畫
_currentConnection._client.updateEditingValue(TextEditingValue.fromJSON(args[1]));
break;
case 'TextInputClient.performAction':
// TextInputAction.send波材,go 事件等。通過 onSubmitted() 回調
_currentConnection._client.performAction(_toTextInputAction(args[1]));
break;
case 'TextInputClient.updateFloatingCursor':
// 更新浮動光標的位置和狀態(tài)身隐。
_currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
break;
case 'TextInputClient.onConnectionClosed':
_currentConnection._client.connectionClosed();
break;
default:
throw MissingPluginException();
}
}
(2) TextInput.attach()
調用'TextInput.setClient'
方法與 native 建立連接廷区,configuration
信息傳入傳給native,例如鍵盤的類型贾铝。
void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
final ByteData result = await binaryMessenger.send(
'TextInput.setClient',
codec.encodeMethodCall(MethodCall(method, arguments)),
);
_currentConnection = connection;
_currentConfiguration = configuration;
}
(3) _textInputConnection.show()
調用 'TextInput.show
方法顯示鍵盤隙轻。
(4) _updateSizeAndTransform()
方法,調用 TextInput.setEditableSizeAndTransform
將文本大小參數傳遞過去垢揩,設置輸入文字的大小
-
_startOrStopCursorTimerIfNeeded()
使用 Timer 控制光標玖绿。
-
- 5
_updateOrDisposeSelectionOverlayIfNeeded()
選中的文本 '復制/粘貼' 相關配置。
void _handleFocusChanged() {
_openOrCloseInputConnectionIfNeeded(); /// 建立關閉連接
_startOrStopCursorTimerIfNeeded(); /// 光標
_updateOrDisposeSelectionOverlayIfNeeded(); /// 選擇的文本
if (_hasFocus) {
// Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; /// 獲取系統鍵盤的高度更新視圖
_showCaretOnScreen();
if (!_value.selection.isValid) { /// TextSelectionOverlay 復制/粘貼 布局
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
// Clear the selection and composition state if this widget lost focus.
_value = TextEditingValue(text: _value.text);
}
updateKeepAlive();
}
六叁巨、InsightBank 中如何調用自定義鍵盤
鍵盤相關的事件
-
TextInput.show:
展示自定義鍵盤 -
TextInput.hide:
隱藏自定義鍵盤 -
TextInput.setClient
: 初始化鍵盤斑匪,設置鍵盤類型等,并監(jiān)聽輸入事件 -
TextInput.clearClient
:關閉鍵盤锋勺,內存回收 -
TextInput.setEditingState
:設置編輯狀態(tài)蚀瘸。光標的位置,當前文本 -
TextInput.setEditableSizeAndTransform
: 設置寬高相關信息 -
TextInput.setStyle
: 設置文本字體
static void _interceptInput() {
// custom keyboard will cover the driver's MockMessageHandler
// so we disable custom keyboard when run driver test
if (_isIntercepted || moduleConfig.isDriverTestMode) return;
_isIntercepted = true;
ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
_channelTextInput,
(ByteData data) async {
final methodCall = _codec.decodeMethodCall(data);
switch (methodCall.method) {
case _methodShow:
return _handleShow(methodCall, data);
case _methodHide:
return _handleHide(methodCall, data);
case _methodSetClient:
return _handleSetClient(methodCall, data);
case _methodClearClient:
return _handleClearClient(methodCall, data);
case _methodSetEditingState:
return _handleSetEditingState(methodCall, data);
default:
return _dispatchOriginResponse(data);
}
},
);
}
七庶橱、如何解決自定義鍵盤與原生鍵盤互相切換問題
(1)之前的解決方案
final insets = window.viewInsets.bottom / window.devicePixelRatio;
就是鍵盤的高度
(2)_showCaretOnScreen()
_showCaretOnScreen() 起到的作用如下
- 計算文字贮勃,橫向滾動
- 計算 bottomSpacing ,將輸入框顯示在屏幕上合適的位置苏章。
問題:
為什么使用的是:
addPostFrameCallback
是否可以換成setState()
衙猪。
換成 setState 后輸入文字出現閃爍的情況debug
模式下,鍵盤互相切換沒有問題布近,release
版本卻不行(偶現問題)垫释。
我猜想,一下前提條件:
- _showCaretOnScreenScheduled 控制 _showCaretOnScree() 調用
- setState() 不會調用 _showCaretOnScreen()撑瞧,意味著收起鍵盤不會調用
- window 更新 vidwinsets 是異步的棵譬。
由于 release 版本更快,window 更新時预伺,_showCaretOnScreenScheduled 控制著 _showCaretOnScree() 無法更新订咸,所以出現了輸入框被遮擋的情況。
同理 debug 版本相對慢酬诀,意味著 _showCaretOnScree() 執(zhí)行完成后脏嚷,因為 window 更新又執(zhí)行了一遍
@override
void didChangeMetrics() {
// 展示鍵盤的事件
if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
_showCaretOnScreen();
}
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
}
// 調用時機,didChangeMetrics(), initState(), 輸入字符時候瞒御。
void _showCaretOnScreen() {
if (_showCaretOnScreenScheduled) {
return;
}
_showCaretOnScreenScheduled = true;
// 這里沒有使用 setState 而是使用了 addPostFrameCallback
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_showCaretOnScreenScheduled = false;
if (_currentCaretRect == null || !_scrollController.hasClients) {
return;
}
final double scrollOffsetForCaret = _getScrollOffsetForCaret(_currentCaretRect);
/// 1. 文字橫向滾動父叙,如果輸入文字超過一屏幕
_scrollController.animateTo(
scrollOffsetForCaret,
duration: _caretAnimationDuration,
curve: _caretAnimationCurve,
);
final Rect newCaretRect = _getCaretRectAtScrollOffset(_currentCaretRect, scrollOffsetForCaret);
// Enlarge newCaretRect by scrollPadding to ensure that caret is not
// positioned directly at the edge after scrolling.
double bottomSpacing = widget.scrollPadding.bottom;
if (_selectionOverlay?.selectionControls != null) {
final double handleHeight = _selectionOverlay.selectionControls
.getHandleSize(renderEditable.preferredLineHeight).height;
final double interactiveHandleHeight = math.max(
handleHeight,
kMinInteractiveDimension,
);
final Offset anchor = _selectionOverlay.selectionControls
.getHandleAnchor(
TextSelectionHandleType.collapsed,
renderEditable.preferredLineHeight,
);
final double handleCenter = handleHeight / 2 - anchor.dy;
bottomSpacing = math.max(
handleCenter + interactiveHandleHeight / 2,
bottomSpacing,
);
}
final Rect inflatedRect = Rect.fromLTRB(
newCaretRect.left - widget.scrollPadding.left,
newCaretRect.top - widget.scrollPadding.top,
newCaretRect.right + widget.scrollPadding.right,
newCaretRect.bottom + bottomSpacing,
);
/// 2. 找到輸入框光標的位置,顯示在屏幕上
_editableKey.currentContext.findRenderObject().showOnScreen(
rect: inflatedRect,
duration: _caretAnimationDuration,
curve: _caretAnimationCurve,
);
});
}
總結:
- nativeBottomInsets 可以通過 window 直接獲取。
- addPostFrameCallback() 具有延遲計算的作用趾唱,第一幀使用老的 insets涌乳,這樣焦點還在原來的位置不變。
即使甜癞,出現了以上情況夕晓,
https://phabricator.d.xiaomi.net/D224058
static double get nativeBottomInsets => window.viewInsets.bottom / window.devicePixelRatio;
/// 在 ScrollView 中,原生鍵盤切換自定義鍵盤悠咱,會發(fā)生 ScrollView 滾動位置不準確問題蒸辆。
///
/// 原因:ScrollView 的計算依賴與第一幀 viewInsets 去計算。
/// updateBottomInsets() 會在 [window] 更新 viewInsets 之前觸發(fā)析既。所以使用 [onPostFrame] 延遲處理吁朦。
/// 另外,setState() 可以更新 viewInsets 重繪渡贾,但在 release 版本下會變得不可控逗宜。
void updateBottomInsets(double insets) {
// 收起自定義鍵盤時,不能立即更新 insets 空骚,會造成 ScrollView 滾動位置不準確纺讲。
if (insets < customBottomInsets) {
return UiUtils.onPostFrame(() => setState(() => customBottomInsets = insets));
}
// 展示自定義鍵盤需要立刻更新 insets, 會造成 ScrollView 滾動位置不準確
setState(() => customBottomInsets = insets);
}`