美文网首页Flutter从入门到精通
Flutter键盘不遮挡输入框,保证输入框获取焦点时可见

Flutter键盘不遮挡输入框,保证输入框获取焦点时可见

作者: 吉原拉面 | 来源:发表于2018-08-20 14:31 被阅读562次

      翻译自原文:https://www.didierboelens.com/2018/04/hint-4-ensure-a-textfield-or-textformfield-is-visible-in-the-viewport-when-has-the-focus/

    2018.09.10更新:
    如果你更新了最新版本的Flutter SDK,控件EnsureVisibleWhenFocused会有两处报错:

     if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) {
          // Move down to the top of the viewport
          alignment = 0.0;
        } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)){
          // Move up to the bottom of the viewport
          alignment = 1.0;
        } else {
          // No scrolling is necessary to reveal the child
          return;
        }
    

    请改为:

    if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) {
          // Move down to the top of the viewport
          alignment = 0.0;
        } 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;
        }
    

    问题背景

      如何保证一个TextField或者TextFormField获取焦点时不被键盘遮挡,在视图中可见?
      难度:中等
      跟很多Flutter开发者一样,在处理包含TextField或者TextFormField的表单时碰到了问题:当TextField或者TextFormField获取焦点时,键盘会弹出,然后可能会遮挡住输入框。
      我在网络上搜索后在GitHub发现了一段Collin Jackson写的代码(link),这段代码部分解决了这个问题,但是如果用户关闭了键盘后再次点击同一个TextField或者TextFormField,那么这个解决方法就行不通了。
      这篇文章完全解决了这个问题,保证了输入框永远在视图中保证可见(即使键盘被关闭过)。
      这个解决方法依赖于以下两点:

    FocusNode

      FocusNode这个类用来通知控件获取/失去焦点。

    如何使用FocusNode

      下方的代码是一个很基础的例子,包含了两个TextFormField,当第一个输入框获取/失去焦点的时候,空间会收到通知:

    class TestPage extends StatefulWidget {
        @override
        _TestPageState createState() => new _TestPageState();
    }
    
    class _TestPageState extends State<TestPage> {
        FocusNode _focusNode = new FocusNode();  // 初始化一个FocusNode控件
    
        @override
        void initState(){
            super.initState();
            _focusNode.addListener(_focusNodeListener);  // 初始化一个listener
        }
    
        @override
        void dispose(){
            _focusNode.removeListener(_focusNodeListener);  // 页面消失时必须取消这个listener!!
            super.dispose();
        }
    
        Future<Null> _focusNodeListener() async {  // 用async的方式实现这个listener
            if (_focusNode.hasFocus){
                print('TextField got the focus');
            } else {
                print('TextField lost the focus');
            }
        }
    
        @override
        Widget build(BuildContext context) {
            return new Scaffold(
                appBar: new AppBar(
                    title: new Text('My Test Page'),
                ),
                body: new SafeArea(
                    top: false,
                    bottom: false,
                    child: new Form(
                        child: new Column(
                            children: <Widget> [
                                new TextFormField(
                                    focusNode: _focusNode,  // 将listener和TextFormField绑定
                                ),
                                new TextFormField(
                                    ...
                                ),
                            ],
                        ),
                    ),
                ),
            );
        }
    }
    

    WidgetsBindingObserver

      WidgetsBindingObserver方法暴露了多个可重写的函数,这些函数在应用/屏幕/内存/路由和地区发生变化时会触发。更详细的内容可以看下这个 文档
      在本文的情况下,我们只关心在屏幕矩阵(Screen metrics)发生改变时的通知(包括键盘的开/闭)。
      使用Observer,我们需要用到mixins(文档有些过时,但是还是有必要看一下,理解mixins的概念)。

    class _TestPageState extends State<TestPage> with WidgetsBindingObserver {
        @override
        void initState(){
            super.initState();
            WidgetsBinding.instance.addObserver(this);
        }
    
        @override
        void dispose(){
            WidgetsBinding.instance.removeObserver(this);
            super.dispose();
        }
    
        ///
        /// This routine is invoked when the window metrics have changed.
        /// 
        @override
        void didChangeMetrics(){
            ...
        }
    }
    

    解决方法

      解决方法包括了两部分:委派一个帮助控件来控制TextField或者TextFormField的可见性;使用这个帮助控件来包装TextField或者TextFormField。

    帮助控件:

      这个帮助控件实现了上面说到的两个监听,全部代码如下:

    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
    ///
    /// How to use it:
    ///
    /// 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
      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
    ///
    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(){
        if (widget.focusNode.hasFocus){
          _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
        final RenderObject object = context.findRenderObject();
        final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
        assert(viewport != null);
    
        // 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;
        double alignment;
    
        if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) {
          // Move down to the top of the viewport
          alignment = 0.0;
        } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)){
          // Move up to the bottom of the viewport
          alignment = 1.0;
        } else {
          // No scrolling is necessary to reveal the child
          return;
        }
    
        position.ensureVisible(
          object,
          alignment: alignment,
          duration: widget.duration,
          curve: widget.curve,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return widget.child;
      }
    }
    

    Sample

      下面的代码展示了如何使用帮助控件:

    class TestPage extends StatefulWidget {
      @override
      _TestPageState createState() => new _TestPageState();
    }
    
    class _TestPageState extends State<TestPage> {
      final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
      FocusNode _focusNodeFirstName = new FocusNode();
      FocusNode _focusNodeLastName = new FocusNode();
      FocusNode _focusNodeDescription = new FocusNode();
      static final TextEditingController _firstNameController = new TextEditingController();
      static final TextEditingController _lastNameController = new TextEditingController();
      static final TextEditingController _descriptionController = new TextEditingController();
    
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          appBar: new AppBar(
            title: new Text('My Test Page'),
          ),
          body: new SafeArea(
            top: false,
            bottom: false,
            child: new Form(
              key: _formKey,
              child: new SingleChildScrollView(
                padding: const EdgeInsets.symmetric(horizontal: 16.0),
                child: new Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    /* -- Something large -- */
                    Container(
                      width: double.infinity,
                      height: 150.0,
                      color: Colors.red,
                    ),
    
                    /* -- First Name -- */
                    new EnsureVisibleWhenFocused(
                      focusNode: _focusNodeFirstName,
                      child: new TextFormField(
                        decoration: const InputDecoration(
                          border: const UnderlineInputBorder(),
                          filled: true,
                          icon: const Icon(Icons.person),
                          hintText: 'Enter your first name',
                          labelText: 'First name *',
                        ),
                        onSaved: (String value) {
                          //TODO
                        },
                        controller: _firstNameController,
                        focusNode: _focusNodeFirstName,
                      ),
                    ),
                    const SizedBox(height: 24.0),
    
                    /* -- Last Name -- */
                    new EnsureVisibleWhenFocused(
                      focusNode: _focusNodeLastName,
                      child: new TextFormField(
                        decoration: const InputDecoration(
                          border: const UnderlineInputBorder(),
                          filled: true,
                          icon: const Icon(Icons.person),
                          hintText: 'Enter your last name',
                          labelText: 'Last name *',
                        ),
                        onSaved: (String value) {
                          //TODO
                        },
                        controller: _lastNameController,
                        focusNode: _focusNodeLastName,
                      ),
                    ),
                    const SizedBox(height: 24.0),
    
                    /* -- Some other fields -- */
                    new Container(
                      width: double.infinity,
                      height: 250.0,
                      color: Colors.blue,
                    ),
    
                    /* -- Description -- */
                    new EnsureVisibleWhenFocused(
                      focusNode: _focusNodeDescription,
                      child: new TextFormField(
                        decoration: const InputDecoration(
                          border: const OutlineInputBorder(),
                          hintText: 'Tell us about yourself',
                          labelText: 'Describe yourself',
                        ),
                        onSaved: (String value) {
                          //TODO
                        },
                        maxLines: 5,
                        controller: _descriptionController,
                        focusNode: _focusNodeDescription,
                      ),
                    ),
                    const SizedBox(height: 24.0),
    
                    /* -- Save Button -- */
                    new Center(
                      child: new RaisedButton(
                        child: const Text('Save'),
                        onPressed: () {
                          //TODO
                        },
                      ),
                    ),
                    const SizedBox(height: 24.0),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    

    相关文章

      网友评论

        本文标题:Flutter键盘不遮挡输入框,保证输入框获取焦点时可见

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