Flutter 入門指北(Part 3)之 Appbar,Scaffold 填坑
該文已授權公眾號 「碼個蛋」,轉載請指明出處
上一篇講完 Flutter
中的一些基本部件,這篇就先填完上篇留下的沒寫的 AppBar
的坑,以及 Scaffold
其他引數的使用,在開始前,先補一張縮略版的腦圖

完整版放在網盤,小夥伴自己下載。完整版腦圖,提取碼:el9q,xmind 檔案 提取碼:1o5d
####AppBar(part2)
這一部分,我們只關注 Scaffold
中的 AppBar
剩下的還是埋坑【坑4】(/內牛滿面,居然已經埋了那麼多坑了,坑雖多,程式碼還是要繼續的),因為稍後會用到 StatefulWidget
的屬性,所以就直接先使用了,和 StatelessWidget
區別用法可以這麼記 需要資料更新的介面用 StatefulWidget
,當然也不是絕對的,就是之前留的【坑1】所說的狀態管理
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { List<String> _abs = ['A', 'B', 'S']; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, // 標題內容居中 automaticallyImplyLeading: false, // 不使用預設 leading: Icon(Icons.menu, color: Colors.red, size: 30.0), // 左側按鈕 flexibleSpace: Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), // 背景 title: Text('AppBar Demo', style: TextStyle(color: Colors.red)), // 標題內容 // 末尾的操作按鈕列表 actions: <Widget>[ PopupMenuButton( onSelected: (val) => print('Selected item is $val'), icon: Icon(Icons.more_vert, color: Colors.red), itemBuilder: (context) => List.generate(_abs.length, (index) => PopupMenuItem(value: _abs[index], child: Text(_abs[index])))) ], ), ); } } 複製程式碼
最後的效果圖,未點選右側按鈕如左側所示,點選右側按鈕會彈出相應的 mune

該部分程式碼檢視 app_bar_main.dart
檔案
看到效果圖,相信很多小夥伴會吐槽,「**,上面那層半透明的啥玩意,那麼醜」,接下來我們來解決這個問題,修改 void main
方法
void main() { runApp(DemoApp()); // 新增如下程式碼,使狀態列透明 if (Platform.isAndroid) { var style = SystemUiOverlayStyle(statusBarColor: Colors.transparent); SystemChrome.setSystemUIOverlayStyle(style); } } 複製程式碼
關閉後重新執行,就可以看到那層醜醜的「半透明蒙層」沒有了。

PopupMenuButton
這個部件,還是按照慣例看建構函式
// itemBuilder typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context); // onSelected typedef PopupMenuItemSelected<T> = void Function(T value); const PopupMenuButton({ Key key, @required this.itemBuilder, // 用於定義 menu 列表,需要傳入 List<PopupMenuEntry<T>> this.initialValue, // 初始值,是個泛型 T,也就是型別和你傳入的值有關 this.onSelected, // 選中 item 的回撥函式,返回 T value,例如選中 `s` 則返回 s this.onCanceled, // 未選擇任何 menu,直接點選外側使 mune 列表關閉的回撥 this.tooltip, // 長按時的提示 this.elevation = 8.0, this.padding = const EdgeInsets.all(8.0), this.child, // 用於自定義按鈕的內容 this.icon, // 按鈕的圖示 this.offset = Offset.zero, // 展示時候的便宜,Offset 需要傳入 x,y 軸偏移量,會根據傳入值平移 }) 複製程式碼
####AppBar - bottom
AppBar
還有個 bottom
屬性沒講,因為 bottom
這個屬性和圖片背景一起使用會比較醜,所以就單獨拎出來講,我們直接在原來的程式碼上修改
// 這裡需要用 with 引入 `SingleTickerProviderStateMixin` 這個類 class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { List<String> _abs = ['A', 'B', 'S']; TabController _tabController; // TabBar 必須傳入這個引數 @override void initState() { super.initState(); // 引入 `SingleTickerProviderStateMixin` 類主要是因為 _tabController 需要傳入 vsync 引數 _tabController = TabController(length: _abs.length, vsync: this); } @override void dispose() { // 需要在介面 dispose 之前把 _tabController dispose,防止記憶體洩漏 _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, automaticallyImplyLeading: false, leading: Icon(Icons.menu, color: Colors.red, size: 30.0), //flexibleSpace: Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), title: Text('AppBar Demo', style: TextStyle(color: Colors.red)), actions: <Widget>[ PopupMenuButton( offset: Offset(50.0, 100.0), onSelected: (val) => print('Selected item is $val'), icon: Icon(Icons.more_vert, color: Colors.red), itemBuilder: (context) => List.generate(_abs.length, (index) => PopupMenuItem(value: _abs[index], child: Text(_abs[index])))) ], bottom: TabBar( labelColor: Colors.red, // 選中時的顏色 unselectedLabelColor: Colors.white, // 未選中顏色 controller: _tabController, isScrollable: false, // 是否固定,當超過一定數量的 tab 時,如果一行排不下,可設定 true indicatorColor: Colors.yellow, // 導航的顏色 indicatorSize: TabBarIndicatorSize.tab, // 導航樣式,還有個選項是 TabBarIndicatorSize.label tab 時候,導航和 tab 同寬,label 時候,導航和 icon 同寬 indicatorWeight: 5.0, // 導航高度 tabs: List.generate(_abs.length, (index) => Tab(text: _abs[index], icon: Icon(Icons.android)))), // 導航內容列表 ), ); } } 複製程式碼
最終的效果圖如下:

####PageView + TabBar
那麼如何通過 TabBar
切換介面呢,這邊我們需要用到 PageView
這個部件,當然還有別的部件,例如 IndexStack
等,小夥伴可以自己嘗試使用別的,這邊通過 PageView
和 TabBar
進行關聯,帶動頁面切換, PageViede
的屬性引數相對比較簡單,這邊就不貼啦。最終的效果我們目前只展示一個文字即可,我們先定義一個通用的切換介面
class TabChangePage extends StatelessWidget { // 需要傳入的引數 final String content; // TabChangePage(this.content); 不推薦這樣寫構造方法 // 推薦用這樣的構造方法,key 可以作為唯一值查詢 TabChangePage({Key key, this.content}) : super(key: key); @override Widget build(BuildContext context) { // 僅展示傳入的內容 return Container( alignment: Alignment.center, child: Text(content, style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 30.0))); } } 複製程式碼
定義通用介面後,就可以作為 PageView
的子介面傳入並展示
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { List<String> _abs = ['A', 'B', 'S']; TabController _tabController; // 用於同 TabBar 進行聯動 PageController _pageController; @override void initState() { super.initState(); _tabController = TabController(length: _abs.length, vsync: this); _pageController = PageController(initialPage: 0); _tabController.addListener(() { // 判斷 TabBar 是否切換位置了,如果切換了,則修改 PageView 的顯示 if (_tabController.indexIsChanging) { // PageView 的切換通過 controller 進行滾動 // duration 表示切換滾動的時長,curve 表示滾動動畫的樣式, // flutter 已經在 Curves 中定義許多樣式,可以自行切換檢視效果 _pageController.animateToPage(_tabController.index, duration: Duration(milliseconds: 300), curve: Curves.decelerate); } }); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, automaticallyImplyLeading: false, leading: Icon(Icons.menu, color: Colors.red, size: 30.0), //flexibleSpace: Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), title: Text('AppBar Demo', style: TextStyle(color: Colors.red)), actions: <Widget>[ PopupMenuButton( offset: Offset(50.0, 100.0), onSelected: (val) => print('Selected item is $val'), icon: Icon(Icons.more_vert, color: Colors.red), itemBuilder: (context) => List.generate(_abs.length, (index) => PopupMenuItem(value: _abs[index], child: Text(_abs[index])))) ], bottom: TabBar( labelColor: Colors.red, unselectedLabelColor: Colors.white, controller: _tabController, isScrollable: false, indicatorColor: Colors.yellow, indicatorSize: TabBarIndicatorSize.tab, indicatorWeight: 5.0, tabs: List.generate(_abs.length, (index) => Tab(text: _abs[index], icon: Icon(Icons.android)))), ), // 通過 body 來展示內容,body 可以傳入任何 Widget,裡面就是你需要展示的介面內容 // 所以前面留下 Scaffold 中 body 部分的坑就解決了 body: PageView( controller: _pageController, children: _abs.map((str) => TabChangePage(content: str)).toList(), // 通過 Map 轉換後再通過 toList 轉換成列表,效果同 List.generate onPageChanged: (position) { // PageView 切換的監聽,這邊切換 PageView 的頁面後,TabBar 也需要隨之改變 // 通過 tabController 來改變 TabBar 的顯示位置 _tabController.index = position; }, ), ); } } 複製程式碼
最終的效果圖就不貼了,可以發現滑動 PageView
或者點選切換 TabBar
的位置,介面顯示的內容都會隨之改變,同時,解決前面 Scaffold
留下 body
屬性沒講的一個坑,就剩下 drawer
、 bottomNavigationBar
屬性沒講了,在解決這兩個坑之前,我們先處理下另一個問題
Scaffold
能夠使我們快速去搭建一個介面,但是,並不是所有的介面都需要 AppBar
這個標題,那麼我們就不會傳入 appBar
的屬性,我們註釋 _HomePageState
中 Scaffold
的 appBar
傳入值,把 body
傳入的 PageView
修改成單個 TabChangePage
,然後把 TabChangePage
這個類做下修改,把 Container
的 aligment
屬性也註釋了,這樣顯示的內容就會顯示在左上角
// _HomePageState // .. @override Widget build(BuildContext context) { return Scaffold(body: TabChangePage(content: 'Content')); } class TabChangePage extends StatelessWidget { final String content; TabChangePage({Key key, this.content}) : super(key: key); @override Widget build(BuildContext context) { return Container(child: Text(content, style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 30.0))); } } 複製程式碼
然後執行下,「**,文字怎麼被狀態列給擋了...」 不要慌,靜下心喝杯茶,眺望下遠方,這裡就需要用 SafeArea
來處理了,在 TabChangePage
的 Container
外層加一層 SafeArea
@override Widget build(BuildContext context) { return SafeArea( child: Container(child: Text(content, style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 30.0)))); } 複製程式碼
然後重新執行,一切正常, SafeArea
的用途可以看下原始碼的解釋
/// A widget that insets its child by sufficient padding to avoid intrusions by /// the operating system. /// /// For example, this will indent the child by enough to avoid the status bar at /// the top of the screen. 複製程式碼
翻譯過來大概就是「給子部件和系統點選無效區域留有足夠空間,比如狀態列和系統導航欄」, SafeArea
可以很好解決劉海屏覆蓋頁面內容的問題,那麼到目前為止, AppBar
的一些坑就說的差不多了,就要解決剩下的坑了
Scaffold - Drawer
drawer
同 endDrawer
屬性是一樣的,除了滑動的方向, Drawer
這個元件也相對比較簡單,只要傳入一個 child
即可,在展示之前,先對 appBar
做下處理,設定 leading
為系統預設,點選 leading
的時候 Drawer
就可以滑出來了,當然手動滑也可以
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, //automaticallyImplyLeading: false, //leading: Icon(Icons.menu, color: Colors.red, size: 30.0), //flexibleSpace: Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), title: Text('AppBar Demo', style: TextStyle(color: Colors.red)), actions: <Widget>[ PopupMenuButton( offset: Offset(50.0, 100.0), onSelected: (val) => print('Selected item is $val'), icon: Icon(Icons.more_vert, color: Colors.red), itemBuilder: (context) => List.generate(_abs.length, (index) => PopupMenuItem(value: _abs[index], child: Text(_abs[index])))) ], bottom: TabBar( labelColor: Colors.red, unselectedLabelColor: Colors.white, controller: _tabController, isScrollable: false, indicatorColor: Colors.yellow, indicatorSize: TabBarIndicatorSize.tab, indicatorWeight: 5.0, tabs: List.generate(_abs.length, (index) => Tab(text: _abs[index], icon: Icon(Icons.android)))), ), // body .... drawer: Drawer( // 記得要先新增 `SafeArea` 防止檢視頂到狀態列下面 child: SafeArea( child: Container( child: Text('Drawer', style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 30.0)), )), ), ); //return Scaffold(body: TabChangePage(content: 'Content')); } 複製程式碼
最終的效果圖也不貼了,當手勢從左側滑出或者點選 leading
圖示,抽屜就出來了
AppBar - bottomNavigationBar
bottomNavigarionBar
可以傳入一個 BottomNavigationBar
例項, BottomNavigationBar
需要傳入 BottomNavigationBarItem
列表作為 items
,但是這邊為了實現一個 bottomNavigationBar
和 floatingActionButton
一個特殊的組合效果,我們不使用 BottomNavigationBar
,換做 BottomAppBar
,直接上程式碼吧
@override Widget build(BuildContext context) { return Scaffold( /// 一樣的程式碼省略.... bottomNavigationBar: BottomAppBar( shape: CircularNotchedRectangle(), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ IconButton(icon: Icon(Icons.android, size: 30.0, color: Theme.of(context).primaryColor), onPressed: () {}), IconButton(icon: Icon(Icons.people, size: 30.0, color: Theme.of(context).primaryColor), onPressed: () {}) ], ), ), floatingActionButton: FloatingActionButton(onPressed: () => print('Add'), child: Icon(Icons.add, color: Colors.white)), // FAB 的位置,一共有 7 中位置可以選擇,centerDocked, endDocked, centerFloat, endFloat, endTop, startTop, miniStartTop,這邊選擇懸浮在 dock floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ); 複製程式碼
最終的效果圖:

既然提到了 StatefulWidget
,順帶提下兩種比較簡單的部件,也算是基礎部件吧。 CheckBox
、 CheckboxListTile
, Switch
、 SwitchListTile
因為比較簡單,就直接上程式碼了,裡面都有完整的註釋
class CheckSwitchDemoPage extends StatefulWidget { @override _CheckSwitchDemoPageState createState() => _CheckSwitchDemoPageState(); } class _CheckSwitchDemoPageState extends State<CheckSwitchDemoPage> { var _isChecked = false; var _isTitleChecked = false; var _isOn = false; var _isTitleOn = false; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Check Switch Demo'), ), body: Column(children: <Widget>[ Row( children: <Widget>[ Checkbox( // 是否開啟三態 tristate: true, // 控制當前 checkbox 的開啟狀態 value: _isChecked, // 不設定該方法,處於不可用狀態 onChanged: (checked) { // 管理狀態值 setState(() => _isChecked = checked); }, // 選中時的顏色 activeColor: Colors.pink, // 這個值有 padded 和 shrinkWrap 兩個值, // padded 時候所佔有的空間比 shrinkWrap 大,別的原諒我沒看出啥 materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), /// 點選無響應 Checkbox(value: _isChecked, onChanged: null, tristate: true) ], ), Row( children: <Widget>[ Switch( // 開啟時候,那個條的顏色 activeTrackColor: Colors.yellow, // 關閉時候,那個條的顏色 inactiveTrackColor: Colors.yellow[200], // 設定指示器的圖片,當然也有 color 可以設定 activeThumbImage: AssetImage('images/ali.jpg'), inactiveThumbImage: AssetImage('images/ali.jpg'), // 開始時候的顏色,貌似會被 activeTrackColor 頂掉 activeColor: Colors.pink, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _isOn, onChanged: (onState) { setState(() => _isOn = onState); }), /// 點選無響應 Switch(value: _isOn, onChanged: null) ], ), CheckboxListTile( // 描述選項 title: Text('Make this item checked'), // 二級描述 subtitle: Text('description...description...\ndescription...description...'), // 和 checkbox 對立邊的部件,例如 checkbox 在頭部,則 secondary 在尾部 secondary: Image.asset('images/ali.jpg', width: 30.0, height: 30.0), value: _isTitleChecked, // title 和 subtitle 是否為垂直密集列表中一員,最明顯就是部件會變小 dense: true, // 是否需要使用 3 行的高度,該值為 true 時候,subtitle 不可為空 isThreeLine: true, // 控制 checkbox 選擇框是在前面還是後面 controlAffinity: ListTileControlAffinity.leading, // 是否將主題色應用到文字或者圖示 selected: true, onChanged: (checked) { setState(() => _isTitleChecked = checked); }, ), SwitchListTile( title: Text('Turn On this item'), subtitle: Text('description...description...\ndescription...description...'), secondary: Image.asset('images/ali.jpg', width: 30.0, height: 30.0), isThreeLine: true, value: _isTitleOn, selected: true, onChanged: (onState) { setState(() => _isTitleOn = onState); }) ]), ); } } 複製程式碼

該部分程式碼檢視 checkbox_swicth_main.dart
檔案
終於這節把 Scaffold
留下的坑都填完了,然後又講了兩種基礎部件,下節要填留下的別的坑了,目測還留了 2 個大坑,那就等以後繼續解決吧~
最後程式碼的地址還是要的:
-
文章中涉及的程式碼: demos
-
基於郭神
cool weather
介面的一個專案,實現BLoC
模式,實現狀態管理: flutter_weather -
一個課程(當時買了想看下程式碼規範的,程式碼更新會比較慢,雖然是跟著課上的一些寫程式碼,但是還是做了自己的修改,很多地方看著不舒服,然後就改成自己的實現方式了): flutter_shop
如果對你有幫助的話,記得給個 Star ,先謝過,你的認可就是支援我繼續寫下去的動力~