美文网首页dartFlutterflutter
Flutter中解决输入框(TextField)被键盘遮挡问题

Flutter中解决输入框(TextField)被键盘遮挡问题

作者: 天上飘的是浮云 | 来源:发表于2020-09-05 10:00 被阅读0次

      最近在工作中遇到了文本框被输入法遮挡的问题,在网上找了一些方法,一言难尽,现在很多人写技术博客,要不就随便转一转,或者随便一写,也不讲解清楚,或者传图等。个人觉得不好,既然要写技术博客,就要把他写好,可能自己会麻烦点,费点事。但是,如果要写博客,我觉得要尽量让人理解,不要就放个链接,或者放段代码等等,一副只可意会不可言传的表情~~
      这是我写博客的初衷,给自己留下知识,也给别人带来知识。尽管你的一篇技术博客内容可能很简单。最近在一本《靠谱》的书中读到一节“让对方听得懂”,也是这么个意思,把读你博客的读者都当成小白,用简练的文字向读者讲述你的知识。

    扯远了~
    《Flutter的拨云见日》系列文章如下:
    1、Flutter中指定字体(全局或者局部,自有字库或第三方)
    2、Flutter发布Package(Pub.dev或私有Pub仓库)
    3、Flutter中解决输入框(TextField)被键盘遮挡问题

    一、Flutter自带文本框自适应输入法buff

      首先一个页面如果在buildView中被包裹在Scaffold组件中,那么很幸运Scaffold是自带自适应输入法弹出的,它有一个属性resizeToAvoidBottomInset,用来控制Scaffold组件是否需要自适应输入法弹出,重新计算view的高度,它是默认打开的。

    Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: _buildContentView(context)  //被ListView或者SingleChildScrollView等滑动控件包裹的TextField
        );
    

    效果如下图(如果文本框被输入法遮挡,Scaffold会默认重新计算整个View的高度,其实也就是减去输入法的高度,让文本框滑动不被遮挡):


    normal.gif

    并且随着输入的字数增加,文本框是可以自适应向上滑动的。自带的就是香~👍👍👍

    下面我们看下resizeToAvoidBottomInset设置为false,也就是不自适应输入法的情况,如图:

    Scaffold(
          resizeToAvoidBottomInset: false,
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: _buildContentView(context) //被ListView或者SingleChildScrollView等滑动控件包裹的TextField
    );
    
      _buildContentView(BuildContext context) {
        return Container(
          padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 20.0, ),
          child: Stack(
            children: [
    ///用SingleChildScrollView或者ListView都可以
    //          SingleChildScrollView(
    //            child: Column(
    //                  children: [
    //                    Text(
    //                      '''
    //                      ''',
    //                      style: TextStyle(fontSize: 20.0, color: Colors.black),
    //                    ),
    //                   TextField(
    //                    ),
    //                  ],
    //                )
                 ListView(
                      children: [
                        Text(
                          '''
                          ''',
                          style: TextStyle(fontSize: 20.0, color: Colors.black),
                        ),
                       TextField(
                        ),
                      ],
                 )
            ],
          ),
        );
      }
    
    no_resize.gif

    二、“变态”需求,文本框全显示

      在Part 1中,其实我们可以看到flutter Scaffold已经为大家考虑了文本框被输入法遮挡的问题,文本框也可以根据输入的问题自适应向上滑动,可以木有办法,PO要求文本框全部显示粗来,怎么办? 😩😩😩~

      木有办法,只有把民工必备技能使出,只有把度娘、古哥请出来。还真别说,还真是乱七八糟的,没一个讲清楚,讲透的。不是没图没真相,就是贴了一段不知出处的代码。算了算了,实践出真知。

    2.1 首先,看图说话,下图确实做到了,输入法弹出是,文本框全显示,👍👍👍(有两把烂刷子~ 😆)
    resize_display_all.gif
    2.2 嗯,前方高能,上一段不知出处的代码👌😯
    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:meta/meta.dart';
    
    ///
    /// Helper class that ensures a Widget is visible when it has the focus
    /// For example, for a TextFormField when the keyboard is displayed
    ///
    /// 使用方法:
    ///
    /// In the class that implements the Form,
    ///   Instantiate a FocusNode
    ///   FocusNode _focusNode = new FocusNode();
    ///
    /// In the build(BuildContext context), wrap the TextFormField as follows:
    ///
    ///   new EnsureVisibleWhenFocused(
    ///     focusNode: _focusNode,
    ///     child: new TextFormField(
    ///       ...
    ///       focusNode: _focusNode,
    ///     ),
    ///   ),
    ///
    /// Initial source code written by Collin Jackson.
    /// Extended (see highlighting) to cover the case when the keyboard is dismissed and the
    /// user clicks the TextFormField/TextField which still has the focus.
    ///
    class EnsureVisibleWhenFocused extends StatefulWidget {
      const EnsureVisibleWhenFocused({
        Key key,
        @required this.child,
        @required this.focusNode,
        this.curve: Curves.ease,
        this.duration: const Duration(milliseconds: 100),
      }) : super(key: key);
    
      /// The node we will monitor to determine if the child is focused
      ///传入FocusNode,用于监听TextField获取焦点事件
      final FocusNode focusNode;
    
      /// The child widget that we are wrapping
      final Widget child;
    
      /// The curve we will use to scroll ourselves into view.
      ///
      /// Defaults to Curves.ease.
      final Curve curve;
    
      /// The duration we will use to scroll ourselves into view
      ///
      /// Defaults to 100 milliseconds.
      final Duration duration;
    
      @override
      _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState();
    }
    
    ///
    /// We implement the WidgetsBindingObserver to be notified of any change to the window metrics
    ///实现WidgetsBindingObserver接口,监听屏幕矩阵变化事件
    class _EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> with WidgetsBindingObserver  {
    
      @override
      void initState(){
        super.initState();
        widget.focusNode.addListener(_ensureVisible);  ///监听焦点事件
        WidgetsBinding.instance.addObserver(this);      ///监听屏幕矩阵是否发生变化
      }
    
      @override
      void dispose(){
        WidgetsBinding.instance.removeObserver(this);
        widget.focusNode.removeListener(_ensureVisible);
        super.dispose();
      }
    
      ///
      /// This routine is invoked when the window metrics have changed.
      /// This happens when the keyboard is open or dismissed, among others.
      /// It is the opportunity to check if the field has the focus
      /// and to ensure it is fully visible in the viewport when
      /// the keyboard is displayed
      ///屏幕矩阵发生变化时系统调用,如键盘弹出或是收回
      @override
      void didChangeMetrics(){
        super.didChangeMetrics();
        if (widget.focusNode.hasFocus){ ///有焦点时,进入滑动显示处理Function
          _ensureVisible();
        }
      }
    
      ///
      /// This routine waits for the keyboard to come into view.
      /// In order to prevent some issues if the Widget is dismissed in the
      /// middle of the loop, we need to check the "mounted" property
      ///
      /// This method was suggested by Peter Yuen (see discussion).
      ///等待键盘显示在屏幕上
      Future<Null> _keyboardToggled() async {
        if (mounted){
          EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
          while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
            await new Future.delayed(const Duration(milliseconds: 10));
          }
        }
    
        return;
      }
    
      Future<Null> _ensureVisible() async {
        // Wait for the keyboard to come into view
        await Future.any([new Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);
    
        // No need to go any further if the node has not the focus
        if (!widget.focusNode.hasFocus){
          return;
        }
        // Find the object which has the focus
        //找到Current RenderObjectWidget,获得当前获得焦点的widget,这里既TextField
        final RenderObject object = context.findRenderObject();
        final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
    
        // If we are not working in a Scrollable, skip this routine
        if (viewport == null) {
          return;
        }
    
        // Get the Scrollable state (in order to retrieve its offset)
        //获取滑动状态,目的是为了获取滑动的offset
        ScrollableState scrollableState = Scrollable.of(context);
        assert(scrollableState != null);
    
        // Get its offset
        ScrollPosition position = scrollableState.position;
        double alignment;
    
        ///这里需要解释下
        ///1、position.pixels是指滑动widget,滑动的offset(一般指距离顶部的偏移量(滑出屏幕多少距离))
        ///2、viewport.getOffsetToReveal(object, 0.0).offset 这个方法,可以看下源码
        ///      他有一个alignment参数,0.0 代表显示在顶部,0.5代表显示在中间,1.0代表显示在底部
        ///       offset是指view显示在三个位置时距离顶部的偏移量
        ///       他们两者相比较就可以知道当前滑动widget是需要向上还是向下滑动,来完全显示TextField
    
        ///判断TextField处于顶部时是否全部显示,需不需下滑来完整显示
        if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) { 
          // Move down to the top of the viewport
          alignment = 0.0;
        ///判断TextField处于低部时是否全部显示,需不需上滑来完整显示
        } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset){
          // Move up to the bottom of the viewport
          alignment = 1.0;
        } else {
          // No scrolling is necessary to reveal the child
          return;
        }
    
        //这是ScrollPosition的内部方法,将给定的view 滚动到给定的位置,
       //alignment的意义和上面描述的一致, 三种位置顶部,底部,中间
        position.ensureVisible(
          object,
          alignment: alignment,
          duration: widget.duration,
          curve: widget.curve,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return widget.child;
      }
    }
    

    这里的不可描述代码挺长的,其实仔细看并不复杂,有很多基本我已经进行了中文解释。大家多看代码
    1、传入FocusNode,这里是为了监听TextField获取焦点情况
    2、实现WidgetsBindingObserver接口,是为了监听屏幕矩阵变化(输入法弹出或收回)
    3、在didChangeMetrics()方法中接受屏幕矩阵变化,进入滑动逻辑处理方法_ensureVisible()
    4、在_ensureVisible()方法中首先会进行300毫秒的循环等待,等待输入法显示在屏幕中
    5、然后获取当前获取焦点的RenderObject

        // Find the object which has the focus
        final RenderObject object = context.findRenderObject();
        final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
    

    6、获取滑动widget的ScrollPosition,实则是为了获取滑动的偏移量(也就是滑出屏幕距离)

        // Get the Scrollable state (in order to retrieve its offset)
        ScrollableState scrollableState = Scrollable.of(context);
        assert(scrollableState != null);
    
        // Get its offset
        ScrollPosition position = scrollableState.position;
    

    7、先解释viewport.getOffsetToReveal(object, 0.0).offset方法,可以看下源码,他有一个alignment参数,0.0 代表显示在顶部,0.5代表显示在中间,1.0代表显示在底部。offset是指view显示在三个位置时距离顶部的偏移量

        ///判断TextField处于顶部时是否全部显示,需不需下滑来完整显示
        if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) { 
          // Move down to the top of the viewport
          alignment = 0.0;
        ///判断TextField处于低部时是否全部显示,需不需上滑来完整显示
        } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset){
          // Move up to the bottom of the viewport
          alignment = 1.0;
        } else {
          // No scrolling is necessary to reveal the child
          return;
        }
    

    7.1 position.pixels > viewport.getOffsetToReveal(object, 0.0).offset 代表这种情况


    image.png image.png

    7.2 position.pixels < viewport.getOffsetToReveal(object, 1.0).offset 代表这种情况


    image.png

    8、根据7中比较得出的alignment也就是需要显示的位置,调用ScrollPosition内部方法滑动至指定位置

        //这是ScrollPosition的内部方法,将给定的view 滚动到给定的位置,
       //alignment的意义和上面描述的一致, 三种位置顶部,底部,中间
        position.ensureVisible(
          object,
          alignment: alignment,
          duration: widget.duration,
          curve: widget.curve,
        );
    

    文字多有难懂,意乱,该用图时就用图,上图
    第一个TextField的位置是alignment = 0.0, 底下那个TextField的位置是alignment = 1.0


    滑动位置根据alignment.gif
    2.3 使用方法

    代码中也是讲到了,其实他就是一个包装类,将TextField用EnsureVisibleWhenFocused类包裹就可以,并讲FocusNode传入,因为它需要监听焦点

    EnsureVisibleWhenFocused(
      focusNode: _contentFocusNode,
        child: TextField(
    
        ),
    ),
    

    三、全显需求解决,还剩下一个问题

      因为有时候我们使用ListView或者ScrollView,然后这些滑动View中有文本框,我们在页面底部需要有一个Submit或者Next按钮。 这需求并不变态,常规操作。

    如图:


    底部固定悬浮按钮.png

    实现底部固定悬浮按钮,想必大家都知道,类似于android中的FrameLayout,在Flutter中我们可以使用Stack和Positioned两个widget实现。 这不难。

    嗯~ 实现个这个有什么难度,小case!!!

    可是当你再一点文本框输入时,你傻脸了,什么鬼? 底部固定悬浮按钮跟着输入框一起上来了, ̄□ ̄||.png

    什么原因?我是谁?我在哪?因为前面说过Scaffold默认会打开重新计算View高度的设置,而布局是Stack的,Next按钮使用Positioned布局在离底部38.0px的地方,自然就出现在输入法上面了~~~
    但是又不能把Scaffold设置关掉,因为关了文本框又不会自适应滑动了~

    Stack(
       children: [
          ListView(
             children: [],
          ),
    
          Positioned(
               bottom: 38.0,
               left: 0,
               right: 0,
               child: MaterialButton(
               ),
          ),
        ],
    ),
    

    大写尴尬~

    后面思考了下,还是有办法滴~
    主要是有两种方式:

    3.1 第一种估计都能想到,监听键盘弹出事件,来隐藏或者显示Next按钮

    既然想到就开始action吧~,
    1、这里我用了第三方库来获取键盘弹出事件--->flutter_keyboard_visibility: 3.2.2
    2、自己动手类似于Part 2中一样继承StatefullWidget,自己捣鼓一个监听键盘的包装类

    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
    import 'package:meta/meta.dart';
    
    class EnsureButtonVisibleWhenFocused extends StatefulWidget {
      const EnsureButtonVisibleWhenFocused({
        Key key,
        @required this.child,
      }) : super(key: key);
    
      /// The child widget that we are wrapping
      final Widget child;
    
      @override
      _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState();
    }
    
    class _EnsureVisibleWhenFocusedState extends State<EnsureButtonVisibleWhenFocused> {
      bool isKeyboardVisible = false;
    
      @override
      void initState(){
        super.initState();
        KeyboardVisibility.onChange.listen((isKeyboardVisible) {
          if(this.mounted) {
            setState(() {
              this.isKeyboardVisible = isKeyboardVisible;
            });
          }
        });
      }
    
      @override
      void dispose(){
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return isKeyboardVisible ? SizedBox() : widget.child;
      }
    }
    

    3、将监听键盘包装类EnsureButtonVisibleWhenFocused包裹Next Button

    EnsureButtonVisibleWhenFocused(
       child: Positioned(
         bottom: 38.0,
         left: 0,
         right: 0,
         child: MaterialButton(
         ),
       ),
    )
    

    4、成品展示,铛铛铛铛~


    resize_display_all_with_next_display.gif
    3.2 是不是感觉方法一过于繁琐?魔改从无止境~

    1、在Page页面的build方法中加入

    @override
    Widget build(BuildContext context) {
        double bottom = MediaQuery.of(context).viewInsets.bottom;   ///这里bottom为0说明键盘没有弹出,>0则是键盘弹出
    }
    

    2、在Positioned中加入如下鬼魅逻辑O(∩_∩)O哈哈~

    Positioned(
       bottom: bottom > 0 ? 100.0 * -1 : 38.0, ///键盘弹出时,给Positioned的bottom设置负值,
                                                          ///那么它肯定被遗落在看不见的边边jiaojiao
                                                          ///当键盘收回时,给其设置正常的bottom就粗现了
       left: 0,
       right: 0,
        child: MaterialButton(
       ),
    )
    

    3、效果和3.1出奇的一致,不贴图了,去试试吧~

    四、收队

    一上午加一中午,写博客实属不易,很简单的东西,要全部写出来,写清楚,讲明白,还是很耗费时间的,我既然写,就要把它写清楚,讲明白,这是我的初衷。希望是偶确实写清楚,讲明白的。如有不明白,欢迎留言,偶们一起探讨,为您解忧。也有助于偶更好的写清楚,讲明白~

    今天就到这吧~ 休息休息会儿

    申明:禁用于商业用途,如若转载,请附带原文链接。https://www.jianshu.com/p/5bf431c5d03d蟹蟹~

    PS: 写文不易,觉得没有浪费你时间,请给个点赞~ 😁

    相关文章

      网友评论

        本文标题:Flutter中解决输入框(TextField)被键盘遮挡问题

        本文链接:https://www.haomeiwen.com/subject/kpgvsktx.html