1. 簡介
這篇文章主要講解有關drawer的一切。
另:接Flutter相關專案,需要的私信或通過QQ:708959817,聯絡我
2. 初探
我們先來看看簡單的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
,用於 給endDrawer開啟做判斷,emmm....這個不合理吧!
到這裡,我們可以總結:
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 2 width.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 2 width.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
的開啟了,完美!
6.定製彈出Drawer的按鈕
到目前為止,我們使用的drawer
開啟按鈕都是Scaffold
預設給我們新增的,我們可以通過Scaffold
原始碼看到
Scaffold
原始碼:
leading
引數的內容,然後判斷是否為空和是否自動新增leading
,若為空,如果存在Drawer
,Scaffold
會預設給我們新增一個Icon
為Icons.menu
的IconButton
,如果不存在,會判斷是否能返回,如果能返回,就新增返回按鈕。
我們這裡只需要知道,Scaffold
為我們預設新增一個IconButton
現在,我們來看一下預設新增的IconButton
的點選事件onPressed
做了什麼
呼叫Scaffold.of(context).openDrawer()
開啟drawer,所以,我們定製彈出Drawer
按鈕可以如下這樣寫:
//.....
//new start
void _handlerDrawerButton() {
Scaffold.of(context).openDrawer();
}
//new end
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar,
drawer: _drawer,
);
}
get _appbar=>AppBar(
//edit start
leading: IconButton(icon: Icon(Icons.storage), onPressed: _handlerDrawerButton),
//edit end
title: Text('Drawer Test'),
);
//...
複製程式碼
然後就可以通過該按鈕進行點選了,有人可能問,能不能換成其他的按鈕形式,答案是可以的,只要點選事件裡面呼叫的是_handlerDrawerButton()
方法
7.禁止手勢側滑出Drawer
有同學問我如何禁止手勢側滑出Drawer,我們只需要修改一個屬性即可
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar,
drawer: _drawer,
//new start
drawerEdgeDragWidth: 0.0,
//new end
);
}
複製程式碼
目前遇到上面的定製問題,本篇文章會繼續更新,請持續關注! 如果這篇文章對你有所幫助,希望能討個贊,謝謝!