Flutter键盘不遮挡输入框,保证输入框获取焦点时可见
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),
],
),
),
),
),
);
}
}