Flutter进阶:用OverlayEntry 实现所有弹窗效果
2026年4月24日 20:57
一、需求来源
最近遇到一个需求:在直播页面弹窗(Sheet 和 Dialog),因为直播页面比较重,根据路由条件做了进入前台推流和退到后台断流的功能。在 Flutter 中 Sheet 和 Dialog 都通过路由拉起,发生了功能冲突。
只能通过 OverlayEntry 来实现 Sheet 和 Dialog 的效果。所以有 NOverlayDialog,支持 Dialog & Sheet & Drawer & Toast。
// SDK 弹窗拉起部分源码
Navigator.of(context, rootNavigator: useRootNavigator).push
二、使用示例
Dialog
NOverlayDialog.show(
context,
from: v,//v 是 Alignment 类型参数
barrierColor: Colors.black12,
// barrierDismissible: false,
onBarrier: () {
DLog.d('NOverlayDialog onBarrier');
},
child: GestureDetector(
onTap: () {
NOverlayDialog.dismiss();
DLog.d('NOverlayDialog onBarrier');
},
child: Container(
width: 300,
height: 300,
child: buildContent(
title: v.toString(),
onTap: () {
NOverlayDialog.dismiss();
DLog.d('NOverlayDialog onBarrier');
},
),
),
),
);
Sheet
NOverlayDialog.sheet(
context,
child: buildContent(
height: 400,
margin: EdgeInsets.symmetric(horizontal: 30),
onTap: () {
NOverlayDialog.dismiss();
},
),
);
Toast
NOverlayDialog.toast(
context,
hideBarrier: true,
from: Alignment.center,
message: "This is a Toast!",
);
三、源码 NOverlayDialog
//
// NOverlayDialog.dart
// flutter_templet_project
//
// Created by shang on 2026/3/4 18:47.
// Copyright © 2026/3/4 shang. All rights reserved.
//
import 'package:flutter/material.dart';
/// Dialog & Sheet & Drawer & Toast
class NOverlayDialog {
NOverlayDialog._();
static OverlayEntry? _entry;
static AnimationController? _controller;
static bool get isShowing => _entry != null;
/// 隐藏
static Future<void> dismiss({bool immediately = false}) async {
if (!isShowing) {
return;
}
final controller = _controller;
final entry = _entry;
_controller = null;
_entry = null;
if (immediately || controller == null) {
entry?.remove();
controller?.dispose();
return;
}
await controller.reverse();
entry?.remove();
controller.dispose();
}
/// 显示 BottomSheet
static void show(
BuildContext context, {
required Widget child,
Alignment from = Alignment.bottomCenter,
Duration duration = const Duration(milliseconds: 300),
Curve curve = Curves.easeOutCubic,
bool barrierDismissible = true,
Color barrierColor = const Color(0x80000000),
VoidCallback? onBarrier,
bool hideBarrier = false,
Duration? autoDismissDuration,
}) {
if (isShowing) {
dismiss(immediately: true);
}
final overlay = Overlay.of(context, rootOverlay: true);
_controller = AnimationController(
vsync: overlay,
duration: const Duration(milliseconds: 300),
);
final animation = CurvedAnimation(
parent: _controller!,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
Widget content = child;
// ⭐ 中心弹窗:Fade
if (from == Alignment.center) {
content = FadeTransition(
opacity: animation.drive(
CurveTween(curve: Curves.easeOut),
),
child: ScaleTransition(
scale: Tween<double>(begin: 0.9, end: 1.0).animate(animation),
child: content,
),
);
}
// ⭐ 其余方向:Slide
content = FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: Offset(from.x.sign, from.y.sign),
end: Offset.zero,
).chain(
CurveTween(curve: curve),
),
),
child: content,
),
);
content = Align(
alignment: from,
child: content,
);
_entry = OverlayEntry(
builder: (context) {
if (hideBarrier) {
return content;
}
return Stack(
children: [
// ===== Barrier =====
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: barrierDismissible ? dismiss : onBarrier,
child: FadeTransition(
opacity: animation,
child: Container(
color: barrierColor,
),
),
),
content,
],
);
},
);
overlay.insert(_entry!);
_controller?.forward();
if (autoDismissDuration != null) {
Future.delayed(autoDismissDuration, dismiss);
}
}
/// 显示
static void sheet(
BuildContext context, {
required Widget child,
Alignment from = Alignment.bottomCenter,
Duration duration = const Duration(milliseconds: 300),
Curve curve = Curves.easeOutCubic,
bool hideBarrier = false,
Duration? autoDismissDuration,
}) {
return show(
context,
child: child,
from: from,
duration: duration,
curve: curve,
hideBarrier: hideBarrier,
autoDismissDuration: autoDismissDuration,
);
}
/// 显示 BottomSheet
static void toast(
BuildContext context, {
Widget? child,
String message = "",
EdgeInsets margin = const EdgeInsets.only(bottom: 34),
Alignment from = Alignment.center,
Duration duration = const Duration(milliseconds: 300),
Curve curve = Curves.easeOutCubic,
bool hideBarrier = true,
Duration? autoDismissDuration = const Duration(milliseconds: 2000),
}) {
final childDefault = Material(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.all(Radius.circular(8)),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
message,
style: TextStyle(color: Colors.white),
),
),
);
return show(
context,
child: Padding(
padding: margin,
child: child ?? childDefault,
),
from: from,
duration: duration,
curve: curve,
hideBarrier: hideBarrier,
autoDismissDuration: autoDismissDuration,
);
}
}
最后、总结
1、NOverlayDialog 脱离路由系统,基于 OverlayEntry 实现。
2、NOverlayDialog 核心是动画,from 是视图出现和消失方位。
from:Alignment.topCenter,是顶部下拉弹窗 TopSheet;
from:Alignment.bottomCenter,是底部上拉弹窗 BottomSheet;
from:Alignment.centerLeft | Alignment.centerRight, 是两侧弹窗 Drawer;
from:Alignment.center,是渐变弹窗 Dialog & Toast;
3、已添加进