原文地址: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(免費版)翻譯