Flutter入门——设计实现一个自定义Widget的弹出工具

2,542 阅读4分钟

介绍

Flutter中的Dialog和Drawer的弹出本质上与push一个新的页面是相同的,只是他们的route在构建时,有层次和装饰性的区别。

如,页面常用的有:

MaterialPageRoute、CupertinoPageRoute

对话框的则是:

_DialogRoute

下面我们通过PageRouteBuilder实现一个自定义widget的弹出功能。

系统提供的showDialog()也可以应付大多数需求

此功能已加入Bedrock框架

Bedrock - Mvvm+Provider快速开发脚手架

实现

我们想要弹出自己的widget,需要用PageRouteBuilder 构建我们的route,并通过Navigator.of(context).push()方法进行显示。

PageRouteBuilder extends PageRoute extends ...
如果你不需要路由page route相关的功能
可以继承overlayRoute等实现自己的builder

PageRouteBuilder

它的构造函数

  PageRouteBuilder({
    RouteSettings settings,//route相关配置,例如传参
    @required this.pageBuilder,//返回你的widget
    //自定义过渡builder
    this.transitionsBuilder = _defaultTransitionsBuilder,
    //过渡时间(例如:弹窗的阴影在进入和退出时所消耗的时间)
    //你可以把它调大来观察区别
    this.transitionDuration = const Duration(milliseconds: 300),
    //上个route是否可见
    this.opaque = true,
    //点击背景是否弹出
    this.barrierDismissible = false,
    this.barrierColor,//背景颜色
    this.barrierLabel,//背景标签
    this.maintainState = true,//是否保持状态
    bool fullscreenDialog = false,//全屏
  })

我们通过pageBuilder返回我们要弹出的widget即可,而为了进一步将它工具化,我们在页面的基类中(bedrock框架中的pageState)增加一个函数:

  ///弹出自定义widget(效果类似dialog)
  ///你可以调整你的widget来达到预期的表现效果
  ///也可以通过PageRouteBuilder的参数进行调整
  void floatWidget(Widget child,{
    ///弹出层的退出由此参数控制
    ///默认值Navigator.pop(ctx),或自定义
    FloatWidgetDismiss floatWidgetDismiss,
    bool barrierDismissible = false,
    ///浮层背景色
    Color bgColor = const Color.fromRGBO(34, 34, 34, 0.3),
    ///浮层对齐方式
    Alignment alignment = Alignment.center,
    ///页面结束和路由操作完成的回调
    Function afterPop,Function onComplete,
    ///页面进入/退出时间
    Duration transitionDuration = const Duration(milliseconds: 300),
    ///新版本 此参数已作废
    //Duration reverseTransitionDuration = const Duration(milliseconds: 300),
  }){
    Navigator.of(context).push(
        PageRouteBuilder(
          transitionDuration: transitionDuration,
        ///新版本无此参数
        //reverseTransitionDuration: reverseTransitionDuration,
        opaque: false,
        pageBuilder:(ctx,animation,secondAnimation){
        //自定义widget的root container
          return FloatContainerWidget(child,
              floatWidgetDismiss: floatWidgetDismiss??(ctx)=>Navigator.pop(ctx),
              barrierDismissible:barrierDismissible ,bgColor: bgColor,alignment: alignment).generateWidget();
        }))
        .then((value) => afterPop??(){})
        .whenComplete(() => onComplete??(){});
  }

可以看到,函数的参数最终都传给了一个FloatContainerWidget,我们来看一下他的实现。

FloatContainerWidget

其内部代码很简单,主要是对一些弹窗所共有的特性进行统一以及后续可能出现的修改提供便捷性,你甚至可以不用这个直接在上节中 return 你自己的child。

代码实现:

typedef FloatWidgetDismiss = void Function(BuildContext context);


class FloatContainerWidget extends WidgetState{
  ///背景颜色
  final Color bgColor;
  final Widget child;
  ///对齐方式
  final Alignment alignment;
  ///是否点击背景可以退出
  final bool barrierDismissible;
  ///‘pop’ 退出 外置
  ///例如,我们需要做一个动画后再弹出。
  final FloatWidgetDismiss floatWidgetDismiss;

  FloatContainerWidget(this.child,{@required this.floatWidgetDismiss,this.bgColor,this.alignment,this.barrierDismissible})
    :assert(child != null),assert(floatWidgetDismiss != null);


  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    return GestureDetector(
      onTap: (){
        //点击背景弹出
        if(barrierDismissible){
          floatWidgetDismiss(context);
        }
      },
      child: Container(
        color: bgColor,
        width: size.width,height: size.height,
        alignment:alignment,
        child:child,
      ),
    );
  }

}

至此工具就基本实现了,我们看一下如何使用它。

底部弹出一个Drawer

我们定义一个可以从底部滑出的Drawer:

class CustomBottomDrawerWidget extends WidgetState with SingleTickerProviderStateMixin{
  ///动画
  AnimationController animationController;
  Animation animation;
  ///drawer的bottom值
  ///初始值使drawer在屏幕外面
  double bottom = -600;

  @override
  void initState() {
    animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
    animation =Tween<double>(begin: bottom,end: 0).animate(animationController);
    super.initState();
    animationController.addListener(() {
    ///更改bottom的值实现drawer的滑动效果
      setState(() {
        bottom = animation.value;
      });
    });

    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      animationController.forward();
    });
  }
  @override
  void dispose() {
    animationController.dispose();
    // TODO: implement dispose
    super.dispose();
  }



  @override
  Widget build(BuildContext context) {
    ///基本的布局
    return Stack(
      alignment: Alignment.bottomCenter,
      children: [
        Positioned(
          bottom: getHeightPx(bottom),
          child: Material(
            color: const Color.fromRGBO(0, 0, 0, 0),
            child: Container(
              width: getWidthPx(750),height: getHeightPx(600),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(topLeft: Radius.circular(getHeightPx(16)),topRight: Radius.circular(getHeightPx(16))),
              ),
              alignment: Alignment.center,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('this is a drawer.\n slide from bottom',style: TextStyle(
                      color: Colors.black,fontSize: getSp(32)
                  ),),
                  getSizeBox(height: getWidthPx(70)),
                  RaisedButton(
                    onPressed: (){
                      animationController.reverse().then((value) => Navigator.pop(context));
                    },
                    child: Text('close',style: TextStyle(color: Colors.black,fontSize: getSp(30)),),
                  ),
                ],
              ),
            ),
          ),
        )
      ],
    );
  }

}

实现好drawer后,我们在需要弹出drawer的地方(页面),调用咱们上面设计好的函数:

			///抽屉
            final CustomBottomDrawerWidget bottomDrawerWidget = CustomBottomDrawerWidget();
            ///页面基类 封装的方法
            floatWidget(
                bottomDrawerWidget.generateWidget(),
                ///一般情况下可以不用传入这个参数
                ///不过drawer的情况下,我们需要drawer时滑出的
                ///而非直接消失。
                
                floatWidgetDismiss: (ctx){
                ///所以我们需要等drawer动画结束后
                ///再pop
                bottomDrawerWidget.animationController?.reverse()
                  ?.whenComplete(() => Navigator.pop(ctx));
                },
                barrierDismissible: true,

至此大功告成,谢谢大家阅读。

如果有不完善的地方,欢迎指出,非常感谢。

更复杂的Drawer

如果需要使用响应手势的drawer,可以参考下面的文章。

网易云音乐

仿高德三级联动Drawer

Bedrock

Bedrock - Mvvm+Provider快速开发脚手架