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

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'),
  );
}


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


image.png

image.png

可以看到,根据我们对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 => 一个名为"设置"的点击项
然后我们热部署一下

image.png

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

3 . 解决Drawer灰色头部

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

image.png

可以看到:
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('设置'),
            )
          ],
        ),
      );

image.png

确实,跟ListView有关,这是什么原因导致ListView加上一个statusBarHeight大小的内边距呢?我们可以继续找ListView的源码
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: <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('设置'),
            )
          ],
        ),
      );
image.png

ok,我们成功解决了Drawer灰色头部

4. 定制Drawer的滑出大小

我们来看看drawer的源码,其实看源码并不是一件痛苦的事,我们一般直接跳到build方法就好

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: <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('设置'),
            )
          ],
        ),
      );

image.png

可以看到,我们成功的修改了Drawer弹出的大小

5.监听Drawer的弹出和关闭

监听Drawer这里官方给我们埋了一个坑
监听我们以Tab为例,Flutter会给我我们一个XXXController部件,而Drawer会不会也会有个DrawerController呢?

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
  }

我们来运行一下吧

image.png

当我点击AppBar中左边的按钮是发现,弹出了一个蒙版,Drawer并没有弹出来,这是怎么回事?别急,我们开启一下布局边界
image.png

点击Toggle Debug Paint按钮
image.png

会发现,你的布局左边有一条矩形,这个是什么,我们在左边矩形区域拖动一下看看
image.png

诶!我们的Drawer出现了,这是什么回事?为什么要拖动两遍才出现,神奇了?别急,这一切都可以分析
我们先来看看Scaffold是怎么定义Drawer
Scaffold源码
image.png

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

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

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

    image.png

    这里将值给了_drawerOpened,用于
    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<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

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<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源码:

image.png

可以看到,获取leading参数的内容,然后判断是否为空和是否自动添加leading,若为空,如果存在DrawerScaffold会默认给我们添加一个IconIcons.menuIconButton,如果不存在,会判断是否能返回,如果能返回,就添加返回按钮。
我们这里只需要知道,Scaffold为我们默认添加一个IconButton
现在,我们来看一下默认添加的IconButton的点击事件onPressed做了什么
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
    );
  }

目前遇到上面的定制问题,本篇文章会继续更新,请持续关注!
如果这篇文章对你有所帮助,希望能讨个赞,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容