# Scaffold、TabBar、底部導航
Material庫提供了很多Widget,本節介紹一些常用的Widget,其余的讀者可以查看文檔或Flutter Gallery中Material組件部分的示例。注意,筆者強烈建議用戶將Flutter Gallery示例跑起來,它是一個很全面的Flutter示例,是非常好的參考Demo。
### Scaffold
大多數路由頁都會包含一個導航欄,有些路由頁可能會有抽屜菜單(Drawer)以及底部Tab導航菜單等。如果每個頁面都需要開發者自己手動去實現,這會是一件非常無聊的事。幸運的是,我們前面提到過,Flutter Material庫提供了一個Scaffold Widget,它是一個路由頁的骨架,可以非常容易的拼裝出一個完整的頁面。
## 示例
我們實現一個頁面,它包含:
1. 一個導航欄
2. 導航欄右邊有一個分享按鈕
3. 有一個抽屜菜單
4. 有一個底部導航
5. 右下角有一個懸浮的動作按鈕
最終效果如下:

實現代碼如下:
```
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( //導航欄
title: Text("App Name"),
actions: <Widget>[ //導航欄右側菜單
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
),
drawer: new MyDrawer(), //抽屜
bottomNavigationBar: BottomNavigationBar( // 底部導航
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(){
}
}
```
上面代碼中我們用到了另外幾個Widget,下面我們來分別介紹一下:
### AppBar
AppBar是一個Material風格的導航欄,它可以設置標題、導航欄菜單、底部Tab等。下面我們看看AppBar的定義:
```
AppBar({
Key key,
this.leading, //導航欄最左側Widget,常見為抽屜菜單按鈕或返回按鈕。
this.automaticallyImplyLeading = true, //如果leading為null,是否自動實現默認的leading按鈕
this.title,// 頁面標題
this.actions, // 導航欄右側菜單
this.bottom, // 導航欄底部菜單,通常為Tab按鈕組
this.elevation = 4.0, // 導航欄陰影
this.centerTitle, //標題是否居中
this.backgroundColor,
... //其它屬性見源碼注釋
})
```
如果給Scaffold添加了抽屜菜單,默認情況下Scaffold會自動將AppBar的leading設置為菜單按鈕(如上面截圖所示)。如果我們想自定義菜單圖標,可以手動來設置leading,如:
```
Scaffold(
appBar: AppBar(
title: Text("App Name"),
leading: Builder(builder: (context) {
return IconButton(
icon: Icon(Icons.dashboard, color: Colors.white), //自定義圖標
onPressed: () {
// 打開抽屜菜單
Scaffold.of(context).openDrawer();
},
);
}),
...
)
```
代碼運行效果:

可以看到左側菜單已經替換成功。
代碼中打開抽屜菜單的方法在ScaffoldState中,通過`Scaffold.of(context)`可以獲取父級最近的Scaffold Widget的State對象,原理可以參考本書后面“Element與BuildContext” 一章。Flutter還有一種通用的獲取StatefulWidget對象State的方法:通過GlobalKey來獲取! 步驟有兩步:
1. 給目標StatefulWidget添加GlobalKey
```
//定義一個globalKey, 由于GlobalKey要保持全局唯一性,我們使用靜態變量存儲
static GlobalKey<ScaffoldState> _globalKey= new GlobalKey();
...
Scaffold(
key: _globalKey , //設置key
...
)
```
2. 通過GlobalKey來獲取State對象
```
_globalKey.currentState.openDrawer()
```
#### TabBar
下面我們通過“bottom”屬性來添加一個導航欄底部tab按鈕組,將要實現的效果如下:

