這篇文章主要講解有關drawer的一切。android
另:接Flutter相關項目,須要的私信或經過QQ:708959817,聯繫我markdown
咱們先來看看簡單的drawer
在Flutter的應用app
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'),
);
}
複製代碼
而後運行一下項目: 以下圖所示 less
能夠看到,根據咱們對drawer
的認識,並非想要的結果,因此這個drawer
並不完整,而後咱們繼續添加代碼,修改drawer
ide
///...
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
嘢!上面那坨灰色的東西是怎麼肥事!不急不急,咱們慢慢來分析
由於加了一個DrawerHeader
,因此,咱們須要看看DrawerHeader
裏面是什麼緣由致使添加灰色的地方 DrawerHeader
源碼: 動畫
能夠看到: Container
=>限制高度(默認高度+狀態欄高度) BoxDecoration
=> 底部添加毫無用處的分割線 AnimatedContainer
=>動畫版的Container
添加默認內邊距+頂部狀態欄高度的內邊距 嗯,感受沒錯啊,這是怎麼肥事,MediaQuery.of(context).padding.top
是獲取狀態欄的高度,而後自身高度加上狀態欄的高度,應該是顯示藍色纔對,那會不會跟ListView
有關係呢? 咱們將DrawerHeader
去掉看看ui
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
this
3.sliver
添加一層MediaQuery
,這個代表sliver
的子部件會使用該MediaQuery
的值,根據判斷,子部件會使用mediaQueryHorizontalPadding
,而上面的兩個複製:spa
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灰色頭部
咱們來看看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
彈出的大小
監聽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
的打開了,完美!
到目前爲止,咱們使用的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()
方法
有同窗問我如何禁止手勢側滑出Drawer,咱們只須要修改一個屬性便可
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar,
drawer: _drawer,
//new start
drawerEdgeDragWidth: 0.0,
//new end
);
}
複製代碼
目前遇到上面的定製問題,本篇文章會繼續更新,請持續關注! 若是這篇文章對你有所幫助,但願能討個贊,謝謝!