美文网首页
Flutter 确保具有焦点时,视口中的TextField或Te

Flutter 确保具有焦点时,视口中的TextField或Te

作者: 不要和哥闹 | 来源:发表于2019-07-30 18:15 被阅读0次

    如何确保TextField或TextFormField在视口中具有焦点而不被键盘覆盖时可见?

    难度:中级

    (最后更新于2018年8月29日,根据应用于Flutter框架v.0.5.7的更改修复_ensureVisible方法中的错误)

    像许多Flutter开发人员一样,我在处理包含TextField或TextFormField的表单时最近遇到了这个问题。当这些字段获得焦点时,键盘会显示并可能覆盖它们。

    浏览互联网,我在GitHub上找到了一个源代码,由Collin Jackson(链接)提供。这段代码部分地解决了问题,但并不完全:如果用户解除键盘然后单击相同的TextField或TextFormField,则解决方案不起作用。

    本文补充了该解决方案,并确保在显示键盘时(即使在被解除之后)这些输入字段始终在视口中可见。

    请注意,此解决方案仅在TextField位于Scrollable区域时有效。

    该解决方案依赖于以下2个概念的使用:

    FocusNode

    使用FocusNode类是为了在Widget获得或失去焦点时得到通知。

    如何使用FocusNode?

    下面的代码说明了一个带有2个TextFormField的Form的非常基本的实现,我们希望在第一个输入框获得和失去焦点时得到通知。

    class TestPage extends StatefulWidget {
        @override
        _TestPageState createState() => new _TestPageState();
    }
    
    class _TestPageState extends State<TestPage> {
        FocusNode _focusNode = new FocusNode();
    
        @override
        void initState(){
            super.initState();
            _focusNode.addListener(_focusNodeListener);
        }
    
        @override
        void dispose(){
            _focusNode.removeListener(_focusNodeListener);
            super.dispose();
        }
    
        Future<Null> _focusNodeListener() async {
            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,
                                ),
                                new TextFormField(
                                    ...
                                ),
                            ],
                        ),
                    ),
                ),
            );
        }
    }
    

    说明

    • 第7行:实例化FocusNode小部件。
    • 第12行:当字段接收/失去焦点时,我们初始化要调用的侦听器。
    • 第17行:非常重要的是我们需要在页面被关闭时删除监听器。
    • 第21-27行:将侦听器实现为异步功能。
    • 第42行:我们将TextFormField绑定到侦听器。

    WidgetsBindingObserver

    WidgetsBindingObserver暴露了一些overridable functions,这些函数在Application, Screen, Memory, Route and Locale触发事件时会被调用。

    有关更多详细信息,请参阅文档

    在本文所涉及的情况下,我们只对屏幕指标(metrics)更改时的通知感兴趣(键盘打开或关闭时就是这种情况)。

    要使用此Observer,我们需要实现一个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的可见性控制委托给专用的Helper Widget,并使用此帮助程序窗口小部件包装 TextField或TextFormField。

    Helper Widget

    辅助Widget(EnsureVisibleWhenFocused)实现了本文前面解释的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
    /// 
    /// 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);
    
        // 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)
        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).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;
        }
    
        position.ensureVisible(
          object,
          alignment: alignment,
          duration: widget.duration,
          curve: widget.curve,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return widget.child;
      }
    }
    

    示例代码
    以下代码说明了解决方案的实现。

    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 确保具有焦点时,视口中的TextField或Te

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