概述
Flutter 自带的 showDialog 和 showModalBottomSheet 确实能“把东西弹出来”,但在复杂项目里,它们很快就暴露出三个痛点:
- 动效/遮罩写死,做不到“像原生一样”;
- push/pop、埋点、日志被打散在业务代码里,难维护;
- 想再封装成复用组件?要么复制
ModalRoute,要么手撸OverlayEntry,都很费劲。
于是我们决定自建一套骨架:BaseModalRoute。它专门解决这三个问题,又保留高度扩展性。
1. BaseModalRoute:所有弹窗的“底盘”
- 统一遮罩控制,
ModalBarrierConfig表述颜色/可否点击关闭等。 - 统一生命周期日志,方便排查 push/pop。
- 支持注入自定义
transitionBuilder,默认提供淡入 + 缩放。
typedef ModalTransitionBuilder = Widget Function(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
);
abstract class BaseModalRoute<T> extends ModalRoute<T> {
BaseModalRoute({
required this.barrier,
this.transitionDurationValue = const Duration(milliseconds: 220),
this.reverseTransitionDurationValue,
this.alignment = Alignment.center,
this.forwardCurve = Curves.easeOutCubic,
this.reverseCurve = Curves.easeInCubic,
this.logTag = 'BaseModalRoute',
this.transitionBuilder,
});
// ... existing code ...
Widget _defaultTransitionBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final scaleTween = Tween<double>(begin: 0.85, end: 1);
return FadeTransition(
opacity: animation,
child: ScaleTransition(
alignment: alignment,
scale: scaleTween.animate(animation),
child: child,
),
);
}
}
为什么不用 showDialog 或手撸 Overlay?
-
showDialog不能自定义动效; -
OverlayEntry要自己算层级/销毁; - 每个弹窗都重写
ModalRoute,那还不如集中维护一个基类。
2. ModalTransitionPresets:常见动效信手拈来
- 居中
fadeScale:用在弹窗、Toast。 -
slideFromBottom/slideFromTop/slideFromLeft/slideFromRight:抽成静态方法,任何 route 都可以直接调用。 - 有新动效?往这里加即可,业务不用改。
class ModalTransitionPresets {
const ModalTransitionPresets._();
static ModalTransitionBuilder fadeScale({
Alignment alignment = Alignment.center,
double beginScale = 0.85,
}) { /* ... */ }
static ModalTransitionBuilder slideFromBottom({
double beginOffsetY = 0.2,
bool fade = true,
}) { /* ... */ }
static ModalTransitionBuilder slideFromTop({ /* ... */ })
static ModalTransitionBuilder slideFromLeft({ /* ... */ })
static ModalTransitionBuilder slideFromRight({ /* ... */ })
}
3. DirectionalPopoverRoute:锚点气泡的最终方案
- 利用
GlobalKey精确定位锚点位置,支持anchorInsets缩放矩形,popoverOffset微调。 - 默认动效是方向一致的淡入+滑入。
- 提供
GlobalKey.showDirectionalPopover()扩展,一行代码就能弹。
enum PopoverDirection { top, bottom, left, right }
class DirectionalPopoverRoute extends BaseModalRoute<void> {
DirectionalPopoverRoute({
required this.anchorKey,
required this.direction,
required this.child,
this.gap = 0,
this.margin = EdgeInsets.zero,
this.maxWidth = 260,
this.popoverOffset = Offset.zero,
this.anchorInsets = EdgeInsets.zero,
ModalBarrierConfig barrierConfig = const ModalBarrierConfig(
color: Color(0x00000000), dismissible: true,
),
}) : super(/* ... */);
// ... existing code ...
}
/// 为使用 GlobalKey 的组件提供便捷的锚点弹窗能力。
extension DirectionalPopoverExtension on GlobalKey {
Future<void> showDirectionalPopover({
required BuildContext context,
PopoverDirection direction = PopoverDirection.bottom,
Widget? child,
WidgetBuilder? builder,
double gap = 0,
EdgeInsets margin = EdgeInsets.zero,
double maxWidth = 260,
Offset popoverOffset = Offset.zero,
EdgeInsets anchorInsets = EdgeInsets.zero,
bool useRootNavigator = true,
ModalBarrierConfig barrierConfig = const ModalBarrierConfig(
color: Color(0x00000000), dismissible: true,
),
}) {
// ... existing code ...
}
}
4. 实战演示:几种弹窗如何接入?
页面里,我们通过 BaseModalRoute 子类直接 push,动效与遮罩统一管理。例如:
Future<void> _showAnchorPopover() async {
await _popoverAnchorKey.showDirectionalPopover(
context: context,
direction: PopoverDirection.bottom,
popoverOffset: const Offset(0, -40),
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'筛选提示',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
SizedBox(height: 8),
Text(
'DirectionalPopoverRoute 基于 BaseModalRoute,可根据锚点朝向展示气泡弹窗,适合筛选按钮等场景。',
),
],
),
),
);
}
- 居中信息弹窗:
InfoModalRoute,template + 可插入 builder。 - 确认弹窗:
ConfirmModalRoute,遮罩不可点击关闭,返回 bool。 - 底部操作面板:
BottomSheetModalRoute,自带 slideFromBottom 动效。 - 锚点气泡:
DirectionalPopoverRoute+ extension。
5. 成果总结(及后续扩展方向)
- 统一骨架:遮罩/动画/日志集中维护;跨项目也好迁移。
- 动效预设:新增动效只是扩展
ModalTransitionPresets,不再重复造轮子。 - 锚点精准:
DirectionalPopoverRoute专门做定位和偏移,不用再被 Overlay 坑到。 - 内容自定义:每个 Route 都有
contentBuilder插槽,模板“友好但不束缚”。 - 后续扩展:想新增右下角弹窗、全屏模态,只需新建
Route继承骨架 + 选择动效即可。
换句话说,我们构建的不是“某几个弹窗”,而是一整套可持续迭代的弹窗体系。这套体系让 Flutter 弹窗既能贴近原生体验,又能保持工程级的可维护性,真正实现“写一次,处处弹得对”。