一、Widget
Flutter設(shè)計思想,Everything is Widget。
Widget 是一個比較寬泛的概念,無論基本部件、布局、還是手勢等都是 Widget。
它是對視圖的一種包含配置及狀態(tài)信息的“描述數(shù)據(jù)”,用于約束具體的視圖元素
Widget樹
StatelessWidget
StatelessWidget一旦創(chuàng)建就無法進行修改,這意味著它不會因為外部條件變化而重新繪制。
其生命周期:
- 1、初始化
- 2、通過build()方法進行渲染。
StatefulWidget
它可以在其生命周期中操作內(nèi)部持有數(shù)據(jù)的變化,這些數(shù)據(jù)被稱為State,這樣的Widget也叫做StatefulWidget。
-
createState()
:StatefulWidget類中提供了新的接口,用于創(chuàng)建State;
例:
class RoutePageA extends StatefulWidget {
@override
RoutePageAState createState() => RoutePageAState();
}
class RoutePageAState extends State<RoutePageA> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Text("abc" );
}
}
State
一個StatefulWidget類會對應(yīng)一個State類,State表示與其對應(yīng)的StatefulWidget要維護的狀態(tài),State中的保存的狀態(tài)信息可以:
1、在widget 構(gòu)建時可以被同步讀取。
2、在widget生命周期中可以被改變,當State被改變時,可以手動調(diào)用其
setState()
方法通知Flutter framework狀態(tài)發(fā)生改變,F(xiàn)lutter framework在收到消息后,會重新調(diào)用其build方法重新構(gòu)建widget樹,從而達到更新UI的目的。
State中有兩個常用屬性:
1、widget,它表示與該State實例關(guān)聯(lián)的widget實例,由Flutter framework動態(tài)設(shè)置。注意,這種關(guān)聯(lián)并非永久的,因為在應(yīng)用生命周期中,UI樹上的某一個節(jié)點的widget實例在重新構(gòu)建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建,當在重新構(gòu)建時,如果widget被修改了,F(xiàn)lutter framework會動態(tài)設(shè)置State.widget為新的widget實例。
2、context, StatefulWidget對應(yīng)的BuildContext
StatefulWidget生命周期
State作為StatefulWidget的主體,它可以在多個節(jié)點對State進行調(diào)整。
其生命周期:
initState()
:當Widget第一次插入到Widget樹時會被調(diào)用,在生命周期中只被調(diào)用一次。重寫該方法主要完成一些初始化工作。注意:在該方法中context對象可以訪問但并不能拿來使用,因為此時state與context沒有建立關(guān)聯(lián)。didChangeDependencies()
:當State對象的依賴發(fā)生變化時會被調(diào)用;例如:在之前build() 中包含了一個InheritedWidget,然后在之后的build() 中InheritedWidget發(fā)生了變化,那么此時InheritedWidget的子widget的didChangeDependencies()回調(diào)都會被調(diào)用build()
:在didChangeDependencies()
、didUpdateWidget()
和setState()
之后執(zhí)行,主要用于構(gòu)建Widget子樹(類似RN中render()
方法)。reassemble()
:此回調(diào)專門為開發(fā)調(diào)試而提供,在熱重載時會被調(diào)用,此回調(diào)在Release模式下永遠不會被調(diào)用。didUpdateWidget()
:在widget重新構(gòu)建時,F(xiàn)lutter framework會調(diào)用Widget.canUpdate來檢測Widget樹中同一位置的新舊節(jié)點,然后決定是否需要更新,如果Widget.canUpdate返回true則會調(diào)用此回調(diào)。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被調(diào)用。deactivate()
:當State對象從樹中被移除時,會調(diào)用此回調(diào),在一些場景下,F(xiàn)lutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法。dispose()
:當State對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源
[圖片上傳失敗...(image-64ecb1-1629883919829)]
Widget構(gòu)造函數(shù)
例:
const WidgetPage({
Key? key,
required this.text,
this.backgroundColor: Colors.grey,
this.fontSize: 40,
this.child,
}) : super(key: key);
final String text;
final Color backgroundColor;
final double fontSize;
final Widget? child;
調(diào)用:
return WidgetPage(
text: "Widget Demo",
fontSize: 100,//非必要參數(shù)
);
widget的構(gòu)造函數(shù)應(yīng)使用命名參數(shù)
命名參數(shù)中必要的參數(shù)要加required
在繼承Widget時,第一個參數(shù)通常為Key,主要要的作用是決定是否在下一次build時復(fù)用舊的widget,決定的條件在canUpdate()方法中。
如果需要接收子widget,child或children參數(shù)通常放在最后
widget的屬性盡可能的被聲明為final,防止被意外改變
BuildContext
Context 僅僅是已創(chuàng)建的所有 Widget 樹結(jié)構(gòu)中某個 Widget 的位置引用。
BuildContext是Widget樹結(jié)構(gòu)中每個Widget的上下文環(huán)境,每個BuildContext都只屬于一個Widget。
如果Widget A和它的子Widget B,則Widget A的BuildContext是Widget B的BuildContext的父context。
context 提供了一些方法,比如沖當前widget開始向上遍歷widget樹以及按照類型查找widget的方法。
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context測試"),
),
body: Container(
child: Builder(builder: (context) {
// 在Widget樹中向上查找最近的父級`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此處實際上是Text("Context測試")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
在flutter中我們經(jīng)常會使用到這樣的代碼
//打開一個新的頁面
Navigator.of(context).push
//打開Scaffold的Drawer
Scaffold.of(context).openDrawer
//獲取display1樣式文字主題
Theme.of(context).textTheme.display1
of(context)實際上對context跨組件獲取數(shù)據(jù)的一個分裝。
需要注意的是,在 State 中 initState階段是無法跨組件拿數(shù)據(jù)的,只有在didChangeDependencies之后才可以使用這些方法。
二、基礎(chǔ)組件
flutter官方提供了一些組件,如下官網(wǎng)截圖。
文本
Text
Text
用于顯示簡單樣式文本
Text(
"文本",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
maxLines: 1,
textScaleFactor: 1.5,
style: TextStyle(
color: Colors.cyan,
fontSize: 20,
),
);
- textAlign:文本的對齊方式
- maxLines:文本顯示最大行數(shù)
- overflow:指定截斷方式
- textScaleFactor:代表文本相對于當前字體大小的縮放因子
- style:通過TextStyle設(shè)置文本樣式
TextSpan
如果我們需要對一個Text內(nèi)容的不同部分按照不同樣式顯示,這時可以使用TextSpan,它代表文本的一個"片斷"
其定義如下:
const TextSpan({
TextStyle style,
Sting text,
List<TextSpan> children,
GestureRecognizer recognizer,
});
例:
Text.rich(TextSpan(children: [
TextSpan(
text: "Hello",
style: TextStyle(
color: Colors.blue,
)),
TextSpan(
text: " World",
style: TextStyle(
color: Colors.red,
),
recognizer: _tapSpanText
..onTap = () {
print("SpanTextAction");
}),
]));
通過Text.rich
方法將TextSpan
添加到Text中,之所以可以這樣做,是因為Text其實就是RichText
的一個包裝,而RichText
是可以顯示多種樣式(富文本)的widget
- text:文本
- style:文本樣式
- recognizer:手勢
DefaultTextStyle
在Widget樹中,文本的樣式默認是可以被繼承的,因此,如果在Widget樹的某一個節(jié)點處設(shè)置一個默認的文本樣式,那么該節(jié)點的子樹中所有文本都會默認使用這個樣式。
例:
DefaultTextStyle(
//1.設(shè)置文本默認樣式
style: TextStyle(
color: Colors.red,
fontSize: 20.0,
),
child: Column(
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
Text(
"I am Jack",
style: TextStyle(
inherit: false, //2.不繼承默認樣式
color: Colors.grey),
),
],
),
);
-
DefaultTextStyle
通過style
設(shè)置了默認樣式,其子Text都繼承其樣式 - 例中最后一個Text使用
inherit: false
設(shè)置不繼承默認樣式
按鈕
Material組件庫中的按鈕
TextButton
簡單的扁平按鈕,按下后,會有背景色
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
child: Text("TextButton"),
onPressed: () => print("TextButton"),
)
-
onPressed: null
會自動顯示不可點擊樣式
IconButton
可點擊的Icon,不包括文字,默認沒有背景,點擊后會出現(xiàn)背景
IconButton(
onPressed: () => print("IconButton"),
icon: Icon(Icons.thumb_up),
);
FloatingActionButton
懸浮按鈕,圓形、有陰影
FloatingActionButton(
child: Text("漂"),
tooltip: "點我",
onPressed: () => print("FloatingActionButton"));
}
ElevatedButton
漂浮按鈕,它默認帶有陰影和灰色背景
Column(
children: [
ElevatedButton(
child: Text("漂"), onPressed: () => print("ElevatedButton")),
ElevatedButton(child: Text("漂"), onPressed: null),
],
);
-
onPressed: null
會自動顯示不可點擊樣式
OutlineButton
默認有一個邊框,不帶陰影且背景透明。按下后,邊框顏色會變亮、同時出現(xiàn)背景和陰影(較弱)
OutlinedButton(
onPressed: () => print("OutlinedButton"),
child: Text("OutlinedButton")
);
圖片
Flutter中,我們可以通過Image組件來加載并顯示圖片,Image的數(shù)據(jù)源可以是asset、文件、內(nèi)存以及網(wǎng)絡(luò)。
ImageProvider
ImageProvider
是一個抽象類,主要定義了圖片數(shù)據(jù)獲取的接口load(),從不同的數(shù)據(jù)源獲取圖片需要實現(xiàn)不同的ImageProvider
,如AssetImage
是實現(xiàn)了從Asset
中加載圖片的ImageProvider
,而NetworkImage
實現(xiàn)了從網(wǎng)絡(luò)加載圖片的ImageProvider
。
Image
Image
widget有一個必選的image
參數(shù),它對應(yīng)一個ImageProvider
。下面我們分別演示一下如何從asset和網(wǎng)絡(luò)加載圖片
在asset中加載圖片
1、在工程根目錄下創(chuàng)建一個images目錄,并將圖片avatar.png拷貝到該目錄。
2、在pubspec.yaml中的flutter部分添加如下內(nèi)容:
assets:
- images/avatar.png
- 3、加載圖片
Image(
image: AssetImage("images/avatar.png"),
);
在網(wǎng)絡(luò)中加載圖片
Image(
image: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
)
Image也提供了一個快捷的構(gòu)造函數(shù)Image.network用于從網(wǎng)絡(luò)加載、顯示圖片:
Image.network(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
)
參數(shù):
-
image
: 它對應(yīng)一個ImageProvider
-
width、height
:設(shè)置圖片寬高 -
fit
:圖片顯示適應(yīng)模式,在BoxFit
中定義
fill:會拉伸填充滿顯示空間,圖片本身長寬比會發(fā)生變化,圖片會變形。
cover:會按圖片的長寬比放大后居中填滿顯示空間,圖片不會變形,超出顯示空間部分會被剪裁。
contain:這是圖片的默認適應(yīng)規(guī)則,圖片會在保證圖片本身長寬比不變的情況下縮放以適應(yīng)當前顯示空間,圖片不會變形。
fitWidth:圖片的寬度會縮放到顯示空間的寬度,高度會按比例縮放,然后居中顯示,圖片不會變形,超出顯示空間部分會被剪裁。
fitHeight:圖片的高度會縮放到顯示空間的高度,寬度會按比例縮放,然后居中顯示,圖片不會變形,超出顯示空間部分會被剪裁。
none:圖片沒有適應(yīng)策略,會在顯示空間內(nèi)顯示圖片,如果圖片比顯示空間大,則顯示空間只會顯示圖片中間部分。
-
color
和colorBlendMode
:在圖片繪制時可以對每一個像素進行顏色混合處理,color指定混合色,而colorBlendMode指定混合模式 -
repeat
:重復(fù)規(guī)則
輸入框
Material組件庫中提供了輸入框組件TextField和表單組件Form。
TextField
TextField用于文本輸入,它提供了很多屬性,如下:
const TextField({
...
TextEditingController controller,
FocusNode focusNode,
InputDecoration decoration = const InputDecoration(),
TextInputType keyboardType,
TextInputAction textInputAction,
TextStyle style,
TextAlign textAlign = TextAlign.start,
bool autofocus = false,
bool obscureText = false,
int maxLines = 1,
int maxLength,
bool maxLengthEnforced = true,
ValueChanged<String> onChanged,
VoidCallback onEditingComplete,
ValueChanged<String> onSubmitted,
List<TextInputFormatter> inputFormatters,
bool enabled,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
...
})
controller
:編輯框的控制器,通過它可以設(shè)置/獲取編輯框的內(nèi)容、選擇編輯內(nèi)容、監(jiān)聽編輯文本改變事件。大多數(shù)情況下我們都需要顯式提供一個controller來與文本框交互。如果沒有提供controller,則TextField內(nèi)部會自動創(chuàng)建一個。focusNode
:用于控制TextField是否占有當前鍵盤的輸入焦點。它是我們和鍵盤交互的一個句柄(handle)。InputDecoration
:用于控制TextField的外觀顯示,如提示文本、背景顏色、邊框等。keyboardType
:用于設(shè)置該輸入框默認的鍵盤輸入類型
First Header | Second Header |
---|---|
TextInputType枚舉值 | 含義 |
text | 文本輸入鍵盤 |
multiline | 多行文本,需和maxLines配合使用(設(shè)為null或大于1) |
number | 數(shù)字;會彈出數(shù)字鍵盤 |
phone | 優(yōu)化后的電話號碼輸入鍵盤;會彈出數(shù)字鍵盤并顯示“* #” |
datetime | 優(yōu)化后的日期輸入鍵盤;Android上會顯示“: -” |
emailAddress | 優(yōu)化后的電子郵件地址;會顯示“@ .” |
url | 優(yōu)化后的url輸入鍵盤; 會顯示“/ .” |
textInputAction
:鍵盤動作按鈕圖標(即回車鍵位圖標),它是一個枚舉值,有多個可選值,全部的取值列表讀者可以查看API文檔,如:TextInputAction.search。style
:正在編輯的文本樣式。textAlign
: 輸入框內(nèi)編輯文本在水平方向的對齊方式。autofocus
: 是否自動獲取焦點。obscureText
:是否隱藏正在編輯的文本,如用于輸入密碼的場景等,文本內(nèi)容會用“?”替換。maxLines
:輸入框的最大行數(shù),默認為1;如果為null,則無行數(shù)限制。maxLength和maxLengthEnforced
:maxLength代表輸入框文本的最大長度,設(shè)置后輸入框右下角會顯示輸入的文本計數(shù)。maxLengthEnforced決定當輸入文本長度超過maxLength時是否阻止輸入,為true時會阻止輸入,為false時不會阻止輸入但輸入框會變紅。onChange
:輸入框內(nèi)容改變時的回調(diào)函數(shù);注:內(nèi)容改變事件也可以通過controller來監(jiān)聽。onEditingComplete和onSubmitted
:這兩個回調(diào)都是在輸入框輸入完成時觸發(fā),比如按了鍵盤的完成鍵(對號圖標)或搜索鍵(??圖標)。不同的是兩個回調(diào)簽名不同,onSubmitted回調(diào)是ValueChanged<String>類型,它接收當前輸入內(nèi)容做為參數(shù),而onEditingComplete不接收參數(shù)。inputFormatters
:用于指定輸入格式;當用戶輸入內(nèi)容改變時,會根據(jù)指定的格式來校驗。enable
:如果為false,則輸入框會被禁用,禁用狀態(tài)不接收輸入和事件,同時顯示禁用態(tài)樣式(在其decoration中定義)。cursorWidth、cursorRadius和cursorColor
:這三個屬性是用于自定義輸入框光標寬度、圓角和顏色的。
自定義樣式
可以通過decoration屬性來定義輸入框樣式
decoration: InputDecoration(
hintText: "請輸入用戶名",//提示文字
prefixIcon: Icon(Icons.lock),//左側(cè)圖標
//邊框樣式
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.grey,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(
color: Colors.red,
),
),
),
獲取&監(jiān)聽文本變化
獲取輸入內(nèi)容有兩種方式:
- 1、通過
onChange
獲取
onChanged: (value) => print(value),
- 2、通過
controller
獲取
定義一個controller:
TextEditingController _unameController = TextEditingController();
然后設(shè)置輸入框controller:
TextField(
autofocus: true,
controller: _unameController, //設(shè)置controller
...
)
通過controller獲取輸入框內(nèi)容
@override
void initState() {
//監(jiān)聽輸入改變
_unameController.addListener((){
print(_unameController.text);
});
}
兩種方式相比,onChanged是專門用于監(jiān)聽文本變化,而controller的功能卻多一些,除了能監(jiān)聽文本變化外,它還可以設(shè)置默認值、選擇文本等
監(jiān)聽焦點狀態(tài)改變事件:
...
// 創(chuàng)建 focusNode
FocusNode focusNode = new FocusNode();
...
// focusNode綁定輸入框
TextField(focusNode: focusNode);
...
// 監(jiān)聽焦點變化
focusNode.addListener((){
print(focusNode.hasFocus);
});
此章:Icon、表單Form、進度指示器ProgressIndicator 自己看一下吧
三、布局方式
布局類組件簡介
布局類組件都會包含一個或多個子組件,不同的布局類組件對子組件排版(layout)方式不同。
根據(jù)Widget是否包含子節(jié)點將其分為3類
1、
LeafRenderObjectWidget
:Widget樹的葉子節(jié)點,用于沒有子節(jié)點的widget,通常基礎(chǔ)組件都屬于這一類,如Image。2、
SingleChildRenderObjectWidget
:包含一個子Widget,如:ConstrainedBox、DecoratedBox等3、
MultiChildRenderObjectWidget
:包含多個子Widget,一般都有一個children參數(shù),接受一個Widget數(shù)組。如Row、Column、Stack等
布局類組件就是指直接或間接繼承(包含)MultiChildRenderObjectWidget
的Widget
線性布局(Row和Column)
所謂線性布局,即指沿水平或垂直方向排布子組件。Flutter中通過Row和Column來實現(xiàn)線性布局,類似于Android中的LinearLayout控件。Row和Column都繼承自Flex,我們將在彈性布局一節(jié)中詳細介紹Flex。
主軸和縱軸
對于線性布局,有主軸和縱軸之分,如果布局是沿水平方向,那么主軸就是指水平方向,而縱軸即垂直方向;如果布局沿垂直方向,那么主軸就是指垂直方向,而縱軸就是水平方向
Row
Row可以在水平方向排列其子widget
Row({
...
TextDirection textDirection,
MainAxisSize mainAxisSize = MainAxisSize.max,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
VerticalDirection verticalDirection = VerticalDirection.down,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
List<Widget> children = const <Widget>[],
})
children
:子組件數(shù)組。textDirection
:表示水平方向子組件的布局順序(是從左往右還是從右往左),默認為從左往右,而阿拉伯語是從右往左。mainAxisSize
:表示Row在主軸(水平)方向占用的空間,默認是MainAxisSize.max,表示盡可能多的占用水平方向的空間;而MainAxisSize.min表示盡可能少的占用水平空間;mainAxisAlignment
:表示子組件在Row所占用的水平空間內(nèi)對齊方式(包含start、end、 center三個值),如果mainAxisSize值為MainAxisSize.min,則此屬性無意義。默認start。注意:textDirection是mainAxisAlignment的參考系。verticalDirection
:表示Row縱軸(垂直)的對齊方向,默認是VerticalDirection.down,表示從上到下。crossAxisAlignment
:表示子組件在縱軸方向的對齊方式,Row的高度等于子組件中最高的子元素高度,它的取值和MainAxisAlignment一樣(包含start、end、 center三個值),不同的是crossAxisAlignment的參考系是verticalDirection;
Column
Column可以在垂直方向排列其子widget
參數(shù)和Row一樣,不同的是布局方向為垂直,不同的是布局方向為垂直,主軸縱軸正好相反。
特殊情況
如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column會占用盡可能大的空間,里面Row或Column所占用的空間為實際大小,若想內(nèi)部占用盡可能空間可以使用Expanded
組件。
彈性布局(Flex)
彈性布局允許子組件按照一定比例來分配父容器空間。彈性布局的概念在其它UI系統(tǒng)中也都存在,如H5中的彈性盒子布局,Android中的FlexboxLayout等。Flutter中的彈性布局主要通過Flex
和Expanded
來配合實現(xiàn)。
Flex
Flex組件可以沿著水平或垂直方向排列子組件,如果你知道主軸方向,使用Row或Column會方便一些,因為Row和Column都繼承自Flex,參數(shù)基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。
Flex({
...
@required this.direction, //彈性布局的方向, Row默認為水平方向,Column默認為垂直方向
List<Widget> children = const <Widget>[],
})
-
direction
:Axis.vertical、Axis.horizontal
Expanded
可以按比例“擴伸” Row、Column和Flex子組件所占用的空間。
const Expanded({
int flex = 1,
@required Widget child,
})
flex
參數(shù)為彈性系數(shù),如果為0或null,則child是沒有彈性的,即不會被擴伸占用的空間。如果大于0,所有的Expanded
按照其flex
的比例來分割主軸的全部空閑空間。
Spacer
Spacer
的功能是占用指定比例的空間,實際上它只是Expanded的一個包裝類
流式布局
在介紹Row和Colum時,如果子widget超出屏幕范圍,則會報溢出錯誤
我們把超出屏幕顯示范圍會自動折行的布局稱為流式布局。Flutter中通過Wrap和Flow來支持流式布局。
Wrap
我們可以看到Wrap的很多屬性在Row(包括Flex和Column)中也有,如direction、crossAxisAlignment、textDirection、verticalDirection等,這些參數(shù)意義是相同的。Wrap和Flex(包括Row和Column)除了超出顯示范圍后Wrap會折行外,其它行為基本相同。下面我們看一下Wrap特有的幾個屬性:
spacing
:主軸方向子widget的間距runSpacing
:縱軸方向的間距alignment
: 主軸方向的對齊方式runAlignment
:縱軸方向的對齊方式
Wrap({
...
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 0.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
List<Widget> children = const <Widget>[],
})
Flow
我們一般很少會使用Flow,因為其過于復(fù)雜,需要自己實現(xiàn)子widget的位置轉(zhuǎn)換,在很多場景下首先要考慮的是Wrap是否滿足需求。
性能好
靈活
使用復(fù)雜
不能自適應(yīng)子組件大小,必須通過指定父容器大小或?qū)崿F(xiàn)TestFlowDelegate的getSize返回固定大小
總結(jié):復(fù)雜,先知道有這么個布局方式
層疊布局Stack、Positioned
層疊布局和Web中的絕對定位、Android中的Frame布局是相似的,子組件可以根據(jù)距父容器四個角的位置來確定自身的位置。絕對定位允許子組件堆疊起來(按照代碼中聲明的順序)
Flutter中使用Stack
和Positioned
這兩個組件來配合實現(xiàn)絕對定位。Stack允許子組件堆疊,而Positioned用于根據(jù)Stack的四個角來確定子組件的位置。
Stack
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
textDirection
:和Row、Wrap的textDirection功能一樣,都用于確定alignment對齊的參考系。alignment
:此參數(shù)決定如何去對齊沒有定位(沒有使用Positioned)或部分定位的子組件。所謂部分定位,在這里特指沒有在某一個軸上定位:left、right為橫軸,top、bottom為縱軸,只要包含某個軸上的一個定位屬性就算在該軸上有定位。fit
:此參數(shù)用于確定沒有定位的子組件如何去適應(yīng)Stack的大小。StackFit.loose表示使用子組件的大小,StackFit.expand表示擴伸到Stack的大小。
Positioned
const Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
@required Widget child,
})
left、top 、right、 bottom分別代表離Stack左、上、右、底四邊的距離
width、height 表示寬高
這里和約束布局一樣,如確定left、top、height、width或確定left、top 、right、 bottom及可確定一個組件的位置和大小
Stack子元素是堆疊的,所以后添加的widget在最上層
對齊與相對定位
Align
Align 組件可以調(diào)整子組件的位置,并且可以根據(jù)子組件的寬高來確定自身的的寬高,定義如下:
Align({
Key key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget child,
})
alignment : 需要一個
AlignmentGeometry
類型的值,表示子組件在父組件中的起始位置,AlignmentGeometry
是一個抽象類,它有兩個常用的子類:Alignment
和FractionalOffset
。widthFactor
和heightFactor
是用于確定Align 組件本身寬高的屬性;它們是兩個縮放因子,會分別乘以子元素的寬、高,最終的結(jié)果就是Align 組件的寬高。如果值為null,則組件的寬高將會占用盡可能多的空間。
Alignment
Alignment繼承自AlignmentGeometry,表示矩形內(nèi)的一個點,他有兩個屬性x、y,分別表示在水平和垂直方向的偏移,Alignment定義如下:
Alignment(this.x, this.y)
Alignment Widget會以矩形的中心點作為坐標原點,即Alignment(0.0, 0.0)
x、y的值從-1到1分別代表矩形左邊到右邊的距離和頂部到底邊的距離
Alignment(-1.0, -1.0) 代表矩形的左側(cè)頂點,而Alignment(1.0, 1.0)代表右側(cè)底部終點,即Alignment.topRight
Alignment可以通過其坐標轉(zhuǎn)換公式將其坐標轉(zhuǎn)為子元素的具體偏移坐標。
(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)
其中childWidth為子元素的寬度,childHeight為子元素高度。
FractionalOffset
FractionalOffset 繼承自 Alignment,它和 Alignment唯一的區(qū)別就是坐標原點不同!FractionalOffset 的坐標原點為矩形的左側(cè)頂點,這和布局系統(tǒng)的一致,所以理解起來會比較容易。FractionalOffset的坐標轉(zhuǎn)換公式為:
實際偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)
Align和Stack對比
Align和Stack/Positioned都可以用于指定子元素相對于父元素的偏移,但它們還是有兩個主要區(qū)別:
1、定位參考系統(tǒng)不同;Stack/Positioned定位的的參考系可以是父容器矩形的四個頂點;而Align則需要先通過alignment 參數(shù)來確定坐標原點,不同的alignment會對應(yīng)不同原點,最終的偏移是需要通過alignment的轉(zhuǎn)換公式來計算出。
2、Stack可以有多個子元素,并且子元素可以堆疊,而Align只能有一個子元素,不存在堆疊。
Center
Center
組件其實是對齊方式確定(Alignment.center)了的Align
四、容器類組件
容器類Widget和布局類Widget都作用于其子Widget,不同的是:
布局類Widget一般都需要接收一個widget數(shù)組(children),他們直接或間接繼承自(或包含)MultiChildRenderObjectWidget ;而容器類Widget一般只需要接收一個子Widget(child),他們直接或間接繼承自(或包含)SingleChildRenderObjectWidget。
布局類Widget是按照一定的排列方式來對其子Widget進行排列;而容器類Widget一般只是包裝其子Widget,對其添加一些修飾(補白或背景色等)、變換(旋轉(zhuǎn)或剪裁等)、或限制(大小等)。
填充(Padding)
Padding可以給其子節(jié)點添加填充(留白),和邊距效果類似。我們在前面很多示例中都已經(jīng)使用過它了,現(xiàn)在來看看它的定義:
Padding({
...
EdgeInsetsGeometry padding,
Widget child,
})
EdgeInsetsGeometry
是一個抽象類,開發(fā)中,我們一般都使用EdgeInsets
類,它是EdgeInsetsGeometry的一個子類,定義了一些設(shè)置填充的便捷方法。
EdgeInsets
我們看看EdgeInsets提供的便捷方法:
fromLTRB(double left, double top, double right, double bottom):分別指定四個方向的填充。
all(double value) : 所有方向均使用相同數(shù)值的填充。
only({left, top, right ,bottom }):可以設(shè)置具體某個方向的填充(可以同時指定多個方向)。
symmetric({ vertical, horizontal }):用于設(shè)置對稱方向的填充,vertical指top和bottom,horizontal指left和right。
尺寸限制類容器
尺寸限制類容器用于限制容器大小,F(xiàn)lutter中提供了多種這樣的容器,如ConstrainedBox
、SizedBox
、UnconstrainedBox
、AspectRatio
等,本節(jié)將介紹一些常用的。
ConstrainedBox
ConstrainedBox用于對子組件添加額外的約束。例如,如果你想讓子組件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)作為子組件的約束。
實現(xiàn)一個最小高度為50,寬度盡可能大的容器
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity, //寬度盡可能大
minHeight: 50.0 //最小高度為50像素
),
child: Container(
height: 5.0,
color: Colors.red,
),
)
BoxConstraints用于設(shè)置限制條件,它的定義如下:
const BoxConstraints({
this.minWidth = 0.0, //最小寬度
this.maxWidth = double.infinity, //最大寬度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
BoxConstraints還定義了一些便捷的構(gòu)造函數(shù),用于快速生成特定限制規(guī)則的BoxConstraints,如BoxConstraints.tight(Size size)
,它可以生成給定大小的限制;const BoxConstraints.expand()
可以生成一個盡可能大的用以填充另一個容器的BoxConstraints。除此之外還有一些其它的便捷函數(shù),讀者可以查看API文檔 (opens new window)。
SizedBox
SizedBox用于給子元素指定固定的寬高,如:
SizedBox(
width: 80.0,
height: 80.0,
child: redBox
)
實際上SizedBox只是ConstrainedBox的一個定制,上面代碼等價于:
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: redBox,
)
而BoxConstraints.tightFor(width: 80.0,height: 80.0)等價于:
BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)
多重限制
如果某一個組件有多個父級ConstrainedBox限制,那么最終會是哪個生效?我們看一個例子:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
)
)
最終顯示效果是寬90,高60,也就是說是子ConstrainedBox的minWidth生效,而minHeight是父ConstrainedBox生效。單憑這個例子,我們還總結(jié)不出什么規(guī)律,我們將上例中父子限制條件換一下:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
最終的顯示效果仍然是90,高60
結(jié)論:有多重限制時,對于minWidth和minHeight來說,是取父子中相應(yīng)數(shù)值較大的。
思考題:對于maxWidth和maxHeight,多重限制的策略是什么樣的呢?
結(jié)論:有多重限制時,對于maxWidth和maxHeight來說,是取父子中相應(yīng)數(shù)值較小的。
UnconstrainedBox
UnconstrainedBox不會對子組件產(chǎn)生任何限制,它允許其子組件按照其本身大小繪制。一般情況下,我們會很少直接使用此組件,但在"去除"多重限制的時候也許會有幫助,我們看下下面的代碼:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
child: UnconstrainedBox( //“去除”父級限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
),
)
)
上面代碼中,如果沒有中間的UnconstrainedBox,那么根據(jù)上面所述的多重限制規(guī)則,那么最終將顯示一個90×100的紅色框。但是由于UnconstrainedBox “去除”了父ConstrainedBox的限制,則最終會按照子ConstrainedBox的限制來繪制redBox,即90×20
裝飾容器DecoratedBox
DecoratedBox可以在其子組件繪制前(或后)繪制一些裝飾(Decoration),如背景、邊框、漸變等。DecoratedBox定義如下:
const DecoratedBox({
Decoration decoration,
DecorationPosition position = DecorationPosition.background,
Widget child
})
decoration:代表將要繪制的裝飾,它的類型為Decoration。Decoration是一個抽象類,它定義了一個接口 createBoxPainter(),子類的主要職責(zé)是需要通過實現(xiàn)它來創(chuàng)建一個畫筆,該畫筆用于繪制裝飾。
-
position:此屬性決定在哪里繪制Decoration,它接收DecorationPosition的枚舉類型,該枚舉類有兩個值:
1、 background:在子組件之后繪制,即背景裝飾
2、foreground:在子組件之上繪制,即前景。
BoxDecoration
我們通常會直接使用BoxDecoration類,它是一個Decoration的子類,實現(xiàn)了常用的裝飾元素的繪制。
BoxDecoration({
Color color, //顏色
DecorationImage image,//圖片
BoxBorder border, //邊框
BorderRadiusGeometry borderRadius, //圓角
List<BoxShadow> boxShadow, //陰影,可以指定多個
Gradient gradient, //漸變
BlendMode backgroundBlendMode, //背景混合模式
BoxShape shape = BoxShape.rectangle, //形狀
})
變換(Transform)
Transform可以在其子組件繪制時對其應(yīng)用一些矩陣變換來實現(xiàn)一些特效。
平移
Transform.translate接收一個offset參數(shù),可以在繪制時沿x、y軸對子組件平移指定的距離。
Widget _createTransform_translate() {
return Center(
child: Transform.translate(
offset: Offset(10, 100),
child: Text(
"哈哈哈哈哈哈",
style: TextStyle(
backgroundColor: Colors.grey,
),
),
),
);
}
旋轉(zhuǎn)
Transform.rotate可以對子組件進行旋轉(zhuǎn)變換,如:
Widget _createTransform_rotate() {
return Center(
child: Transform.rotate(
angle: pi / 2,
child: Text(
"哈哈哈哈哈哈",
style: TextStyle(
backgroundColor: Colors.grey,
),
),
),
);
}
縮放
Transform.scale可以對子組件進行縮小或放大,如:
Widget _createTransform_scale() {
return Row(
children: [
Text(
"哈哈哈哈哈哈",
style: TextStyle(
backgroundColor: Colors.grey,
),
),
Transform.scale(
scale: 2,
child: Text(
"嘿嘿嘿嘿嘿",
style: TextStyle(
backgroundColor: Colors.red,
),
),
),
],
);
}
由于使用transform只會在繪制時放大,其占用空間依然為原來部分,所以會出現(xiàn)重合現(xiàn)象。
由于矩陣變化只會作用在繪制階段,所以在某些場景下,在UI需要變化時,可以直接通過矩陣變化來達到視覺上的UI改變,而不需要去重新觸發(fā)build流程,這樣會節(jié)省layout的開銷,所以性能會比較好。如之前介紹的Flow組件,它內(nèi)部就是用矩陣變換來更新UI,除此之外,F(xiàn)lutter的動畫組件中也大量使用了Transform以提高性能。
RotatedBox
RotatedBox和Transform.rotate功能相似,它們都可以對子組件進行旋轉(zhuǎn)變換,但是有一點不同:RotatedBox的變換是在layout階段,會影響在子組件的位置和大小。
Widget _createRotatedBox() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 100),
child: Row(
children: [
Text(
"哈哈哈哈哈哈",
style: TextStyle(
backgroundColor: Colors.grey,
),
),
RotatedBox(
quarterTurns: 1,
child: Text(
"嘿嘿嘿嘿嘿",
style: TextStyle(
backgroundColor: Colors.red,
),
),
),
],
),
);
}
Container
Container是一個組合類容器,它本身不對應(yīng)具體的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等組件組合的一個多功能容器,所以我們只需通過一個Container組件可以實現(xiàn)同時需要裝飾、變換、限制的場景。下面是Container的定義:
Container({
this.alignment,
this.padding, //容器內(nèi)補白,屬于decoration的裝飾范圍
Color color, // 背景色
Decoration decoration, // 背景裝飾
Decoration foregroundDecoration, //前景裝飾
double width,//容器的寬度
double height, //容器的高度
BoxConstraints constraints, //容器大小的限制條件
this.margin,//容器外補白,不屬于decoration的裝飾范圍
this.transform, //變換
this.child,
})
容器的大小可以通過width、height屬性來指定,也可以通過constraints來指定;如果它們同時存在時,width、height優(yōu)先。實際上Container內(nèi)部會根據(jù)width、height來生成一個constraints。
color和decoration是互斥的,如果同時設(shè)置它們則會報錯!實際上,當指定color時,Container內(nèi)會自動創(chuàng)建一個decoration。
Padding和Margin
接下來我們來研究一下Container組件margin和padding屬性的區(qū)別:
Container(
margin: EdgeInsets.all(20.0), //容器外補白
color: Colors.orange,
child: Text("Hello world!"),
),
Container(
padding: EdgeInsets.all(20.0), //容器內(nèi)補白
color: Colors.orange,
child: Text("Hello world!"),
),
總結(jié):margin在容器外留白,padding是容器內(nèi)留白
事實上,Container內(nèi)margin和padding都是通過Padding 組件來實現(xiàn)的,上面的示例代碼實際上等價于:
...
Padding(
padding: EdgeInsets.all(20.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Text("Hello world!"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Hello world!"),
),
),
Scaffold
一個完整的路由頁可能會包含導(dǎo)航欄、抽屜菜單(Drawer)以及底部Tab導(dǎo)航菜單等。如果每個路由頁面都需要開發(fā)者自己手動去實現(xiàn)這些,這會是一件非常麻煩且無聊的事。幸運的是,F(xiàn)lutter Material組件庫提供了一些現(xiàn)成的組件來減少我們的開發(fā)任務(wù)。Scaffold是一個路由頁的骨架,我們使用它可以很容易地拼裝出一個完整的頁面。
它主要包括:
- appBar:導(dǎo)航欄
- drawer:抽屜
- bottomNavigationBar:底部導(dǎo)航
- floatingActionButton:懸浮按鈕
- body
AppBar
AppBar是一個Material風(fēng)格的導(dǎo)航欄,通過它可以設(shè)置導(dǎo)航欄標題、導(dǎo)航欄菜單、導(dǎo)航欄底部的Tab標題等。下面我們看看AppBar的定義:
AppBar({
Key key,
this.leading, //導(dǎo)航欄最左側(cè)Widget,常見為抽屜菜單按鈕或返回按鈕。
this.automaticallyImplyLeading = true, //如果leading為null,是否自動實現(xiàn)默認的leading按鈕
this.title,// 頁面標題
this.actions, // 導(dǎo)航欄右側(cè)菜單
this.bottom, // 導(dǎo)航欄底部菜單,通常為Tab按鈕組
this.elevation = 4.0, // 導(dǎo)航欄陰影
this.centerTitle, //標題是否居中
this.backgroundColor,
... //其它屬性見源碼注釋
})
抽屜菜單Drawer
Scaffold的drawe
r和endDrawer
屬性可以分別接受一個Widget來作為頁面的左、右抽屜菜單。如果開發(fā)者提供了抽屜菜單,那么當用戶手指從屏幕左(或右)側(cè)向里滑動時便可打開抽屜菜單。
return Scaffold(
appBar: createAppBar(),
endDrawer: Drawer(
child: Container(
color: Colors.red,
),
),
);
底部Tab導(dǎo)航欄
我們可以通過Scaffold的bottomNavigationBar屬性來設(shè)置底部導(dǎo)航,如本節(jié)開始示例所示,我們通過Material組件庫提供的BottomNavigationBar和BottomNavigationBarItem兩種組件來實現(xiàn)Material風(fēng)格的底部導(dǎo)航欄。
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "首頁",
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: "歷史",
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: "搜索",
),
],
currentIndex: selectedIndex,
onTap: _itemTapAction,
),
實現(xiàn)中間按鈕浮動效果
floatingActionButton: FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 底部導(dǎo)航欄打一個圓形的洞
child: Row(
children: [
IconButton(
onPressed: null,
icon: Icon(Icons.home),
),
SizedBox(), //中間位置空出
IconButton(
onPressed: null,
icon: Icon(Icons.business),
),
],
mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部導(dǎo)航欄橫向空間
),
),
剪裁(Clip)
Flutter中提供了一些剪裁函數(shù),用于對組件進行剪裁。
ClipOval
:子組件為正方形時剪裁為內(nèi)貼圓形,為矩形時,剪裁為內(nèi)貼橢圓ClipRRect
:將子組件剪裁為圓角矩形ClipRect
:剪裁子組件到實際占用的矩形大小(溢出部分剪裁)
Widget _createClip() {
const netWorkImage = NetworkImage(
"https://book.flutterchina.club/assets/img/3-17.a063365a.png");
return Column(
children: [
Padding(
padding: EdgeInsets.all(10.0),
//裁剪為圓形
child: ClipOval(
child: SizedBox(
height: 100,
width: 100,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
),
),
),
),
Padding(
padding: EdgeInsets.all(10.0),
//裁剪為圓角
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: SizedBox(
height: 100,
width: 100,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
),
),
),
),
Padding(
padding: EdgeInsets.all(10.0),
//裁剪為圓角
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ClipRect(
//將溢出部分剪裁
child: Align(
alignment: Alignment.topLeft,
widthFactor: .5, //寬度設(shè)為原來寬度一半
child: Image(
image: netWorkImage,
),
),
),
Text(
"你好世界",
style: TextStyle(color: Colors.green),
)
],
),
),
],
);
}
CustomClipper
如果我們想剪裁子組件的特定區(qū)域,如果我們只想截取圖片中部40×30像素的范圍應(yīng)該怎么做?這時我們可以使用CustomClipper來自定義剪裁區(qū)域,實現(xiàn)代碼如下:
首先,自定義一個CustomClipper:
class MyClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
getClip()
是用于獲取剪裁區(qū)域的接口,由于圖片大小是60×60,我們返回剪裁區(qū)域為Rect.fromLTWH(10.0, 15.0, 40.0, 30.0),即圖片中部40×30像素的范圍。shouldReclip()
接口決定是否重新剪裁。如果在應(yīng)用中,剪裁區(qū)域始終不會發(fā)生變化時應(yīng)該返回false,這樣就不會觸發(fā)重新剪裁,避免不必要的性能開銷。如果剪裁區(qū)域會發(fā)生變化(比如在對剪裁區(qū)域執(zhí)行一個動畫),那么變化后應(yīng)該返回true來重新執(zhí)行剪裁。
然后,我們通過ClipRect
來執(zhí)行剪裁,為了看清圖片實際所占用的位置,我們設(shè)置一個紅色背景:
DecoratedBox(
decoration: BoxDecoration(
color: Colors.red
),
child: ClipRect(
clipper: MyClipper(), //使用自定義的clipper
child: avatar
),
)
可以看到我們的剪裁成功了,但是圖片所占用的空間大小仍然是60×60(紅色區(qū)域),這是因為剪裁是在layout完成后的繪制階段進行的,所以不會影響組件的大小,這和Transform原理是相似的。
五、路由管理
路由(Route)在移動開發(fā)中通常指頁面(Page),這跟web開發(fā)中單頁應(yīng)用的Route概念意義是相同的,Route在Android中通常指一個Activity,在iOS中指一個ViewController。所謂路由管理,就是管理頁面之間如何跳轉(zhuǎn),通常也可被稱為導(dǎo)航管理。Flutter中的路由管理和原生開發(fā)類似,無論是Android還是iOS,導(dǎo)航管理都會維護一個路由棧,路由入棧(push)操作對應(yīng)打開一個新頁面,路由出棧(pop)操作對應(yīng)頁面關(guān)閉操作,而路由管理主要是指如何來管理路由棧。
MaterialPageRoute
MaterialPageRoute
繼承自PageRoute類,PageRoute類是一個抽象類,表示占有整個屏幕空間的一個模態(tài)路由頁面,它還定義了路由構(gòu)建及切換時過渡動畫的相關(guān)接口及屬性。MaterialPageRoute 是Material組件庫提供的組件,它可以針對不同平臺,實現(xiàn)與平臺頁面切換動畫風(fēng)格一致的路由切換動畫:
對于Android,當打開新頁面時,新的頁面會從屏幕底部滑動到屏幕頂部;當關(guān)閉頁面時,當前頁面會從屏幕頂部滑動到屏幕底部后消失,同時上一個頁面會顯示到屏幕上。
對于iOS,當打開頁面時,新的頁面會從屏幕右側(cè)邊緣一致滑動到屏幕左邊,直到新頁面全部顯示到屏幕上,而上一個頁面則會從當前屏幕滑動到屏幕左側(cè)而消失;當關(guān)閉頁面時,正好相反,當前頁面會從屏幕右側(cè)滑出,同時上一個頁面會從屏幕左側(cè)滑入。
MaterialPageRoute
構(gòu)造函數(shù)的各個參數(shù):
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder 是一個WidgetBuilder類型的回調(diào)函數(shù),它的作用是構(gòu)建路由頁面的具體內(nèi)容,返回值是一個widget。我們通常要實現(xiàn)此回調(diào),返回新路由的實例。
settings 包含路由的配置信息,如路由名稱、是否初始路由(首頁)。
maintainState:默認情況下,當入棧一個新路由時,原來的路由仍然會被保存在內(nèi)存中,如果想在路由沒用的時候釋放其所占用的所有資源,可以設(shè)置maintainState為false。
fullscreenDialog表示新的路由頁面是否是一個全屏的模態(tài)對話框,在iOS中,如果fullscreenDialog為true,新頁面將會從屏幕底部滑入(而不是水平方向)。
Navigator
Navigator是一個路由管理的組件,它提供了打開和退出路由頁方法。Navigator通過一個棧來管理活動路由集合。通常當前屏幕顯示的頁面就是棧頂?shù)穆酚伞avigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:
Future push(BuildContext context, Route route)
將給定的路由入棧(即打開新的頁面),返回值是一個Future對象,用以接收新路由出棧(即關(guān)閉)時的返回數(shù)據(jù)。
bool pop(BuildContext context, [ result ])
將棧頂路由出棧,result為頁面關(guān)閉時返回給上一個頁面的數(shù)據(jù)。
Navigator 還有很多其它方法,如Navigator.replace、Navigator.popUntil等,詳情請參考API文檔或SDK源碼注釋。
實例方法
Navigator類中第一個參數(shù)為context的靜態(tài)方法都對應(yīng)一個Navigator的實例方法, 比如Navigator.push(BuildContext context, Route route)
等價于Navigator.of(context).push(Route route)
路由傳值
很多時候,在路由跳轉(zhuǎn)時我們需要帶一些參數(shù),并且返回上一個路由頁面時同時會帶上一個返回參數(shù)。
例:
_jumpToNextPage() {
//跳轉(zhuǎn)返回的是一個Future
var result =
Navigator.of(this.context).push(MaterialPageRoute(builder: (context) {
return RoutePageA(title: "PageA");
}));
//使用Future取得回調(diào)值value
result.then((value) {
this.setState(() {
nextActionName = value;
});
print(value);
});
}
//pop時傳回調(diào)參數(shù)
leading: IconButton(
icon: BackButtonIcon(),
onPressed: () {
Navigator.pop(context, "我是返回值");
}),
),
命名路由
所謂“命名路由”(Named Route)即有名字的路由,我們可以先給路由起一個名字,然后就可以通過路由名字直接打開新的路由了,這為路由管理帶來了一種直觀、簡單的方式。
路由表
要想使用命名路由,我們必須先提供并注冊一個路由表(routing table),這樣應(yīng)用程序才知道哪個名字與哪個路由組件相對應(yīng)。其實注冊路由表就是給路由起名字,路由表的定義如下:
Map<String, WidgetBuilder> routes;
它是一個Map,key為路由的名字,是個字符串;value是個builder回調(diào)函數(shù),用于生成相應(yīng)的路由widget。我們在通過路由名字打開新路由時,應(yīng)用會根據(jù)路由名字在路由表中查找到對應(yīng)的WidgetBuilder回調(diào)函數(shù),然后調(diào)用該回調(diào)函數(shù)生成路由widget并返回。
注冊路由表
路由表的注冊方式很簡單,我們回到之前“計數(shù)器”的示例,然后在MyApp類的build方法中找到MaterialApp
,添加routes屬性,代碼如下:
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名為"/"的路由作為應(yīng)用的home(首頁)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注冊路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注冊首頁路由
}
);
通過路由名打開新路由頁
要通過路由名稱來打開新路由,可以使用Navigator 的pushNamed方法:
Future pushNamed(BuildContext context, String routeName,{Object arguments})
命名路由參數(shù)傳遞
Navigator.of(context).pushNamed("new_page", arguments: "hi");
在路由頁通過RouteSetting對象獲取路由參數(shù):
class EchoRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
//獲取路由參數(shù)
var args=ModalRoute.of(context).settings.arguments;
//...省略無關(guān)代碼
}
}
路由鉤子(onGenerateRoute)
MaterialApp有一個onGenerateRoute屬性,它在打開命名路由時可能會被調(diào)用,之所以說可能,是因為當調(diào)用Navigator.pushNamed(...)打開命名路由時,如果指定的路由名在路由表中已注冊,則會調(diào)用路由表中的builder函數(shù)來生成路由組件;如果路由表中沒有注冊,才會調(diào)用onGenerateRoute來生成路由。
可以對某些指定的路由進行攔截,有時候不想改變頁面結(jié)構(gòu),但是又想要求跳轉(zhuǎn)到這個頁面的時候可以用到
return MaterialApp(
title: "Welcome to Flutter1",
initialRoute: '/',
// home: HomePage(),
routes: RoutesMap.instance.initRoutes(context),
onGenerateRoute: (RouteSettings settings) {
String? routeName = settings.name;
return MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return LoginPage();
});
},
);
Navigator.pushNamed(context, "沒注冊的page");
會直接跳轉(zhuǎn)到LoginPage
其他
initialRoute
:是項目的根路由,初始化的時候最先展示的頁面
onUnknownRoute
(RouteFactory類型函數(shù)):在路由匹配不到的時候用到,一般都返回一個統(tǒng)一的錯誤頁面或404頁面