釋出時間: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(免費版)翻譯