Material 組件庫(kù)提供了豐富多樣的組件,本節(jié)介紹一些常用的組件,其余的讀者可以自行查看文檔或 Flutter Gallery 中 Material 組件部分的示例。
Flutter Gallery 是 Flutter 官方提供的 Flutter Demo,源碼位于 flutter 源碼中的 examples 目錄下,筆者強(qiáng)烈建議用戶將 Flutter Gallery 示例跑起來(lái),它是一個(gè)很全面的 Flutter 示例應(yīng)用,是非常好的參考 Demo,也是筆者學(xué)習(xí) Flutter 的第一手資料。
一個(gè)完整的路由頁(yè)可能會(huì)包含導(dǎo)航欄、抽屜菜單(Drawer)以及底部Tab導(dǎo)航菜單等。如果每個(gè)路由頁(yè)面都需要開發(fā)者自己手動(dòng)去實(shí)現(xiàn)這些,這會(huì)是一件非常麻煩且無(wú)聊的事。幸運(yùn)的是,F(xiàn)lutter Material組件庫(kù)提供了一些現(xiàn)成的組件來(lái)減少我們的開發(fā)任務(wù)。Scaffold
是一個(gè)路由頁(yè)的骨架,我們使用它可以很容易地拼裝出一個(gè)完整的頁(yè)面。
我們實(shí)現(xiàn)一個(gè)頁(yè)面,它包含:
最終效果如圖5-18、圖5-19所示:
實(shí)現(xiàn)代碼如下:
class ScaffoldRoute extends StatefulWidget {
@override
_ScaffoldRouteState createState() => _ScaffoldRouteState();
}
class _ScaffoldRouteState extends State<ScaffoldRoute> {
int _selectedIndex = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( //導(dǎo)航欄
title: Text("App Name"),
actions: <Widget>[ //導(dǎo)航欄右側(cè)菜單
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
),
drawer: new MyDrawer(), //抽屜
bottomNavigationBar: BottomNavigationBar( // 底部導(dǎo)航
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: _onItemTapped,
),
floatingActionButton: FloatingActionButton( //懸浮按鈕
child: Icon(Icons.add),
onPressed:_onAdd
),
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
void _onAdd(){
}
}
上面代碼中我們用到了如下組件:
組件名稱 | 解釋 |
---|---|
AppBar | 一個(gè)導(dǎo)航欄骨架 |
MyDrawer | 抽屜菜單 |
BottomNavigationBar | 底部導(dǎo)航欄 |
FloatingActionButton | 漂浮按鈕 |
下面我們來(lái)分別介紹一下它們。
AppBar
是一個(gè) Material 風(fēng)格的導(dǎo)航欄,通過(guò)它可以設(shè)置導(dǎo)航欄標(biāo)題、導(dǎo)航欄菜單、導(dǎo)航欄底部的Tab標(biāo)題等。下面我們看看 AppBar 的定義:
AppBar({
Key key,
this.leading, //導(dǎo)航欄最左側(cè)Widget,常見為抽屜菜單按鈕或返回按鈕。
this.automaticallyImplyLeading = true, //如果leading為null,是否自動(dòng)實(shí)現(xiàn)默認(rèn)的leading按鈕
this.title,// 頁(yè)面標(biāo)題
this.actions, // 導(dǎo)航欄右側(cè)菜單
this.bottom, // 導(dǎo)航欄底部菜單,通常為Tab按鈕組
this.elevation = 4.0, // 導(dǎo)航欄陰影
this.centerTitle, //標(biāo)題是否居中
this.backgroundColor,
... //其它屬性見源碼注釋
})
如果給Scaffold
添加了抽屜菜單,默認(rèn)情況下Scaffold
會(huì)自動(dòng)將AppBar
的leading
設(shè)置為菜單按鈕(如上面截圖所示),點(diǎn)擊它便可打開抽屜菜單。如果我們想自定義菜單圖標(biāo),可以手動(dòng)來(lái)設(shè)置leading
,如:
Scaffold(
appBar: AppBar(
title: Text("App Name"),
leading: Builder(builder: (context) {
return IconButton(
icon: Icon(Icons.dashboard, color: Colors.white), //自定義圖標(biāo)
onPressed: () {
// 打開抽屜菜單
Scaffold.of(context).openDrawer();
},
);
}),
...
)
代碼運(yùn)行效果如圖5-20所示:
可以看到左側(cè)菜單已經(jīng)替換成功。
代碼中打開抽屜菜單的方法在ScaffoldState
中,通過(guò)Scaffold.of(context)
可以獲取父級(jí)最近的Scaffold
組件的State
對(duì)象。
下面我們通過(guò)“bottom”屬性來(lái)添加一個(gè)導(dǎo)航欄底部 Tab 按鈕組,將要實(shí)現(xiàn)的效果如圖5-21所示:
Material 組件庫(kù)中提供了一個(gè)TabBar
組件,它可以快速生成Tab
菜單,下面是上圖對(duì)應(yīng)的源碼:
class _ScaffoldRouteState extends State<ScaffoldRoute>
with SingleTickerProviderStateMixin {
TabController _tabController; //需要定義一個(gè)Controller
List tabs = ["新聞", "歷史", "圖片"];
@override
void initState() {
super.initState();
// 創(chuàng)建Controller
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
... //省略無(wú)關(guān)代碼
bottom: TabBar( //生成Tab菜單
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()
),
),
... //省略無(wú)關(guān)代碼
}
上面代碼首先創(chuàng)建了一個(gè)TabController
,它是用于控制/監(jiān)聽Tab
菜單切換的。接下來(lái)通過(guò) TabBar 生成了一個(gè)底部菜單欄,TabBar
的tabs
屬性接受一個(gè) Widget 數(shù)組,表示每一個(gè) Tab 子菜單,我們可以自定義,也可以像示例中一樣直接使用Tab
組件,它是 Material 組件庫(kù)提供的 Material 風(fēng)格的 Tab 菜單。
Tab
組件有三個(gè)可選參數(shù),除了可以指定文字外,還可以指定Tab菜單圖標(biāo),或者直接自定義組件樣式。Tab
組件定義如下:
Tab({
Key key,
this.text, // 菜單文本
this.icon, // 菜單圖標(biāo)
this.child, // 自定義組件樣式
})
開發(fā)者可以根據(jù)實(shí)際需求來(lái)定制。
通過(guò)TabBar
我們只能生成一個(gè)靜態(tài)的菜單,真正的 Tab 頁(yè)還沒(méi)有實(shí)現(xiàn)。由于Tab
菜單和 Tab 頁(yè)的切換需要同步,我們需要通過(guò)TabController
去監(jiān)聽 Tab 菜單的切換去切換 Tab 頁(yè),代碼如:
_tabController.addListener((){
switch(_tabController.index){
case 1: ...;
case 2: ... ;
}
});
如果我們 Tab 頁(yè)可以滑動(dòng)切換的話,還需要在滑動(dòng)過(guò)程中更新 TabBar 指示器的偏移!顯然,要手動(dòng)處理這些是很麻煩的,為此,Material 庫(kù)提供了一個(gè)TabBarView
組件,通過(guò)它不僅可以輕松的實(shí)現(xiàn) Tab 頁(yè),而且可以非常容易的配合 TabBar 來(lái)實(shí)現(xiàn)同步切換和滑動(dòng)狀態(tài)同步,示例如下:
Scaffold(
appBar: AppBar(
... //省略無(wú)關(guān)代碼
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()),
),
drawer: new MyDrawer(),
body: TabBarView(
controller: _tabController,
children: tabs.map((e) { //創(chuàng)建3個(gè)Tab頁(yè)
return Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
);
}).toList(),
),
... // 省略無(wú)關(guān)代碼
)
運(yùn)行后效果如圖5-22所示:
現(xiàn)在,無(wú)論是點(diǎn)擊導(dǎo)航欄 Tab 菜單還是在頁(yè)面上左右滑動(dòng),Tab 頁(yè)面都會(huì)切換,并且 Tab 菜單的狀態(tài)和 Tab 頁(yè)面始終保持同步!那它們是如何實(shí)現(xiàn)同步的呢?細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn),上例中TabBar
和TabBarView
的controller
是同一個(gè)!正是如此,TabBar
和TabBarView
正是通過(guò)同一個(gè)controller
來(lái)實(shí)現(xiàn)菜單切換和滑動(dòng)狀態(tài)同步的,有關(guān)TabController
的詳細(xì)信息,我們不在本書做過(guò)多介紹,使用時(shí)讀者直接查看 SDK 即可。
另外,Material 組件庫(kù)也提供了一個(gè)PageView
組件,它和TabBarView
功能相似,讀者可以自行了解一下。
Scaffold
的drawer
和endDrawer
屬性可以分別接受一個(gè) Widget 來(lái)作為頁(yè)面的左、右抽屜菜單。如果開發(fā)者提供了抽屜菜單,那么當(dāng)用戶手指從屏幕左(或右)側(cè)向里滑動(dòng)時(shí)便可打開抽屜菜單。本節(jié)開始部分的示例中實(shí)現(xiàn)了一個(gè)左抽屜菜單MyDrawer
,它的源碼如下:
class MyDrawer extends StatelessWidget {
const MyDrawer({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding(
context: context,
//移除抽屜菜單頂部默認(rèn)留白
removeTop: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 38.0),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ClipOval(
child: Image.asset(
"imgs/avatar.png",
width: 80,
),
),
),
Text(
"Wendux",
style: TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
Expanded(
child: ListView(
children: <Widget>[
ListTile(
leading: const Icon(Icons.add),
title: const Text('Add account'),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Manage accounts'),
),
],
),
),
],
),
),
);
}
}
抽屜菜單通常將Drawer
組件作為根節(jié)點(diǎn),它實(shí)現(xiàn)了 Material 風(fēng)格的菜單面板,MediaQuery.removePadding
可以移除 Drawer 默認(rèn)的一些留白(比如 Drawer 默認(rèn)頂部會(huì)留和手機(jī)狀態(tài)欄等高的留白),讀者可以嘗試傳遞不同的參數(shù)來(lái)看看實(shí)際效果。抽屜菜單頁(yè)由頂部和底部組成,頂部由用戶頭像和昵稱組成,底部是一個(gè)菜單列表,用 ListView 實(shí)現(xiàn),關(guān)于 ListView 我們將在后面“可滾動(dòng)組件”一節(jié)詳細(xì)介紹。
FloatingActionButton
是 Material 設(shè)計(jì)規(guī)范中的一種特殊 Button,通常懸浮在頁(yè)面的某一個(gè)位置作為某種常用動(dòng)作的快捷入口,如本節(jié)示例中頁(yè)面右下角的"?"號(hào)按鈕。我們可以通過(guò)Scaffold
的floatingActionButton
屬性來(lái)設(shè)置一個(gè)FloatingActionButton
,同時(shí)通過(guò)floatingActionButtonLocation
屬性來(lái)指定其在頁(yè)面中懸浮的位置,這個(gè)比較簡(jiǎn)單,不再贅述。
我們可以通過(guò)Scaffold
的bottomNavigationBar
屬性來(lái)設(shè)置底部導(dǎo)航,如本節(jié)開始示例所示,我們通過(guò) Material 組件庫(kù)提供的BottomNavigationBar
和BottomNavigationBarItem
兩種組件來(lái)實(shí)現(xiàn) Material 風(fēng)格的底部導(dǎo)航欄。可以看到上面的實(shí)現(xiàn)代碼非常簡(jiǎn)單,所以不再贅述,但是如果我們想實(shí)現(xiàn)如圖5-23所示效果的底部導(dǎo)航欄應(yīng)該怎么做呢?
Material組件庫(kù)中提供了一個(gè)BottomAppBar
組件,它可以和FloatingActionButton
配合實(shí)現(xiàn)這種“打洞”效果,源碼如下:
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 底部導(dǎo)航欄打一個(gè)圓形的洞
child: Row(
children: [
IconButton(icon: Icon(Icons.home)),
SizedBox(), //中間位置空出
IconButton(icon: Icon(Icons.business)),
],
mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部導(dǎo)航欄橫向空間
),
)
可以看到,上面代碼中沒(méi)有控制打洞位置的屬性,實(shí)際上,打洞的位置取決于FloatingActionButton
的位置,上面FloatingActionButton
的位置為:
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
所以打洞位置在底部導(dǎo)航欄的正中間。
BottomAppBar
的shape
屬性決定洞的外形,CircularNotchedRectangle
實(shí)現(xiàn)了一個(gè)圓形的外形,我們也可以自定義外形,比如,F(xiàn)lutter Gallery 示例中就有一個(gè)“鉆石”形狀的示例,讀者感興趣可以自行查看。
更多建議: