解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析

Posted Code-Porter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析相关的知识,希望对你有一定的参考价值。

一、 在项目中遇到了个如下问题:

  • 当页面底部有个输入框,点击弹出键盘时;输入框与键盘之间有一段间距
  • 通过排除,最后找到了问题根源所在;原因是使用了这个屏幕适配框架导致的。此框架通过直接修改FlutterViewConfiguration()的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上的devicePixelRatiosize并没有修改到,这才导致了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布局流程)解析的主要内容,如果未能解决你的问题,请参考以下文章

解决Flutter键盘弹起导致与输入框有间距问题(Flutter键盘弹起Scaffold布局流程)解析

flutter中关于软键盘弹起导致的问题

Flutter键盘弹出造成布局异常解决

Flutter键盘弹出造成布局异常解决

Flutter键盘弹出造成布局异常解决

flutter弹起键盘页面布局超限问题