flutter drawer的使用

作者: super_chao | 来源:发表于2019-10-22 16:16 被阅读0次
    1. 简介
      这篇文章主要讲解有关drawer的一切。

    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 => 一个名为"设置"的点击项
    然后我们热部署一下

    Oh,emmm....还是很丑的一个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灰色头部

    1. 定制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呢?

    可以看到,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
      }
    

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

    image.png
    该代码比较简单:
    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

    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源码看到

    可以看到,获取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()方法

    相关文章

      网友评论

        本文标题:flutter drawer的使用

        本文链接:https://www.haomeiwen.com/subject/ffbqcctx.html