原文地址:medium.com/flutter/imp…html
原文做者:medium.com/@perclassongit
发布时间:2018年9月2日github
想象一下:你设计了你的迷人的表格。app
你把它发给你的产品经理,他看了看说:"那我得把整个国家的名字都打进去?你就不能在我输入的时候给我看看建议吗?"而后你就想:"嗯,他说得对!"嗯,他是对的!" 因此你决定实现一个 "typeahead",一个 "自动完成 "或任何你想叫它的东西。一个文本字段,在用户输入时显示建议。你开始工做......你知道如何得到建议,你知道如何作逻辑,你什么都知道......除了如何让建议漂浮在其余widget之上。ide
你想想,为了达到这个目的,你必须把整个屏幕从新设计成一个Stack,而后计算出每一个widget必须显示的确切位置。这很是麻烦,很是严格,很是容易出错,并且感受就是不对。但还有另外一种方法。函数
你可使用Flutter预先提供的Stack
,即 Overlay 。工具
在这篇文章中,我将解释如何使用Overlaywidget来建立浮在其余一切之上的widget,而没必要重组你的整个视图。学习
你能够用它来建立自动完成建议,工具提示,或者基本上任何浮动的东西。ui
官方文档对Overlay widget的定义是。this
一堆能够独立管理的条目。
叠加让独立的子widget经过插入到叠加的堆栈中,将视觉元素 "漂浮 "在其余widget之上。
这正是咱们要找的。当咱们建立MaterialApp时,它会自动建立一个Navigator,而Navigator又会建立一个Overlay
;一个Stack
widget,Navigator用它来管理视图的显示。
因此咱们来看看如何使用Overlay
来解决咱们的问题。
注意:本文关注的是显示浮动widget,所以不会过多地介绍实现typeahead(自动完成)字段的细节。若是你对一个编码良好、高度可定制的typeahead widget感兴趣,必定要看看个人包,flutter_typeahead。
让咱们从简单的形式开始。
Scaffold(
body: Padding(
padding: const EdgeInsets.all(50.0),
child: Form(
child: ListView(
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'Address'
),
),
SizedBox(height: 16.0,),
TextFormField(
decoration: InputDecoration(
labelText: 'City'
),
),
SizedBox(height: 16.0,),
TextFormField(
decoration: InputDecoration(
labelText: 'Address'
),
),
SizedBox(height: 16.0,),
RaisedButton(
child: Text('SUBMIT'),
onPressed: () {
// submit the form
},
)
],
),
),
),
)
复制代码
而后,咱们将国家字段抽象成本身的有状态widget,咱们称之为 CountriesField
。
class CountriesField extends StatefulWidget {
@override
_CountriesFieldState createState() => _CountriesFieldState();
}
class _CountriesFieldState extends State<CountriesField> {
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'Country'
),
);
}
}
复制代码
接下来咱们要作的是,每当字段接收到焦点时就显示一个浮动列表,每当焦点丢失时就隐藏该列表。你能够根据你的用例来改变这个逻辑。你可能想只在用户输入一些字符时才显示它,而在用户点击Enter时删除它。在全部状况下,让咱们来看看如何显示这个浮动widget。
class CountriesField extends StatefulWidget {
@override
_CountriesFieldState createState() => _CountriesFieldState();
}
class _CountriesFieldState extends State<CountriesField> {
final FocusNode _focusNode = FocusNode();
OverlayEntry _overlayEntry;
@override
void initState() {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
this._overlayEntry.remove();
}
});
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: offset.dy + size.height + 5.0,
width: size.width,
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: <Widget>[
ListTile(
title: Text('Syria'),
),
ListTile(
title: Text('Lebanon'),
)
],
),
),
)
);
}
@override
Widget build(BuildContext context) {
return TextFormField(
focusNode: this._focusNode,
decoration: InputDecoration(
labelText: 'Country'
),
);
}
}
复制代码
咱们为TextFormField
分配一个FocusNode,并在initState
中为其添加一个监听器。咱们将使用这个监听器来检测字段什么时候得到/失去焦点。
每当咱们接收到焦点 (_focusNode.hasFocus == true
),咱们就使用 _createOverlayEntry
建立一个 OverlayEntry
,并使用 Overlay.of(context).insert
将它插入到最近的 Overlay
widget 中。
每当咱们失去焦点 (_focusNode.hasFocus == false
),咱们就会使用 _overlayEntry.remove
删除咱们添加的覆盖条目。
_createOverlayEntry
使用context.findRenderObject
函数,查询咱们widget的渲染框。这个渲染框使咱们可以知道widget的位置、大小和其余渲染信息。这将帮助咱们之后知道在哪里放置咱们的浮动列表。
_createOverlayEntry
使用渲染框来获取widget的大小,它还使用renderBox.localToGlobal
来获取widget在屏幕中的坐标。咱们为localToGlobal
方法提供了Offset.zero
,这意味着咱们要在这个渲染框里面获取 (0,0) 坐标,并将其转换为屏幕上的对应坐标。
而后咱们建立一个OverlayEntry
,这是一个用于显示Overlay
中的widget的widget。
OverlayEntry的内容是一个Positioned
widget。请记住,Positioned
widgets只能插入Stack
中,但也请记住,Overlay确实是一个Stack
。
咱们设置Positioned
widget的坐标,咱们给它与TextField
相同的x坐标,相同的宽度,相同的y坐标,但为了避免覆盖TextField
,咱们将其向底部移动一点。
在Positioned
里面,咱们显示一个ListView
,里面有咱们想要的建议(我在例子中硬编码了几个条目)。请注意,我把全部的东西都放在一个Material
widget里面。这有两个缘由:由于Overlay
默认不包含Material
widget,而许多widget若是没有Material
祖先就没法显示,并且Material
widget提供了仰角属性,容许咱们给widget一个阴影,使它看起来好像真的是浮动的。
就是这样! 咱们的建议框如今漂浮在全部其余东西的上方了
在咱们离开以前,让咱们试着再学习一件事! 若是咱们的视图是能够滚动的,那么咱们可能会注意到一些东西。
建议框会跟着咱们滚动!
建议框会粘在屏幕上的位置上。在某些状况下,这多是咱们想要的,但在这种状况下,咱们不但愿这样,咱们但愿它跟随咱们的TextField
!
这里的关键是 "跟随 "这个词。Flutter为咱们提供了两个widget:CompositedTransformFollower和CompositedTransformTarget。简单的说,若是咱们把一个跟随者
和一个目标
连接起来,那么跟随者
就会跟随目标
,不管它走到哪里! 要连接一个跟随者
和一个目标
,咱们必须为它们提供相同的LayerLink。
所以,咱们将用CompositedTransformFollower
包装咱们的建议框,用CompositedTransformTarget
包装咱们的TextField
。而后,咱们将经过为它们提供相同的LayerLink
来连接它们。这将使建议框跟随TextField
走到哪里,就跟到哪里。
class CountriesField extends StatefulWidget {
@override
_CountriesFieldState createState() => _CountriesFieldState();
}
class _CountriesFieldState extends State<CountriesField> {
final FocusNode _focusNode = FocusNode();
OverlayEntry _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void initState() {
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
this._overlayEntry.remove();
}
});
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: <Widget>[
ListTile(
title: Text('Syria'),
onTap: () {
print('Syria Tapped');
},
),
ListTile(
title: Text('Lebanon'),
onTap: () {
print('Lebanon Tapped');
},
)
],
),
),
),
)
);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: TextFormField(
focusNode: this._focusNode,
decoration: InputDecoration(
labelText: 'Country'
),
),
);
}
}
复制代码
咱们在OverlayEntry
中用CompositedTransformFollower
包装了咱们的Material
widget,用CompositedTransformTarget
包装了TextFormField
。
咱们为跟随者
和目标
提供了同一个LayerLink
实例。这将致使跟随者
与目标
具备相同的坐标空间,使其有效地跟随它。
咱们从 Positioned widget
中删除了 top
和 left
属性。这些属性再也不须要了,由于在默认状况下,跟随者
将拥有与目标
相同的坐标。然而,咱们保留了Positioned
的width
属性,由于若是不对其进行约束,跟随者
每每会无限延伸。
咱们为CompositedTransformFollower
提供了一个偏移量,以禁止它覆盖TextField
(和以前同样)。
最后,咱们将showWhenUnlinked
设置为false
,当TextField
在屏幕上不可见时(好比当咱们滚动到底部太远时),隐藏OverlayEntry
。
就这样,咱们的OverlayEntry
如今跟随了咱们的TextField
!
重要提示:CompositedTransformFollower
仍是有点bug;即便当目标
再也不可见时,跟随者
从屏幕上隐藏起来,跟随者
仍是会响应点击事件。我已经向Flutter团队开了一个问题。
并将在问题解决后更新帖子。
Overlay
是一个强大的widget,它为咱们提供了一个方便的Stack
来放置咱们的浮动widget。我已经成功地使用它来建立flutter_typeahead,我相信你也能够将它用于各类用例。 我但愿这对你有用。让我知道你的想法
经过www.DeepL.com/Translator(免费版)翻译