Flutter从入门到精通

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

2018-08-20  本文已影响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),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
上一篇下一篇

猜你喜欢

热点阅读