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

1. 简介

这篇文章主要讲解有关drawer的一切。
另:接Flutter相关项目,需要的私信或通过QQ:708959817,联系我
2. 初探 我们先来看看简单的drawer在Flutter的应用
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); }class _HomePageState extends State { @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'), ); }

然后运行一下项目:
如下图所示

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png 可以看到,根据我们对drawer的认识,并不是想要的结果,所以这个drawer并不完整,然后我们继续添加代码,修改drawer
///...get _drawer => Drawer( ///edit start child: ListView( children: [ 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 => 一个名为"设置"的点击项
然后我们热部署一下

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
Oh,emmm....还是很丑的一个 drawer嘢!上面那坨灰色的东西是怎么肥事!不急不急,我们慢慢来分析
3 . 解决Drawer灰色头部 因为加了一个DrawerHeader,所以,我们需要看看DrawerHeader里面是什么原因导致添加灰色的地方
DrawerHeader源码:

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以看到:
Container=>限制高度(默认高度+状态栏高度)
BoxDecoration=> 底部添加毫无用处的分割线
AnimatedContainer =>动画版的Container添加默认内边距+顶部状态栏高度的内边距
嗯,感觉没错啊,这是怎么肥事,MediaQuery.of(context).padding.top是获取状态栏的高度,然后自身高度加上状态栏的高度,应该是显示蓝色才对,那会不会跟ListView有关系呢?
我们将DrawerHeader去掉看看
get _drawer => Drawer( child: ListView( children: [ ///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('设置'), ) ], ), );


Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
确实,跟 ListView有关,这是什么原因导致 ListView加上一个 statusBarHeight大小的内边距呢?我们可以继续找 ListView的源码
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以直接点击 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的注释去掉,热部署一下
get _drawer => Drawer( child: ListView( ///edit start padding: EdgeInsets.zero, ///edit end children: [ 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('设置'), ) ], ), );

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
ok,我们成功解决了Drawer灰色头部
4. 定制Drawer的滑出大小 我们来看看drawer的源码,其实看源码并不是一件痛苦的事,我们一般直接跳到build方法就好

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以看到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: [ 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('设置'), ) ], ), );


Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以看到,我们成功的修改了 Drawer弹出的大小
5.监听Drawer的弹出和关闭 监听Drawer这里官方给我们埋了一个坑
监听我们以Tab为例,Flutter会给我我们一个XXXController部件,而Drawer会不会也会有个DrawerController呢?

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以看到,Flutter是有一个 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 }

我们来运行一下吧

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
当我点击 AppBar中左边的按钮是发现,弹出了一个蒙版, Drawer并没有弹出来,这是怎么回事?别急,我们开启一下布局边界
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
点击Toggle Debug Paint按钮
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
会发现,你的布局左边有一条矩形,这个是什么,我们在左边矩形区域拖动一下看看
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
诶!我们的 Drawer出现了,这是什么回事?为什么要拖动两遍才出现,神奇了? 别急,这一切都可以分析
我们先来看看 Scaffold是怎么定义 Drawer
Scaffold源码
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
该代码比较简单:
1.先判断drawer是否为空,若不为空添加drawer
  1. _addIfNonNull该方法从命名可以看出若不为空添加到children里面
  2. 这里被添加了一个DrawerController,可知道Flutter写死了一个DrawerController(这个真的很郁闷,还不把callback放出来给用户)
    由此可以点击_drawerOpendCallback看看做了什么操作
    _drawerOpendCallback部分代码:

    Flutter之drawer详细分析(你要的操作都有)
    文章图片
    image.png
    这里将值给了_drawerOpened,用于
    Flutter之drawer详细分析(你要的操作都有)
    文章图片
    image.png
    给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 {///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

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
诶,可以看到,每次的打开会触发 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 {@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源码:

Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
可以看到,获取 leading参数的内容,然后判断是否为空和是否自动添加 leading,若为空,如果存在 DrawerScaffold会默认给我们添加一个 IconIcons.menuIconButton,如果不存在,会判断是否能返回,如果能返回,就添加返回按钮。
我们这里只需要知道,Scaffold为我们默认添加一个IconButton
现在,我们来看一下默认添加的 IconButton的点击事件 onPressed做了什么
Flutter之drawer详细分析(你要的操作都有)
文章图片
image.png
调用 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 ); }

【Flutter之drawer详细分析(你要的操作都有)】目前遇到上面的定制问题,本篇文章会继续更新,请持续关注!
如果这篇文章对你有所帮助,希望能讨个赞,谢谢!

    推荐阅读