Flutter之drawer详细分析(你要的操作都有)

1. 简介


这篇文章主要讲解有关drawer的一切。

2. 初探


我们先来看看简单的drawer在Flutter的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
///...
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 => 一个名为”设置”的点击项 然后我们热部署一下

Oh,emmm….还是很丑的一个drawer嘢!上面那坨灰色的东西是怎么肥事!不急不急,我们慢慢来分析

3. 解决Drawer灰色头部


因为加了一个DrawerHeader,所以,我们需要看看DrawerHeader里面是什么原因导致添加灰色的地方DrawerHeader源码:

可以看到: Container=>限制高度(默认高度+状态栏高度)BoxDecoration=> 底部添加毫无用处的分割线 AnimatedContainer=>动画版的Container添加默认内边距+顶部状态栏高度的内边距嗯,感觉没错啊,这是怎么肥事,MediaQuery.of(context).padding.top是获取状态栏的高度,然后自身高度加上状态栏的高度,应该是显示蓝色才对,那会不会跟ListView有关系呢?
我们将DrawerHeader去掉看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  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复制为topbottom都为0,该值会被子部件使用,所以可以知道,DrawerHeader使用了该值,导致statusBarHeader为0
mediaQueryVerticalPadding
=>将原有的MediaQuery的padding复制为leftright都为0

所以,我们只要不让ListViewpadding属性为空就可以了,这里我传入一个zero给ListView,然后把DrawerHeader的注释去掉,热部署一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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的滑出大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
///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呢?

可以看到,Flutter是有一个DrawerController的,然后我们就将DrawerController添加到我们的_drawer中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  @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是怎么定义DrawerScaffold源码

该代码比较简单: 1.先判断drawer是否为空,若不为空添加drawer

  1. _addIfNonNull该方法从命名可以看出若不为空添加到children里面

  2. 这里被添加了一个DrawerController,可知道Flutter写死了一个DrawerController(这个真的很郁闷,还不把callback放出来给用户)
    由此可以点击_drawerOpendCallback看看做了什么操作
    _drawerOpendCallback部分代码:

    这里将值给了_drawerOpened,用于

    给endDrawer打开做判断,emmm….这个不合理吧!

到这里,我们可以总结:Scaffold为我们添加了一个DrawerController后,我们又添加了一个DrawerController导致需要滑动两次才能显示我们的Drawer,所以,我们可以猜测DrawerController就是控制弹出跟关闭的一个部件

那么,到这里,我们基本上想要监听drawer的弹出跟关闭就是死路一条了。
要怎样监听呢?我们可不可以通过我们定制的SmartDrawer去监听呢?
这里先做一个埋点,先来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
///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打开和关闭吗?于是可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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的打开了,完美!

如果这篇文章对你有所帮助,希望求个打赏,谢谢!

转自掘金

-------------本文结束感谢您的阅读-------------
如果你喜欢这篇文章,可以请我喝一杯 Coffee ~