发布时间:2018年9月2日
想象一下:你设计了你的迷人的表格。
你把它发给你的产品经理,他看了看说:"那我得把整个国家的名字都打进去?你就不能在我输入的时候给我看看建议吗?"然后你就想:"嗯,他说得对!"嗯,他是对的!" 所以你决定实现一个 "typeahead",一个 "自动完成 "或任何你想叫它的东西。一个文本字段,在用户输入时显示建议。你开始工作......你知道如何获得建议,你知道如何做逻辑,你什么都知道......除了如何让建议漂浮在其他widget之上。
你想一想,为了达到这个目的,你必须把整个屏幕重新设计成一个Stack,然后计算出每个widget必须显示的确切位置。这非常麻烦,非常严格,非常容易出错,而且感觉就是不对。但还有另一种方法。
你可以使用Flutter预先提供的Stack
,即 Overlay 。
在这篇文章中,我将解释如何使用Overlaywidget来创建浮在其他一切之上的widget,而不必重组你的整个视图。
你可以用它来创建自动完成建议,工具提示,或者基本上任何浮动的东西。
什么是Overlay widget?
官方文档对Overlay widget的定义是。
一堆可以独立管理的条目。
叠加让独立的子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(免费版)翻译