Flutter之drawer詳細分析(你要的操作都有)
我們先來看看簡單的 drawer
在Flutter的應用
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: _appbar, drawer: _drawer, ); } get _appbar=>AppBar( title: Text('Drawer Test'), ); get _drawer =>Drawer( child: Text('This is Drawer'), ); } 複製程式碼
然後執行一下專案: 如下圖所示


可以看到,根據我們對 drawer
的認識,並不是想要的結果,所以這個 drawer
並不完整,然後我們繼續新增程式碼,修改 drawer
///... get _drawer => Drawer( ///edit start child: ListView( children: <Widget>[ DrawerHeader( decoration: BoxDecoration( color: Colors.lightBlueAccent, ), child: Center( child: SizedBox( width: 60.0, height: 60.0, child: CircleAvatar( child: Text('R'), ), ), ), ), ListTile( leading: Icon(Icons.settings), title: Text('設定'), ) ], ), ///edit end ); 複製程式碼
我這裡添加了 ListView
=> 裝載抽屜的部件 DrawerHeader
=>抽屜的頭部 SizeBox
=> 用於限制CircleAvatar的大小 CircleAvatar
=> 頭像部件 ListTile
=> 一個名為"設定"的點選項 然後我們熱部署一下

drawer
嘢!上面那坨灰色的東西是怎麼肥事!不急不急,我們慢慢來分析
3 . 解決Drawer灰色頭部
因為加了一個 DrawerHeader
,所以,我們需要看看 DrawerHeader
裡面是什麼原因導致新增灰色的地方 DrawerHeader
原始碼:

