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

1. 简介

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

另:接Flutter相关项目,须要的私信或经过QQ:708959817,联系我markdown

2. 初探

咱们先来看看简单的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

image.png
image.png

能够看到,根据咱们对drawer的认识,并非想要的结果,因此这个drawer并不完整,而后咱们继续添加代码,修改draweride

///...

  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去掉看看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('设置'),
            )
          ],
        ),
      );
复制代码

image.png
确实,跟 ListView有关,这是什么缘由致使 ListView加上一个 statusBarHeight大小的内边距呢?咱们能够继续找 ListView的源码
image.png
能够直接点击 ListView的构造方法,跳转到455行可看到 1.当 ListView的属性 padding为空时,获取 MediaQueryData的信息

2.由于ListView的滚动方向默认为垂直,会使用mediaQueryVerticalPaddingthis

3.sliver添加一层MediaQuery,这个代表sliver的子部件会使用该MediaQuery的值,根据判断,子部件会使用mediaQueryHorizontalPadding,而上面的两个复制:spa

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是怎么定义 DrawerScaffold源码
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
    );
  }
复制代码

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

相关文章
相关标签/搜索