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

2019-07-30  本文已影响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(
                                ...
                            ),
                        ],
                    ),
                ),
            ),
        );
    }
}

说明

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),
                            ],
                        ),
                    ),
                ),
            ),
        );
    }
}

结论

此解决方案适用于我,我想与您分享。


翻译不易,大家且看且珍惜
原文

上一篇下一篇

猜你喜欢

热点阅读