可以看到: Container
=>限制高度(預設高度+狀態列高度) BoxDecoration
=> 底部新增毫無用處的分割線 AnimatedContainer
=>動畫版的 Container
新增預設內邊距+頂部狀態列高度的內邊距 嗯,感覺沒錯啊,這是怎麼肥事, MediaQuery.of(context).padding.top
是獲取狀態列的高度,然後自身高度加上狀態列的高度,應該是顯示藍色才對,那會不會跟 ListView
有關係呢? 我們將 DrawerHeader
去掉看看
get _drawer => Drawer( child: ListView( children: <Widget>[ ///edit start //DrawerHeader( //decoration: BoxDecoration( //color: Colors.lightBlueAccent, //), //child: Center( //child: SizedBox( //width: 60.0, //height: 60.0, //child: CircleAvatar( //child: Text('R'), //), //), //), //), ///edit end ListTile( leading: Icon(Icons.settings), title: Text('設定'), ) ], ), ); 複製程式碼

ListView
有關,這是什麼原因導致
ListView
加上一個
statusBarHeight
大小的內邊距呢?我們可以繼續找
ListView
的原始碼

ListView
的構造方法,跳轉到455行可看到 1.當
ListView
的屬性
padding
為空時,獲取
MediaQueryData
的資訊
2.因為 ListView
的滾動方向預設為垂直,會使用 mediaQueryVerticalPadding
3. sliver
新增一層 MediaQuery
,這個表明 sliver
的子部件會使用該 MediaQuery
的值,根據判斷,子部件會使用 mediaQueryHorizontalPadding
,而上面的兩個複製:
mediaQueryHorizontalPadding
=>將原有的 MediaQuery
的padding複製為 top
和 bottom
都為0,該值會被子部件使用,所以可以知道,DrawerHeader使用了該值,導致statusBarHeader為0 mediaQueryVerticalPadding
=>將原有的 MediaQuery
的padding複製為 left
和 right
都為0
所以,我們只要不讓 ListView
的 padding
屬性為空就可以了,這裡我傳入一個zero給ListView,然後把DrawerHeader的註釋去掉,熱部署一下
get _drawer => Drawer( child: ListView( ///edit start padding: EdgeInsets.zero, ///edit end children: <Widget>[ DrawerHeader( decoration: BoxDecoration( color: Colors.lightBlueAccent, ), child: Center( child: SizedBox( width: 60.0, height: 60.0, child: CircleAvatar( child: Text('R'), ), ), ), ), ListTile( leading: Icon(Icons.settings), title: Text('設定'), ) ], ), ); 複製程式碼

ok,我們成功解決了Drawer灰色頭部
4. 定製Drawer的滑出大小
我們來看看 drawer
的原始碼, 其實看原始碼並不是一件痛苦的事,我們一般直接跳到build方法就好

可以看到Drawer這個部件就是我們平常的一些部件組合而成 Semantics
=> 語義,用於給無障礙的 ConstrainedBox
=> 限制Drawer的寬度的,以至於 Drawer
不會鋪滿你的螢幕 Material
=> 新增陰影的 咦!聽我這樣解(Hu)釋(Che),是不是對 Drawer
這個部件清晰了不少呀! 所以,其實 Drawer
就是一個普通的 StatelessWidget
,我們完全可以定(Fu)制(Zhi)我們的 Drawer
,比如定製 Drawer
的滑出大小
class SmartDrawer extends StatelessWidget { final double elevation; final Widget child; final String semanticLabel; ///new start final double widthPercent; ///new end const SmartDrawer({ Key key, this.elevation = 16.0, this.child, this.semanticLabel, ///new start this.widthPercent = 0.7, ///new end }) : ///new start assert(widthPercent!=null&&widthPercent<1.0&&widthPercent>0.0) ///new end ,super(key: key); @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); String label = semanticLabel; switch (defaultTargetPlatform) { case TargetPlatform.iOS: label = semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: label = semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel; } ///new start final double _width=MediaQuery.of(context).size.width*widthPercent; ///new end return Semantics( scopesRoute: true, namesRoute: true, explicitChildNodes: true, label: label, child: ConstrainedBox( ///edit start constraints: BoxConstraints.expand(width: _width), ///edit end child: Material( elevation: elevation, child: child, ), ), ); } } 複製程式碼
我這裡將原來的 Drawer
程式碼基礎上修改 _kWidth
的值,把它暴露給使用者自己去定製,讓他能傳入一個 double
型別的寬度百分比,彈出根據螢幕的百分之幾的 Drawer
,該值只允許傳入大於0小於1的值,預設為0.7 下面我們將上面的Drawer改為我們的 SmartDrawer
///edit get _drawer => SmartDrawer( widthPercent: 0.4, ///edit child: ListView( padding: EdgeInsets.zero, children: <Widget>[ DrawerHeader( decoration: BoxDecoration( color: Colors.lightBlueAccent, ), child: Center( child: SizedBox( width: 60.0, height: 60.0, child: CircleAvatar( child: Text('R'), ), ), ), ), ListTile( leading: Icon(Icons.settings), title: Text('設定'), ) ], ), ); 複製程式碼

Drawer
彈出的大小
5.監聽Drawer的彈出和關閉
監聽 Drawer
這裡官方給我們埋了一個坑 監聽我們以 Tab
為例,Flutter會給我我們一個 XXXController
部件,而 Drawer
會不會也會有個 DrawerController
呢?

DrawerController
的,然後我們就將
DrawerController
新增到我們的
_drawer
中去
@override Widget build(BuildContext context) { return Scaffold( appBar: _appbar, ///edit start drawer: DrawerController( child: _drawer, alignment: DrawerAlignment.start, drawerCallback: (isOpen) { print('開啟狀態:$isOpen'); }, ), ); ///edit end } 複製程式碼
我們來執行一下吧

AppBar
中左邊的按鈕是發現,彈出了一個蒙版,
Drawer
並沒有彈出來,這是怎麼回事?別急,我們開啟一下佈局邊界

點選Toggle Debug Paint按鈕

會發現,你的佈局左邊有一條矩形,這個是什麼,我們在左邊矩形區域拖動一下看看

Drawer
出現了,這是什麼回事?為什麼要拖動兩遍才出現,神奇了?
別急,這一切都可以分析 我們先來看看
Scaffold
是怎麼定義
Drawer
的
Scaffold
原始碼

該程式碼比較簡單: 1.先判斷 drawer
是否為空,若不為空新增 drawer
-
_addIfNonNull
該方法從命名可以看出若不為空新增到children裡面 -
這裡被添加了一個
DrawerController
,可知道Flutter寫死了一個DrawerController(這個真的很鬱悶,還不把callback
放出來給使用者) 由此可以點選_drawerOpendCallback
看看做了什麼操作_drawerOpendCallback
部分程式碼:_drawerOpened
,用於
到這裡,我們可以總結: Scaffold
為我們添加了一個 DrawerController
後,我們又添加了一個 DrawerController
導致需要滑動兩次才能顯示我們的 Drawer
,所以,我們可以猜測 DrawerController
就是控制彈出跟關閉的一個部件
那麼,到這裡,我們基本上想要監聽 drawer
的彈出跟關閉就是死路一條了。 要怎樣監聽呢?我們可不可以通過我們定製的 SmartDrawer
去監聽呢? 這裡先做一個埋點,先來看一段程式碼
///edit start class SmartDrawer extends StatefulWidget { ///edit end final double elevation; final Widget child; final String semanticLabel; final double widthPercent; const SmartDrawer({ Key key, this.elevation = 16.0, this.child, this.semanticLabel, this.widthPercent, }): assert(widthPercent < 1.0 && widthPercent > 0.0), super(key: key); ///edit start @override _SmartDrawerState createState() => _SmartDrawerState(); ///edit end } class _SmartDrawerState extends State<SmartDrawer> { ///add start @override void initState() { print('initState'); super.initState(); } @override void dispose() { print('dispose'); super.dispose(); } ///add end ///edit xxx 2width.xxx start @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); String label = widget.semanticLabel; switch (defaultTargetPlatform) { case TargetPlatform.iOS: label = widget.semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: label = widget.semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel; } final double _width = MediaQuery.of(context).size.width * widget.widthPercent; return Semantics( scopesRoute: true, namesRoute: true, explicitChildNodes: true, label: label, child: ConstrainedBox( constraints: BoxConstraints.expand(width: _width), child: Material( elevation: widget.elevation, child: widget.child, ), ), ); } } ///edit xxx 2width.xxx end 複製程式碼
先把 SmartDrawer
的父類由 StatelessWidget
改為 StatefulWidget
,然後新增部件的兩個生命週期(建立和銷燬) 然後繼續熱部署進行使用,正常的開啟和關閉 Drawer

initState
,每次的關閉會觸發
dispose
,這個不就是我們一直想要的
Drawer
開啟和關閉嗎? 於是可以改成這樣:
class SmartDrawer extends StatefulWidget { final double elevation; final Widget child; final String semanticLabel; final double widthPercent; ///add start final DrawerCallback callback; ///add end const SmartDrawer({ Key key, this.elevation = 16.0, this.child, this.semanticLabel, this.widthPercent, ///add start this.callback, ///add end }): assert(widthPercent < 1.0 && widthPercent > 0.0), super(key: key); @override _SmartDrawerState createState() => _SmartDrawerState(); } class _SmartDrawerState extends State<SmartDrawer> { @override void initState() { ///add start if(widget.callback!=null){ widget.callback(true); } ///add end super.initState(); } @override void dispose() { ///add start if(widget.callback!=null){ widget.callback(false); } ///add end super.dispose(); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); String label = widget.semanticLabel; switch (defaultTargetPlatform) { case TargetPlatform.iOS: label = widget.semanticLabel; break; case TargetPlatform.android: case TargetPlatform.fuchsia: label = widget.semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel; } final double _width = MediaQuery.of(context).size.width * widget.widthPercent; return Semantics( scopesRoute: true, namesRoute: true, explicitChildNodes: true, label: label, child: ConstrainedBox( constraints: BoxConstraints.expand(width: _width), child: Material( elevation: widget.elevation, child: widget.child, ), ), ); } } 複製程式碼
現在就可以監聽到 drawer
的打開了,完美!
目前遇到上面的定製問題,本篇文章會繼續更新,請持續關注! 如果這篇文章對你有所幫助,希望能討個贊,謝謝!