• 背景:我所开发的利用是一个点餐的平板利用,有着大量从右边或左边关上 drawer 的场景,最终实现的成果如上图所示

    默认的 drawer 组件

  • flutter 默认的 drawer 是集成在 Scaffold 组件上的,简略代码示例如下:

    Scaffold(  drawer: Widget, // 从右边弹起一个抽屉  endDrawer: Widget, // 从左边弹起一个抽屉);
  • 敞开该抽屉可应用 Navigator.pop(context); ,从此能够看出关上 Drawer 其实是关上了一个新的路由页面
  • 除了应用上述 Navigator 的形式敞开抽屉,还有上面两种办法参考自这里,这两种办法的原理都是通过获取 ScaffoldState 对象而后调用其外部的 open、close 办法进行操作,上面是代码演示

    1. 查找父级最近的 Scaffold 对应的 ScaffoldState 对象

      ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;// 关上抽屉菜单_state.openDrawer();// 或者间接应用 Scaffold.of(contenxt)在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是心愿暴露出的,该当在 StatefulWidget 中提供一个`of` 静态方法来获取其 State 对象,开发者便可间接通过该办法来获取;如果 State不心愿裸露,则不提供`of`办法
    2. 借助 GlobalKey 来获取 ScaffoldState 对象(我上面的自定义 drawer 就是借助这种形式),代码演示如下:

      // 定义一个globalKey, 因为GlobalKey要放弃全局唯一性,咱们应用动态变量存储static GlobalKey<ScaffoldState> _globalKey= GlobalKey();Scaffold(    key: _globalKey , //设置key    ...  )// 而后就能够这样关上 drawer 了 _globalKey.currentState.openDrawer()
  • 下面的默认 drawer 应用形式介绍完了,很容易发现这种形式必须依赖 Scaffold,但通常一个路由页面只有一个 Scaffold 而且是在最外层,况且他只能接管一个 drawer (当然能够通过条件判断来展现多个 drawer),如果是页面中关上 drawer 的场景特地多的话,应用起来就会特地麻烦,所以我写了一个自定义的 drawer 组件


自定义 Drawer - RDrawer

  1. 基本思路是参考这篇文章,从下面的剖析中咱们得出关上一个 drawer 其实就是关上一个新的路由页面(页面背景是通明的能够看到上一个页面的内容,flutter 外面的 showDialog, bottomSheet 都是这种解决)
  2. 先说一下 RDrawer 的应用办法

    // 关上drawerElevatedButton(onPressed: () => RDrawer.open(Widget child));// 敞开drawerElevatedButton(onPressed: () => RDrawer.close());// 此处 child 组件可依据本人的 UI 图封装一个蕴含 title、body、footer 的 DrawerBody 组件,让外界更方便使用// 关上和敞开动作能够在任意中央应用不用依赖 Scaffold 

实现思路

  • 剖析: 关上、敞开路由页面,抽屉关上、敞开时的动画(如果不思考抽屉动画就会变得非常简单和 showDialog 没啥两样)
  • 关上一个新的路由页面次要依赖这个 Widget PageRouteBuilder,从 chatgpt 上晓得他有这么多属性(感叹一下 chatgpt 真是一个神器呀)

    Flutter 的 `PageRouteBuilder` 是一个用于自定义页面过渡动画的小部件,它能够让开发者依据本人的需要创立各种自定义过渡动画。以下是 `PageRouteBuilder` 中可用的参数:-   `pageBuilder`: 必须提供一个 `WidgetBuilder` 函数,用于构建将要过渡到的页面。-   `transitionDuration`: 定义页面过渡的持续时间,类型为 `Duration`。-   `reverseTransitionDuration`: 定义页面返回时的过渡持续时间,类型为 `Duration`。-   `transitionsBuilder`: 定义过渡动画的形式,承受一个 `Widget` 和一个 `Animation<double>` 参数,返回一个 `Widget`。-   `opaque`: 定义页面是否不通明,默认值为 `true`。-   `barrierDismissible`: 定义点击遮罩区域是否能够敞开页面,默认值为 `false`。-   `barrierColor`: 定义遮罩区域的色彩,默认值为半透明彩色。-   `barrierLabel`: 定义遮罩区域的语义标签,默认值为 `null`。-   `maintainState`: 定义页面是否放弃在内存中,默认值为 `true`。-   `fullscreenDialog`: 定义页面是否是全屏对话框,默认值为 `false`。这些参数能够帮忙开发者创立各种自定义过渡动画,并管制页面过渡的各个方面,例如过渡工夫、透明度、遮罩等。
  • 关上一个新的页面 Navigator.of(Get.context!).push(PageRouteBuilder(...))
  • 敞开一个新的新页面 Navigator.pop(context);
  • 其实如果不思考抽屉动画当初的工作曾经实现了,但 drawer 怎么可能没有动画,动画借助 AnimateBuilder 实现,利用 Tween 自定义一个动画

    @overridevoid initState() {  super.initState();  controller = AnimationController(    duration: const Duration(milliseconds: 300),    vsync: this,  );  // drawer 宽度为 563,动画是借助 Stack 让其从 -563 的地位到 0  animation = Tween<double>(begin: -563, end: 0).animate(    CurvedAnimation(parent: controller, curve: Curves.easeInOut),  );  controller.forward();}
  • 动画的启动机会是 initState 时这个不须要非凡解决,drawer敞开机会却要须要等动画实现时在执行Navigator.pop(context); 这个中央解决就麻烦一点,大略思路是这样

    void close() {  controller.reverse().then((value) {    Navigator.pop(context);  });}

    然而这个 close 办法是写在 DrawerState 对象外面外界无法访问到,这时候就须要借助上文提到的 GlobalKey了,来让外界能拜访到 close 办法,大略代码如下:

    // 创立一个类型为 DrawerState 的 GlobalKeystatic final GlobalKey<DrawerState> drawerStateKey = GlobalKey<DrawerState>();/// 关上 drawerstatic open(Widget child) {  Navigator.of(Get.context!).push(    PageRouteBuilder(      // ... 参数省略      pageBuilder: (_, __, ___) => RDrawer(key: drawerStateKey, child: child),    ),  );}/// 通过 drawerStateKey 来敞开 drawerstatic close() => drawerStateKey.currentState?.close();

    残缺代码如下

    enum DrawerDirEnum { left, right }/// Drawer 外围组件class RDrawer extends StatefulWidget {  /// drawer宽度  final double width;  /// 开展方向  final DrawerDirEnum dir;  /// 点击遮罩层是否容许敞开  final bool? maskClose;  /// drawer 内容,此处可在封装一个 DrawerBody 来定制本人的款式,更方便使用  final Widget child;  const RDrawer({    super.key,    required this.child,    this.width = 536,    this.dir = DrawerDirEnum.right,    this.maskClose = false,  });  // 定义用于拜访 state 对象的 key  static final GlobalKey<DrawerState> drawerStateKey = GlobalKey<DrawerState>();  /// 关上 drawer  static open(    Widget child, {    DrawerDirEnum? dir = DrawerDirEnum.right,    double? width = 536,    bool? maskClose = false,  }) {    Navigator.of(Get.context!).push(      // 具体参数含意上文已介绍过      PageRouteBuilder(        opaque: false,        transitionDuration: const Duration(milliseconds: 300),        barrierColor: const Color.fromRGBO(0, 0, 0, 0.7),        fullscreenDialog: true,        pageBuilder: (_, __, ___) => RDrawer(          // 很重要:绑定 globalKey 以是 DrawerState 能被外界拜访到          key: drawerStateKey,          width: width!,          dir: dir!,          maskClose: maskClose!,          child: child,        ),      ),    );  }  /// 敞开 drawer  static close() => drawerStateKey.currentState?.close();  @override  State<RDrawer> createState() => DrawerState();}/// Drawer 外围逻辑class DrawerState extends State<RDrawer> with SingleTickerProviderStateMixin {  late AnimationController controller;  late Animation<double> animation;  @override  void initState() {    super.initState();    controller = AnimationController(      duration: const Duration(milliseconds: 300),      vsync: this,    );    animation = Tween<double>(begin: -widget.width, end: 0).animate(      CurvedAnimation(parent: controller, curve: Curves.easeInOut),    );    controller.forward();  }  /// 敞开 drawer  void close() {    // 待抽屉动画实现后在敞开页面    controller.reverse().then((value) {      Navigator.pop(context);    });  }  @override  Widget build(BuildContext context) {    return Stack(      children: [        // 为了实现 drawer 敞开动画不能间接借助 barrierDismissible  来管制点击遮罩层        GestureDetector(onTap: () => widget.maskClose! ? close() : null),        AnimatedBuilder(          animation: animation,          builder: (BuildContext context, Widget? child) {            return Positioned(              top: 0,              bottom: 0,              right: widget.dir == DrawerDirEnum.right ? animation.value : null,              left: widget.dir == DrawerDirEnum.left ? animation.value : null,              child: SizedBox(width: widget.width, child: widget.child),            );          },        ),      ],    );  }  @override  void dispose() {    controller.dispose();    super.dispose();  }}