Flutter 解决App页面软键盘遮挡住输入框底部区域内容

devtools/2024/10/21 11:04:35/

问题现状

有时候我们需要在唤起键盘的时候,输入框不是刚好在键盘上面,比如输入框下面还有按钮。像这样(最终UI效果截图示例):

(当然这个截图比较简单,因为他头部内容不多,键盘升起可能本身也挡不住,这里只是一个示例,代码可以支持头部内容较多情况)我们不希望键盘遮挡到下面的按钮,需要怎么处理呢?

        首先我们需要了解键盘弹起改变了什么(这个网上搜也会有很多,可以自行搜索也可以,我也是搬迁了一个自认为写的不错的博文:https://www.jianshu.com/p/71978b132a89)。

键盘弹起时,Scaffold 发生了什么

Scaffold 的 resize

Scaffold 是 Flutter 中最常用的页面脚手架,前面知道了通过 resizeToAvoidBottomInset ,我们可以配置在键盘弹起时页面的底部按键和 FloatButton 不会再被顶上来,其实这个行为是因为 Scaffold 的 body 大小被 resize 了。

那这个过程是怎么发生的呢?首先如下图所示,我们在 Scaffold 的源码里可以看到,当resizeToAvoidBottomInset 为 true 时,会使用 mediaQuery.viewInsets.bottom 作为 minInsets 的参数,也就是可以确定:键盘弹起时的界面 resize 和 mediaQuery.viewInsets.bottom 有关系

而如下图所示, Scaffold 内部的布局主要是靠 CustomMultiChildLayout ,CustomMultiChildLayout 的布局逻辑主要在 MultiChildLayoutDelegate 对象里。

前面获取到的 minInsets 会被用到 _ScaffoldLayout 这个 MultiChildLayoutDelegate 里面,也就是说  Scaffold的内部是通过 CustomMultiChildLayout 实现的布局,具体实现逻辑在  _ScaffoldLayout 这个 Delegate 里

关于 CustomMultiChildLayout 的详细使用介绍在之前的文章 《详解自定义布局实战》 里可以找到。

接着看 _ScaffoldLayout , 在  _ScaffoldLayout 进行布局时,会通过传入的
minInsets 来决定 body 显示的 contentBottom , 所以可以看到事实上传入的 minInsets 改变的是 Scaffold 布局的 bottom 位置

上图代码中使用的 _ScaffoldSlot.body 这个枚举其实是作为 LayoutId 的值,MultiChildLayoutDelegate 在布局时可以通过 LayoutId 获取到对应 child 进行布局操作,详细可见: 《详解自定义布局实战》

那么 Scaffold 的 body 是什么呢? 如上图代码所示,其实  Scaffold 的 body 是一个叫 _BodyBuilder 的对象,而这个  _BodyBuilder 内部其实是一个 LayoutBuilder。(注意,在 widget.appbar 不为 null 时,会 removeTopPadding

所以如下图代码所示 body 在添加时,它父级的MediaQueryData 会被重载,特别是 removeTopPadding 会被清空,viewInsets.bottom 也是会被重置

最后如下代码所示,_BodyBuilder 的 LayoutBuilder 里会获取到一个 top 和 bottom 的参数,这两个参数都通过前面在  _ScaffoldLayout 布局时传入的 constraints 去判断得到,最终 copyWith 得到新的 MediaQuery 。

这里就涉及到一个有意思的点,在 _BodyBuilder 里的通过 copyWith 得到新的 MediaQuery 会影响什么呢?如下代码所示,这里用一个简单的例子来解释下。

class MainWidget extends StatelessWidget {final TextEditingController controller =new TextEditingController(text: "init Text");@overrideWidget build(BuildContext context) {print("Main MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}");return Scaffold(appBar: AppBar(title: new Text("MainWidget"),),extendBody: true,body: Column(children: [new Expanded(child: InkWell(onTap: (){FocusScope.of(context).requestFocus(FocusNode());})),///增加 CustomWidgetCustomWidget(),new Container(margin: EdgeInsets.all(10),child: new Center(child: new TextField(controller: controller,),),),new Spacer(),],),);}
}
class CustomWidget extends StatelessWidget {@overrideWidget build(BuildContext context) {print("Custom MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}\n  \n");return Container();}
}

如上代码所示:

  • 代码中定义了 MainWidget 和 CustomWidget 两个控件;
  • MainWidget 里使用了 Scaffold ,并且 CustomWidget 在 MainWidget 里被使用;
  • 分别在这两个 Widget 的build 方法里打印出对应的 MediaQuery.of(context).padding 和 MediaQuery.of(context).viewInsets.bottom 的值;

如下图所示,在键盘弹起和不弹起时可以看到 padding 值是不同的,而 viewInsets.bottom 都为 0。

为什么  padding 值的 top 会不一致,自然是因为 CustomWidget 和 MainWidget获取到的 MediaQuery.of(context) 对象不是同一个数据。

  • MainWidget 使用的 MediaQuery.of(context) 得到的 MediaQueryData 是上级往下传递的,里面包含了 top:47 的状态栏高度和 bottom:34 的底部安全区域高度
  • CustomWidget 里面 MediaQuery.of(context) 得到的 MediaQueryData ,自然就是前面分析过的 _BodyBuilder 里的通过 copyWith 得到新的 MediaQuery,所以  CustomWidget 得到的 MediaQueryData 其实在 Scaffold 内部已经被重置了,所以它的 top:0 ,获取不到状态栏高度

事实上这就是大家为什么有时候 MediaQuery.of( context) 可以获取到状态栏高度,有时候又获取不到的原因,因为你的 context 获取到的是 Scaffold 之外的 MediaQueryData, 还是 Scaffold 内被重载过的 MediaQueryData,自然会得到不一样的结果。

如下图所示,键盘弹起因为被 resize 了,所以界面的 bottom 安全区域变成了 0 ,而

  • 在 MainWidget 中可以获取到 viewInsets.bottom 也就是键盘的高度;
  • 在 CustomWidget 获取不到 viewInsets.bottom ,因为在 Scaffold 内被重载清除了。

总结一下:Scaffold 的 resizeToAvoidBottomInset 会通过 MediaQueryData 影响 body 的布局,同时在 Scaffold 内 MediaQuery 会被重载,所以使用的 context 位置不同,获取到的 MediaQueryData 也不同,如果需要获取键盘高度和状态栏高度的话,最好使用  Scaffold 外的  context 。

这里讲了 MediaQuery 和 MediaQueryData 的内容,为什么 MediaQuery 通过嵌套就可以重载?为什么通过 context 可以往上获取到离 context 最近的  MediaQueryData?因为 MediaQuery 是一个 InheritedWidget : 《全面理解State》 。

键盘如何影响 Scaffold

前面我们聊了 Scaffold 的 resizeToAvoidBottomInset 会通过 MediaQueryData 影响 body 的布局,那是怎么影响的呢?

事实上这得从 MaterialApp 说起,在  MaterialApp 内部的深处嵌套着一个叫 _MediaQueryFromWindow 的 Widget ,它在内部通过 WidgetsBinding.instance.addObserver 对 App 的各种系统事件做了监听,并且对应都执行了 setState 。

所以如下源码所示,当键盘弹出时, build 方法会被执行, 而 MediaQueryData 就会通过MediaQueryData.fromWindow 获取到新的 MediaQueryData 数据。

 @overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);}// ACCESSIBILITY@overridevoid didChangeAccessibilityFeatures() {setState(() { });}// METRICS@overridevoid didChangeMetrics() {setState(() {});}@overridevoid didChangeTextScaleFactor() {setState(() { });}// RENDERING@overridevoid didChangePlatformBrightness() {setState(() {});}@overrideWidget build(BuildContext context) {MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);if (!kReleaseMode) {data = data.copyWith(platformBrightness: debugBrightnessOverride);}return MediaQuery(data: data,child: widget.child,);}@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);super.dispose();}

举个例子,如下图所示,从 Android 的 Java 层弹出键盘开始,会把改变后的视图信息传递给 C++ 层,最后回调到 Dart 层,从而触发 MaterialApp 内的 didChangeMetrics 方法执行 setState(() {}); ,进而让 _MediaQueryFromWindow 内的 build 更新了 MediaQueryData ,最终改变了 Scaffod 的 body 大小。

那么到这里,我们可以了解到键盘升起的scaffold的变化内容。尤其注意MediaQuery.of( context)在scffold内外的内容可能会存在不一样的问题。

解决方案

        我们在scaffold组件存在的widget中添加WidgetsBindingObserver,在页面尺寸改变的回调方法中监听键盘的升起,然后对内部的列表做滑动偏移操作(注:如果observer在子组件中的话这里的滑动也是根据最初键盘未弹起的时候的参数的,偏移执行的时候也会感觉无效的状况)。

代码示例如下:


class FamilyWifiSelectPage extends StatefulWidget {const FamilyWifiSelectPage({Key? key}) : super(key: key);@overrideState<FamilyWifiSelectPage> createState() => _FamilyWifiSelectPageState();
}class _FamilyWifiSelectPageState extends State<FamilyWifiSelectPage>with WidgetsBindingObserver {FamilyWifiSelectProvider? _familyWifiSelectProvider;@overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);}/// 页面尺寸改变时回调@overridedidChangeMetrics() {super.didChangeMetrics();// 在页面重新渲染完成之后,获取软键盘高度WidgetsBinding.instance.addPostFrameCallback((timeStamp) {var isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;if (isKeyboardOpen) {ScrollController? scrollController =_familyWifiSelectProvider?.scrollViewController;
//这里我将整体滑动到底,具体可以根据你自己的布局来控制即可scrollController?.jumpTo(scrollController.position.maxScrollExtent);}});}@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);super.dispose();}Widget _buildNextBtn() {return HYXButton(width: 351.ratio,height: 39.ratio,state: HYXButtonState.normal,title: LocaleKeys.startConfig.tr,onPressed: () async {...},);}@overrideWidget build(BuildContext context) {return Scaffold(appBar: HYXAppBar(title: sn),body: ColoredBox(color: HYXColors.backGround,child: Column(children: [Expanded(child:FamilyWifiSelectWidget(supperContext: context)),_buildNextBtn(),],),),);}
}

希望能给你遇到的问题能提供一些解题思路。


http://www.ppmy.cn/devtools/41685.html

相关文章

【qt】设计器实现界面

设计器实现界面 一.总体思路二.具体操作1.创建项目2.粗略拖放3.水平布局4.垂直布局5.修改名字6.转到槽7.实现槽函数 一.总体思路 创建项目粗略拖放水平布局垂直布局修改名称转到槽实现槽函数 二.具体操作 1.创建项目 这次咱们一定要勾选Generate form哦。 因为我们要使用设…

基于Sentinel-1遥感数据的水体提取

本文利用SAR遥感图像进行水体信息的提取&#xff0c;相比光学影像&#xff0c;SAR图像不受天气影响&#xff0c;在应急情况下应用最多&#xff0c;针对水体&#xff0c;在发生洪涝时一般天气都是阴雨天&#xff0c;云较多&#xff0c;光学影像质量较差&#xff0c;基本上都是利…

sql操作、发送http请求和邮件发送 全栈开发之路——后端篇(2)

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

《五》Word文件编辑软件调试及测试

上一期&#xff0c;我们已经把大致的框架给完成了&#xff0c;那么今天&#xff0c;我们就把剩下的什么复制啊&#xff0c;改变字体啊什么的给做一下。 那我们就一步一步的来就可以了&#xff1a; 新建word&#xff1a; void MyWord::fileNew() {qDebug()<<"hhh&…

K8s 多租户管理

一、K8s 多租户管理 多租户是指在同一集群中隔离多个用户或团队&#xff0c;以避免他们之间的资源冲突和误操作。在K8s中&#xff0c;多租户管理的核心目标是在保证安全性的同时&#xff0c;提高资源利用率和运营效率。 在K8s中&#xff0c;该操作可以通过命名空间&#xff0…

亚信安全发布《2024年第一季度网络安全威胁报告》

亚信安全2024年第一季度网络安全威胁报告 一季度威胁概览 《亚信安全2024年第一季度网络安全威胁报告》的发布旨在从一个全面的视角解析当前的网络安全威胁环境。此报告通过详尽梳理和总结2024年第一季度的网络攻击威胁&#xff0c;目的是提供一个准确和直观的终端威胁感知。…

前端 JS 经典:原型和原型链

1. 前言 这个前言很重要&#xff0c;要理解原型、原型链&#xff0c;就需要理解前言里面的一些定义。开始&#xff01; 所有对象都是通过 new 一个函数去创建的&#xff0c;而这个函数通常首字母大写&#xff0c;被称为构造函数。我们也可以通过自定义构造函数&#xff0c;去…

图文详解JUC:Wait与Sleep的区别与细节

目录 一.Wait() 二.Sleep() 三.总结Wait()与Sleep()的区别 一.Wait() 在Java中&#xff0c;wait() 方法是 Object类中的一个方法&#xff0c;用于线程间的协作。当一个线程调用wait() 方法时&#xff0c;它会释放对象的锁并进入等待状态&#xff0c;直到其他线程调用相同对…