赞
踩
本文是一篇翻译文章,翻译自
原文地址:Flutter: Using Overlay to display floating widgets
作者:AbdulRahman AlHamali
想象一下:你编写出的迷人表单页面
你把它发给产品经理,他看了一眼说:“我一定要完整的输入国家名称吗,当我输入文字时难道你就不能给我展示些建议吗?”,你想了想:“好吧,他是对的”,因此,你决定开发一个‘自动补全‘的’预先输入’功能,随便你怎么称呼它:一个文本展示框TextField,当用户输入文字的时候展示一些建议选项。开始工作了..你知道怎么拿到建议数据,你知道怎么写逻辑,你知道所有要做的事情..除了不知道怎么将建议选项浮动展示在widgets之上。
你想了想:打算重新设计代码结构,为了实现悬浮效果,决定将整个页面包装进一个Stack组件中,你需要准确的计算每个widget显示的位置,非常侵入性、必须要严谨、容易出错,并且直觉告诉你这么做可能是错误的,有其他的实现方式吗?
方案就是:你可以使用Flutter已经提供好的Stack
:Overlay
在这片文章中,我将会介绍如何使用Overlay,来创建悬浮在其他widget之上的widgets,并且并不需要重构你的整个页面。
你可以使用Overlay
来展示自动匹配的建议选项,小提示,或着基本上所有的浮动的东西。
Overlay
是什么?
官方文档这样定义
- A Stack of entries that can be managed independently.
- // 一个可以独立管理的Stack子类
- Overlays let independent child widgets “float” visual elements on top of other widgets by inserting them into the overlay’s Stack.
- // 通过将可独立管理的子节点widgets加入到`overlay`的栈中,Overlays可以将这些widgets浮动展示到显现的elements节点的顶部,
这看起来就是我们正在寻找的内容,当我们创建MaterialApp的时候,它会自动创建一个Navigator,Navigator
则又会创建一个Overlay
:一个navigator用来管理所展示的views视图的Stack
组件
接下来,让我们一起看看怎么使用Overlay
来解决我们的问题吧。
注意:这篇文章的核心是介绍如何显示浮动widgets,因此不会涉及太多如何实现自动补全文本输入框textfield的细节,如果你对一个编写良好、可高度自定义的自动补全widget感兴趣的话,可以来看我写的package 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
- },
- )
- ],
- ),
- ),
- ),
- )
*一个简单页面,包含了三个文本输入框:country、city、address。
然后我们就以country 文本输入框为例吧,将它封装成一个我们自己的statefull 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'
- ),
- );
- }
- }
接下来我们将要做的是,每次当选中文本输入框获取焦点Focus的时候,将一个浮动的list列表展示出来。当失去焦点Focus的时候,再将它隐藏起来,当然你可以按照自己需求来决定如何实现,你可能需要在用户输入了一些文字后展示它,或者当用户点击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添加了一个监听事件,通过监听事件来获取什么时候获得/失去焦点focus。(_focusNode.hasFocus == true)
的时候, 我们通过_createOverlayEntry
创建一个OverlayEntry
实例对象,然后通过使用Overlay.of(context).insert
,将它插入到最邻近的Overlay
widget中去。(_focusNode.hasFocus == false)
的时候,我们通过使用_overlayEntry.remove
来移除刚才添加的overlay实例。_createOverlayEntry
通过使用context.findRenderObject
来获取我们的widget所在的渲染对象RenderBox,渲染对象里包含位置position、大小size、和一些其他关于渲染的信息,有了这些信息能够帮助我们计算在哪里展示我们的悬浮列表。_createOverlayEntry
通过渲染信息来获取当前widget的大小,也可以使用renderBox.localToGlobal
来获取当前widget在屏幕上的坐标。我们将localToGlobal
设置为Offset.zero
这意味着我们将在渲染对象中使用(0,0)坐标,并且将他们转换为屏幕上相对应的坐标。Overlay
中的widget。OverlayEntry
的是一个Positionedwidget。请牢记Positioned
widgets 只能被插入到Stack
中,当然Overlay
其实也是一个Stack
。Positioned
widget的坐标,给它和TextField
相同的X
轴坐标,相同的宽度,相同的Y
轴坐标,当然为了不遮挡到TextField
,在底部进行了一些偏移。Positioned
内部,我们设置了一个展示建议选项的ListView
(里面默写了例子中的一些国家)。注意到我把所有的内容都包在了Material中,关于这样写有两个原因:Overlay
默认不包含Material
widget,并且很多widgets如果没有有Material
祖先节点的话不能展示,除此之外Material
还提供了elevation属性,可以让我们给widget设置隐形效果,看起来就像真正浮在上面一样。以上,我们的建议选择项可以浮在所有widget之上了!
彩蛋:跟随widget滑动!
在我们离开之前,让我们在多学喜一点吧!假如我们的页面是可以滚动,我们可能注意到如下现象:
建议选择列表固定在了屏幕上!在某些场景下你可能的确需要固定的,但是在当前场景中,我们不想要它固定,我们想要它跟随我们的TextField
一起滚动!
关键词滚动,Flutter给我们提供了两个widget:CompositedTransformFollower、CompositedTransformTarget,简单介绍就是,如果我们关联起一个follower
和一个target
,那么无论target
滚动到哪里,这个follower
将跟随它一起滚动!为了关联起一个follower
和一个target
,我们需要给他们设置相同的LayerLink.
因此我们需要将建议选择列表用CompositedTransformFollower
包起来,将TextField
用CompositedTransformTarget
包起来。然后我们将他们使用想用的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'
- ),
- ),
- );
- }
- }
Material
widget用CompositedTransformFollower
包裹进OverlayEntry
中,将TextFormField
包裹进CompositedTransformTarget
中。LayerLink
示例,来关联follower
和target
,这样follower
和target
将会在相同的坐标系中,高效的跟随target
而动。Positioned
widget中移除了top
和left
属性,因为默认follower
和target
有相同的坐标,因此不在需要。然而我们保留了width
属性,因为如果不设置的话,follower
将会无限的延伸。TextFormField
,我们给CompositedTransformFollower
设置了一个offset(和之前一样的原因)。showWhenUnlinked
属性设置为false
,当TextFormField
在屏幕上不可见时,用来隐藏OverlayEntry
(比如我们滑动出屏幕底部很远的时候)。经过这些操作,我们的OverlayEntry
现在可以跟随我们的TextField
一起滚动啦!
重要提示:CompositedTransformFollower
仍然有一点小bug,当target
不可见时,即使follower
已经从屏幕上隐藏了,这个follower
仍然会响应点击事件,我已经给Flutter Team提了issue,此issue已经关闭,标记为解决。
Overlay
是一个强大的widget,它给我们提供了一个简单易用的的用来展示浮动widget的的Stack
组件。我用它成功的创建了flutter_typeahead,我确定你也能在多样化的需求中使用到它。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。