解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析
Posted Code-Porter
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析相关的知识,希望对你有一定的参考价值。
一、 在项目中遇到了个如下问题:
- 当页面底部有个输入框,点击弹出键盘时;输入框与键盘之间有一段间距
- 通过排除,最后找到了问题根源所在;原因是使用了这个屏幕适配框架导致的。此框架通过直接修改Flutter
ViewConfiguration()
的size与devicePixelRatio达到适配的目的
二、要解决这个问题,就需要了解键盘弹起整个页面做了哪些事情来入手了
- 假设页面如下:
void main() {
runApp(MyApp());
}
class MyApp extends BaseStatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter',
home: Scaffold(
body: Column(
children: [
TextField(),
],
),
),
);
}
}
- 那页面的结构就是如下图了
runApp() ——> MaterialApp() ——> Scaffold() ——> _TextField()
三、来看下的Scaffold #build()
函数是如何布局的
- Scaffold是个StatefulWidget组件,所以只要查看
ScaffoldState
中的build函数即可
@override
Widget build(BuildContext context) {
///省略若干代码....
return _ScaffoldScope(
///省略若干代码....
child: Material(
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
return CustomMultiChildLayout(
children: children,
delegate: _ScaffoldLayout(
extendBody: _extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
///省略若干代码....
),
);
}),
),
}
- 最终布局是通过
CustomMultiChildLayout
组件的,这里就只需要关注minInsets
这个参数就行了: - 那这个参数的值从哪获取的呢?如下代码:
// The minimum insets for contents of the Scaffold to keep visible.
final EdgeInsets minInsets = mediaQuery.padding.copyWith(
bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
);
// The minimum viewPadding for interactive elements positioned by the
// Scaffold to keep within safe interactive areas.
final EdgeInsets minViewPadding = mediaQuery.viewPadding.copyWith(
bottom: _resizeToAvoidBottomInset && mediaQuery.viewInsets.bottom != 0.0 ? 0.0 : null,
);
- 可以知道
minInsets
是个EdgeInsets
对象,而它的bottom值则是从mediaQuery.viewInsets.bottom
中获取的 - 那么
viewInsets
这个参数到底是什么意思呢?来看下类中的注释吧
大家可以自己通过谷歌翻译查看下,大致意思就是:
被系统UI遮挡的部分,当键盘可见时viewInsets.bottom的值对应于键盘的顶部
,也就是说键盘高度会等于viewInsets.bottom的值
3.1 这个MediaQueryData
相信大家都不陌生了吧,用来获取系统的一些信息数据
- 通过MediaQuery.of(context)即可拿到数据
MediaQueryData mediaQuery = MediaQuery.of(context)
- MediaQuery是继承自
InheritedWidget
组件的,用来达到各子组件中数据共享的目的。那这个组件是在什么时候初始化的呢?
四、MediaQuery组件的初始化
- 这个就需要到
MaterialApp
组件中寻找答案了,这同样是个StatefulWidget组件 - 最终代码位于
_MediaQueryFromWindowsState#buld()
中
MaterialApp ——> _MaterialAppState#build() ——> WidgetsApp ——> _WidgetsAppState#build() ——> _MediaQueryFromWindow() ——> _MediaQueryFromWindowsState#build()
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery(
data: data,
child: widget.child,
);
}
- 可以看到是在这里初始化的
MediaQuery
,而数据是从WidgetsBinding.instance!.window
上获取的数据
重点就是这里了:当键盘显示、隐藏时会从window上获取最新的数据(window.viewInsets),然后最终影响Scaffold
的布局;一开始说了是由于使用了屏幕适配框架导致的bug,也就是说这个window上的devicePixelRatio
、size
并没有修改到,这才导致了viewInsets的bottom
值计算错误,可以具体看下MediaQueryData的创建
MediaQueryData.fromWindow(ui.SingletonFlutterWindow window)
: size = window.physicalSize / window.devicePixelRatio,
devicePixelRatio = window.devicePixelRatio,
textScaleFactor = window.textScaleFactor,
platformBrightness = window.platformBrightness,
padding = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio),
viewPadding = EdgeInsets.fromWindowPadding(window.viewPadding, window.devicePixelRatio),
viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio),
systemGestureInsets = EdgeInsets.fromWindowPadding(window.systemGestureInsets, window.devicePixelRatio),
accessibleNavigation = window.accessibilityFeatures.accessibleNavigation,
invertColors = window.accessibilityFeatures.invertColors,
disableAnimations = window.accessibilityFeatures.disableAnimations,
boldText = window.accessibilityFeatures.boldText,
highContrast = window.accessibilityFeatures.highContrast,
alwaysUse24HourFormat = window.alwaysUse24HourFormat,
navigationMode = NavigationMode.traditional;
五、知道了整个流程那怎么解决呢?
- 最后我们希望Scaffold中拿到的viewInsets是经过适配后的值,那就可以解决这个问题了
- 最终利用
MediaQuery.of(context)
从树中向上查找拿到MediaQueryData
这个流程来下手
static MediaQueryData of(BuildContext context) {
assert(context != null);
assert(debugCheckHasMediaQuery(context));
return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}
思路就是:在Scaffold
组件外层包一层自己的MediaQuery
,让它获取的是我们给定的值就可以了,具体代码如下:
- 自定义个创建MediaQuery的组件,将viewInsets改成我们屏幕适配后的值
class KeyboardScaffoldWidget extends StatefulWidget {
final Widget child;
const KeyboardScaffoldWidget({Key? key, required this.child})
: super(key: key);
@override
State<StatefulWidget> createState() {
return _KeyboardScaffoldWidgetState();
}
}
class _KeyboardScaffoldWidgetState extends XyBaseState<KeyboardScaffoldWidget>
with WidgetsBindingObserver {
///设计稿宽度
double screenWidth = 375;
double get adapterRatio {
return window.physicalSize.width / screenWidth;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
@override
void didChangeAccessibilityFeatures() {
setState(() {});
}
// METRICS
@override
void didChangeMetrics() {
setState(() {});
}
@override
void didChangeTextScaleFactor() {
setState(() {});
}
// RENDERING
@override
void didChangePlatformBrightness() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return MediaQuery(
data: createMediaQueryData(adapterRatio),
child: widget.child,
);
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance!.removeObserver(this);
}
//创建适配后的MediaQueryData,解决:键盘顶起输入框有间距问题
///只修改viewInsets的devicePixelRatio
static MediaQueryData createMediaQueryData(double devicePixelRatio) {
SingletonFlutterWindow window = WidgetsBinding.instance!.window;
return MediaQueryData(
///省略部分代码....
///只修改viewInsets的devicePixelRatio
viewInsets:
EdgeInsets.fromWindowPadding(window.viewInsets, devicePixelRatio));
}
}
- 然后使用在
Scaffold
外层就可以解决
return KeyboardScaffoldWidget(
child: Scaffold(...),
);
以上是关于解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析的主要内容,如果未能解决你的问题,请参考以下文章