Bootstrap

Flutter简易弹窗

高温限电,疫情防控,一波未平,一波又起。
学习是不可能学习的,只能在居家摸鱼才能勉强维持生活这样子。

Flutter中有集成的弹窗方法,大致是这样:

  void showPopup() {
    showModalBottomSheet(
        context: context,
        shape:
            RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
        builder: (BuildContext context) {
          return Container(
            color: Colors.amber,
            child: Column(
              children: [
                ElevatedButton(onPressed: () {}, child: Text("1")),
                ElevatedButton(onPressed: () {}, child: Text("2")),
              ],
            ),
          );
        });
  }

效果大致如下:
showModalBottomSheet

就是使用showModalBottomSheet或类似的API,但相对来说可定制的参数较少,比如较为重要的位置是难以控制的,而且也不美观。
当然,不自由的组件很难美观起来。

所以想办法自定义一下。
自定义也简单,看看showModalBottomSheet的源码,依葫芦画瓢改改就行,大多自定义API都可以这么干。

而showModalBottomSheet的源码是这样的:

Future<T?> showModalBottomSheet<T>({
  required BuildContext context,
  required WidgetBuilder builder,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder? shape,
  Clip? clipBehavior,
  BoxConstraints? constraints,
  Color? barrierColor,
  bool isScrollControlled = false,
  bool useRootNavigator = false,
  bool isDismissible = true,
  bool enableDrag = true,
  RouteSettings? routeSettings,
  AnimationController? transitionAnimationController,
  Offset? anchorPoint,
}) {
  assert(context != null);
  assert(builder != null);
  assert(isScrollControlled != null);
  assert(useRootNavigator != null);
  assert(isDismissible != null);
  assert(enableDrag != null);
  assert(debugCheckHasMediaQuery(context));
  assert(debugCheckHasMaterialLocalizations(context));

  final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
  return navigator.push(_ModalBottomSheetRoute<T>(
    builder: builder,
    capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
    isScrollControlled: isScrollControlled,
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
    backgroundColor: backgroundColor,
    elevation: elevation,
    shape: shape,
    clipBehavior: clipBehavior,
    constraints: constraints,
    isDismissible: isDismissible,
    modalBarrierColor: barrierColor,
    enableDrag: enableDrag,
    settings: routeSettings,
    transitionAnimationController: transitionAnimationController,
    anchorPoint: anchorPoint,
  ));
}

一下就明白了,弹窗是弹出另一页,自然得另有路由:

navigator.push(_ModalBottomSheetRoute);

接下来抄抄_ModalBottomSheetRoute就行。

class PopupFreeWindow extends PopupRoute {
  ///子组件
  final Widget child;
  ///切换动画时长,必要属性
  final Duration duration;
  ///间隔,用于微调位置
  final EdgeInsets margin;
  ///分布,用于控制大体位置 
  final Alignment alignment;
  ///外围遮罩背景色
  Color? outerBackgroudColor;

  ///子控件具体宽度
  double width;
  ///子控件具体高度
  double height;
  ///宽度比例
  double widthFactor;
  ///高度比例 
  double heightFactor;
  ///是否点击外围收起弹窗
  bool dismissable;

  PopupFreeWindow(
      {required this.child,
      this.duration = const Duration(milliseconds: 300),
      this.alignment = Alignment.bottomCenter,
      this.margin =
          const EdgeInsets.only(bottom: kBottomNavigationBarHeight * 1.5),
      this.widthFactor = 0.95,
      this.heightFactor = 0.3,
      this.width = 0,
      this.height = 0,
      this.dismissable = true});

  @override
  Color? get barrierColor =>
      outerBackgroudColor ?? Colors.black.withOpacity(0.3);

  @override
  bool get barrierDismissible => dismissable;

  @override
  String? get barrierLabel => null;

  @override
  Duration get transitionDuration => duration;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) {
    Widget? content;
    if (width > 0 && height >= 0) {
      content = Container(
        alignment: alignment,
        margin: margin,
        child: Container(
          child: child,
          width: width,
          height: height,
        ),
      );
    } else {
      content = FractionallySizedBox(
          widthFactor: widthFactor,
          heightFactor: heightFactor,
          child: Container(
            child: child,
            margin: margin,
          ),
          alignment: alignment);
    }
    return FadeTransition(
        opacity: animation,
        child: SafeArea(
          child: content,
        ));
  }

}

测试代码:

void showbottom() {
    final size = MediaQuery.of(context).size;
    final width = size.width;
    final height = size.height;
    print("screen w=$width,h=$height");
    Navigator.of(context).push(PopupFreeWindow(
      // widthFactor: 0.95,
      // heightFactor: 0.4,
      height: 200,
      width: width - 30,
      child: ChatBubble(
        direction: ArrowDirection.bottom,
        arrowWidth: 30,
        arrowHeight: 20,
        conicWeight: 4.5,
        child: GridMenu(),
      ),
    ));
  }

根据真实的屏幕大小,来定制弹窗的大小,默认的Alignment为底部,可以调整,默认底部的间隔为状态栏的1.5倍,可以调整,这里这么写是为了结合上一篇博客中的ChatBubble,实现底部悬浮菜单的效果。
实际上都可以自行定义。
效果见下图。
pushRoute
以上。
(继续摸鱼去了~)

;