Material組件庫中提供了一個TabBar組件,它可以快速生成Tab菜單,下面是上圖對應的源碼:
```
class _ScaffoldRouteState extends State<ScaffoldRoute>
with SingleTickerProviderStateMixin {
TabController _tabController; //需要定義一個Controller
List tabs = ["新聞", "歷史", "圖片"];
@override
void initState() {
super.initState();
// 創建Controller
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
... //省略無關代碼
bottom: TabBar( //生成Tab菜單
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()),
),
... //省略無關代碼
}
```
上面代碼首先創建了一個TabController ,它是用于控制/監聽Tab菜單切換。然后通過TabBar生成了一個底部菜單欄,TabBar的`tabs`屬性接受一個Widget數組,表示每一個Tab子菜單,我們可以自定義,也可以像示例中一樣直接使用Tab Widget,它也是Material組件庫提供的Material風格的Tab菜單。
Tab Widget有三個可選參數,除了可以指定文字外,還可以指定Tab菜單圖標,或者直接自定義Widget,定義如下:
```
Tab({
Key key,
this.text, // 菜單文本
this.icon, // 菜單圖標
this.child, // 自定義Widget
})
```
開發者可以根據實際需求來定制。
#### TabBarView
通過TabBar我們只能生成一個靜態的菜單,如果要實現Tab頁,我們可以通過TabController去監聽Tab菜單的切換去切換Tab頁,代碼如:
```
_tabController.addListener((){
switch(_tabController.index){
case 1: ...;
case 2: ... ;
}
});
```
如果我們Tab頁可以滑動切換的話,還需要在滑動過程中更新TabBar指示器的偏移。顯然,要手動處理這些是很麻煩的,為此,Material庫提供了一個TabBarView組件,它可以很輕松的配合TabBar來實現同步切換和滑動狀態同步,示例如下:
```
Scaffold(
appBar: AppBar(
... //省略無關代碼
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()),
),
drawer: new MyDrawer(),
body: TabBarView(
controller: _tabController,
children: tabs.map((e) { //創建3個Tab頁
return Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
);
}).toList(),
),
... // 省略無關代碼
)
```
運行后效果如下:

現在,無論是點擊導航欄Tab菜單還是在頁面上左右滑動,Tab頁面都會切換,并且Tab菜單的狀態和Tab頁面始終保持同步。下面我們來看看代碼,細心的讀者可以發現,TabBar和TabBarView的controller是同一個!正是如此,TabBar和TabBarView正是通過同一個controller來實現菜單切換和滑動狀態同步的。
另外,Material組件庫也提供了一個PageView Widget,它和TabBarView功能相似,讀者可以自行了解一下。
### 抽屜菜單Drawer
Scaffold的`drawer`和`endDrawer`屬性可以分別接受一個Widget作為頁面的左、右抽屜菜單,如果開發者提供了抽屜菜單,那么當用戶手指重屏幕左/右向里滑動時便可打開抽屜菜單。本節開始部分的示例中實現了一個左抽屜菜單MyDrawer,源碼如下:
```
class MyDrawer extends StatelessWidget {
const MyDrawer({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding(
context: context,
// DrawerHeader consumes top MediaQuery padding.
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作為根節點,它實現了Material風格的菜單面板,`MediaQuery.removePadding`可以移除抽Drawer內的一些指定空白,讀者可以嘗試傳遞不同的參數來看看實際效果。抽屜菜單頁頂部由用戶頭像和昵稱組成,底部是一個菜單列表,用ListView實現,關于ListView我們將在后面“可滾動Widget”一節詳細介紹。
### FloatingActionButton
FloatingActionButton是Material設計規范中的一種特殊Button,通常懸浮在頁面的某一個位置作為某種常用動作的快捷入口,如本節示例中頁面右下角的"?"號按鈕。我們可以通過Scaffold的`floatingActionButton`屬性來設置一個FloatingActionButton,同時通過`floatingActionButtonLocation`屬性來指定其在頁面中懸浮的位置,這個比較簡單,不在贅述。
### 底部Tab導航欄
我們可以通過Scaffold的`bottomNavigationBar`屬性來設置底部導航,如本節開始示例所示,我們通過Material組件庫提供的BottomNavigationBar和BottomNavigationBarItem兩個Widget來實現Material風格的底部導航欄,可以看到代碼非常簡單,不在贅述。但是如果我們想實現如下效果的底部導航應該怎么做呢?

Material組件庫中提供了一個BottomAppBar Widget,可以和FloatingActionButton配合實現這種"打洞"效果。源碼如下:
```
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 底部導航欄打一個圓形的洞
child: Row(
children: [
IconButton(icon: Icon(Icons.home)),
SizedBox(), //中間位置空出
IconButton(icon: Icon(Icons.business)),
],
mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部導航欄橫向空間
),
)
```
可以看到,上面代碼中沒有控制打洞位置的屬性,實際上,打洞的位置取決于FloatingActionButton的位置,上面FloatingActionButton的位置為:
```
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
```
BottomAppBar的`shape`屬性決定洞的外形,CircularNotchedRectangle實現了一個圓形的外形,我們也可以自定義外形,比如,Flutter Gallery示例中就有一個”鉆石“形狀的實現,讀者感興趣可以自行查看。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示