其他
Flutter 疑难杂症系列:键盘原理及常见问题解决方案
一、背景
二、Flutter键盘流程及原理
图 2-1 Flutter TextField 调起键盘
图 2-2 Flutter Android 端调用键盘流程图
UITextInput
协议的 FlutterTextInputView 实例通过调用becomeFirstResponder
实现键盘弹出。键盘弹出动画由系统触发,不受 Flutter 控制。 Flutter 页面上移,添加键盘开始触发 FlutterView 的 WindowInsets 特性的改变,引起页面的重绘。
2.1、键盘调起之后页面重绘逻辑
图 2-1-1 调起键盘后,WindowInsets 参数变更及传递路径图
键盘弹出占用 FlutterView 的空间,造成 FlutterView 的 WindowInsets 属性变化 WindowInsets 变化后,引起 Metrics 的变化,从 Platform 线程传递到 UI 线程 最后调用 scheduleForceFrame 强制触发绘制的流程
2.2、页面收缩动画
AnimatedContainer
,并根据 window.viewInsets.bottom / window.devicePixelRatio
的值的变化,设置不同的 Padding,实现比较平滑的动画效果。三、键盘相关问题
3.1 键盘动画卡顿
track-widget-creation
功能,我们在 profie 模式下抓取下 timeline:MediaQuery
的内容。MediaQuery
:MediaQuery
继承了 InheritedWidget
,而 InheritedWidget
是 Flutter 内用于 widget 内数据传入的类,核心方法是 updateShouldNotify
,用于判断是否相关的数据有变更行为。MeidaQuery
的 updateShouldNotify
函数如下:// oldWidget.data is a MediaQueryData
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
MediaQueryData
的 ==
如下:bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is MediaQueryData
&& other.size == size
&& other.devicePixelRatio == devicePixelRatio
&& other.textScaleFactor == textScaleFactor
&& other.platformBrightness == platformBrightness
&& other.padding == padding
&& other.viewPadding == viewPadding
&& other.viewInsets == viewInsets
&& other.alwaysUse24HourFormat == alwaysUse24HourFormat
&& other.highContrast == highContrast
&& other.disableAnimations == disableAnimations
&& other.invertColors == invertColors
&& other.accessibleNavigation == accessibleNavigation
&& other.boldText == boldText
&& other.navigationMode == navigationMode;
MediaQuery
的 updateShouldNotify
返回 true
引起子树的 build 行为。runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
那么这样我们可以简单的弄一个 widget 树的层级:
MaterialApp
WidgetsApp
Shortcuts
Actions
FocusTraversalGroup
_MediaQueryFromWindow
MediaQuery
Localizations
...
HomePage
_MediaQueryFromWindow
的核心函数:const _MediaQueryFromWindow({Key key, this.child}) : super(key: key);
final Widget child;
@override
_MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState();
}
class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
// 注册 WidgetsBinding 的监听
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeMetrics() {
// 当 size 变化的时候,触发刷新
setState(() {});
}
@override
Widget build(BuildContext context) {
// 更新 MediaQueryData 值
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
return MediaQuery(
data: data,
child: widget.child,
);
}
}
MediaQueryFromWindows
通过 监听 WidgetsBinding
监听的诸如 Viewport Size 、屏幕亮度、字体大小等系统行为,从而通过变更 MediaQueryData
并通过 MediaQuery
自顶向下传递信息。正常手机:一次申请高度为 400 的空间,然后通过变更键盘 View 的 translateY 做出场动画 三星 S10:每次申请不同的高度,0, 10, 40, .... 300, 350, 400 如此实现动画的过程
Perforamce.setCurrentIsKeyboardScene
函数,当进入需要键盘的场景之后,将上述开关标记为 true,如此在调用 keyboard 的 show 及 hide 函数的 300 ms 内,我们将屏蔽因 WindowInsets 引起的 MediaQuery 的变化;AnimatorContaner
变为 Padding
。3.2 锁屏后键盘无法收回
_handleFoucusChanged
一定是调用了,不管如何我们可以首先在关键节点添加日志。TextInput.hide
的 MessageChannel
的调用,这个时候我们看下 TextInputChannel
的 onMethodCall
方法:if (textInputMethodHandler == null) {
return;
}
switch (method) {
case "TextInput.hide":
textInputMethodHandler.hide();
isKeyBoardShow = false;
result.success(null);
break;
...
}
}
textInputMethodHandler
被赋值为 null
从而造成了当前的问题。FlutterView.detachFromFlutterEngine()
TexInputPlugin.destroy
TextInputChannel.setTextInputMethodHandler(null)
注:上述逻辑因为要考虑混合路由及引擎复用,才会在 onPause 的时候进 detachFromFlutterEngine 操作。
3.3 iOS 上搜狗输入法长按发送未换行
keyboardType: TextInputType.multiline, // 必现是 multiline 否则回车也不生效
maxLines: 5,
minLines: 1,
textInputAction: TextInputAction.send, // 将键盘的回车键显示为 发送按钮
onChanged: (value) {
// 文本变化的回调
},
onSubmitted: (_) {
// 点击发送按钮的回调
},
decoration: const InputDecoration( // 以下是纯为了看起来美观点。。。。
hintText: '输入',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
isDense: true,
border: const OutlineInputBorder(
gapPadding: 0,
borderRadius: const BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1,
style: BorderStyle.none,
),
),
),
),
onChanged
的回调:print('char code is ${v}');
}
13
。textInputAction: TextInputAction.send
注释掉,让其回到正常的回车模式,得到的结果是 10
。编码 | 含义 | String 中的表示 |
---|---|---|
10 | LF 换行,新起一行 | '\n' |
13 | CR 归位,一般指回到当前行的最开始 | '\r' |
EditableTextState.updateEditgingValue
的关系可以在 Framework 层修改,也可以在 FlutterTextInputPlugin 中进行修改,将字符进行替换即可。3.4 iOS 光标动画使得 CPU 飙升
EditableTextState
中,耗时 250ms 从 alpha 1.0 至 0.0 或 0.0 值 1.0,然后间隔 150 ms,之后再 250 ms 的动画,如此往复。3.5 iOS 上键盘收起之后,光标依旧存在
3.6 iOS12+ 长按系统输入法空格光标卡顿不灵敏
UITextInteraction* interaction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
interaction.textInput = self;
[self addInteraction:interaction];
}