Flutter的弹窗与软键盘交互
前言
开发过 Flutter 应用的同学可能对软键盘多多少少都被坑过,但是大多数使用场景是页面中的软键盘弹起之后遮挡布局的问题,其实也好解决。
主流的方案是加滚动布局,或者设置 Scaffold 的 resizeToAvoidBottomInset 属性,但是如果是弹窗呢?如果不做处理是什么情况?
上图:
如果做了适配之后应该是这样的:
那么弹窗与软键盘的适配怎么做呢?
一、软键盘开关监听
Flutter 中有一些软键盘的插件,可以监听和获取到不同平台下软键盘的弹出与收起的状态,我们可以通过监听软键盘的弹出与收起,给 Dialog 的布局设置不同的 Padding 即可。
比如以我用的 flutter_keyboard_visibility
插件为例:
scala
class VerifyPaymentPasswordView extends StatefulWidget {
VerifyPaymentPasswordView({super.key, this.confirmAction});
VoidCallback? confirmAction;
@override
State<VerifyPaymentPasswordView> createState() => _VerifyPaymentPasswordViewState();
}
class _VerifyPaymentPasswordViewState extends State<VerifyPaymentPasswordView> {
final TextEditingController _controller = TextEditingController();
final FocusNode _node = FocusNode();
bool isKeyboardVisible = false;
StreamSubscription<bool>? subscription;
void _updateKeyboardVisibility(bool isVisible) {
setState(() {
isKeyboardVisible = isVisible;
});
}
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_node.requestFocus();
});
var keyboardVisibilityController = KeyboardVisibilityController();
subscription = keyboardVisibilityController.onChange.listen((bool visible) {
_updateKeyboardVisibility(visible);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: isKeyboardVisible?MediaQuery.of(context).viewInsets:EdgeInsets.zero,
...
);
}
效果:
如果是底部的弹窗呢?
scss
class ReviewDialog extends StatefulWidget {
const ReviewDialog({Key? key}) : super(key: key);
@override
State<ReviewDialog> createState() => _ReviewDialogState();
}
class _ReviewDialogState extends State<ReviewDialog> {
final _focusNode = FocusNode();
bool isKeyboardVisible = false;
StreamSubscription<bool>? subscription;
void _updateKeyboardVisibility(bool isVisible) {
setState(() {
isKeyboardVisible = isVisible;
});
}
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
var keyboardVisibilityController = KeyboardVisibilityController();
subscription = keyboardVisibilityController.onChange.listen((bool visible) {
Log.d('Keyboard visibility update. Is visible: $visible');
_updateKeyboardVisibility(visible);
});
super.initState();
}
@override
void dispose() {
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: isKeyboardVisible ? MediaQuery.of(context).viewInsets.bottom : 0,
),
child: Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: TextField(focusNode: _focusNode),
),
);
}
}
只设置底部的 padding 即可,只是具体弹出Dialog的时候,对齐方式的不同而已,具体的交互是一样的效果。
看代码或效果图(GIF掉帧不明显)能看出,这种效果是可以完成我们要的效果了,但是突然的上下跳动相对来说比较突兀。
二、监听 EdgeInsets 值变化
能不能监听软键盘弹起的过程中整个页面中 EdgeInsets 的变化呢?让我们的布局跟随这个状态不停的刷新不就可以实现类似动画的效果了吗?
如何监听页面中 EdgeInsets 的变化呢?
scala
class ReviewDialog extends StatefulWidget {
const ReviewDialog({Key? key}) : super(key: key);
@override
State<ReviewDialog> createState() => _ReviewDialogState();
}
class _ReviewDialogState extends State<ReviewDialog> with WidgetsBindingObserver {
final _focusNode = FocusNode();
EdgeInsets viewInsets = EdgeInsets.zero;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
// 注册 WidgetsBindingObserver
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
// 移除 WidgetsBindingObserver
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
// 获取最新的 viewInsets 值
final newInsets = WidgetsBinding.instance.window.viewInsets;
setState(() {
viewInsets = EdgeInsets.fromViewPadding(newInsets, WidgetsBinding.instance.window.devicePixelRatio);
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: viewInsets.bottom,
),
child: Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: TextField(focusNode: _focusNode),
),
);
}
}
这里就不重复贴居中的弹窗布局了,是一样的监听方案。
居中弹窗效果:
底部弹窗效果:
效果比软键盘弹起与收起的监听的效果要稍好。
三、直接用AnimatedPadding或Padding
为什么要监听,浪费内存与性能。直接使用 AnimatedPadding 设置一个目标的 Padding 值与动画执行时间,让弹窗按自己的方式做动画,这是网上推荐比较多的方案。
怎么实现呢?
scala
class VerifyPaymentPasswordView extends StatefulWidget {
VerifyPaymentPasswordView({super.key, this.confirmAction});
VoidCallback? confirmAction;
@override
State<VerifyPaymentPasswordView> createState() => _VerifyPaymentPasswordViewState();
}
class _VerifyPaymentPasswordViewState extends State<VerifyPaymentPasswordView> {
final TextEditingController _controller = TextEditingController();
final FocusNode _node = FocusNode();
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_node.requestFocus();
});
super.initState();
}
@override
Widget build(BuildContext context) {
return AnimatedPadding(
padding: MediaQuery.of(context).viewInsets,
...
);
}
效果同样能实现,但是由于带有动画的原因,顺滑度与响应速度上还是比不上上面的两种方案。
这里抛出一个疑问,为什么 AnimatedPadding 的 padding 可以直接使用一个 MediaQuery.of(context).viewInsets 值?有谁给他设置状态了吗?
其实并没有,只是在软键盘弹起的过程中,页面的 viewInsets 发生了改变,会触发 build 方法重新构建视图而已,所以每次 build 之后就可以拿到最新的 viewInsets 值,就能间接的实现设置状态的效果,也就能动啦。
那我直接用 Padding 岂不是更高效? 还避免了重复 build 的问题。
scala
class ReviewDialog extends StatefulWidget {
const ReviewDialog({Key? key}) : super(key: key);
@override
State<ReviewDialog> createState() => _ReviewDialogState();
}
class _ReviewDialogState extends State<ReviewDialog> {
final _focusNode = FocusNode();
EdgeInsets viewInsets = EdgeInsets.zero;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: TextField(focusNode: _focusNode),
),
);
}
}
效果:
总结
害,早说啊,害我走那么多弯路!😅😅
其实如果只是做对应的弹窗效果的话,确实直接用 MediaQuery.of(context).viewInsets 相关的对象即可实现。
但是软键盘的状态监听与viewInsets的值监听在一些特殊的场景下是有作用的,比如当软键盘关闭的时候做出一些移除焦点,清空内容等等的特殊处理。
由于代码比较简单,本文全部贴出没有提供 Demo。由于 GIF 录制出来有压缩效果不明显,其实在 Debug 模式下效果是有明显区别的,有兴趣大家可以自行测试。也可以少走弯路直接使用最后的方案即可。
那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出。
如果感觉本文对你有一点点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力啦。
Ok,这一期就此完结。