Flutter进阶:用OverlayEntry 实现所有弹窗效果

一、需求来源

最近遇到一个需求:在直播页面弹窗(Sheet 和 Dialog),因为直播页面比较重,根据路由条件做了进入前台推流和退到后台断流的功能。在 Flutter 中 Sheet 和 Dialog 都通过路由拉起,发生了功能冲突。

只能通过 OverlayEntry 来实现 Sheet 和 Dialog 的效果。所以有 NOverlayDialog,支持 Dialog & Sheet & Drawer & Toast。

dart 复制代码
// SDK 弹窗拉起部分源码
Navigator.of(context, rootNavigator: useRootNavigator).push

二、使用示例

Dialog

dart 复制代码
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

dart 复制代码
NOverlayDialog.sheet(
  context,
  child: buildContent(
    height: 400,
    margin: EdgeInsets.symmetric(horizontal: 30),
    onTap: () {
      NOverlayDialog.dismiss();
    },
  ),
);

Toast

dart 复制代码
NOverlayDialog.toast(
  context,
  hideBarrier: true,
  from: Alignment.center,
  message: "This is a Toast!",
);

三、源码 NOverlayDialog

dart 复制代码
//
//  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 是视图出现和消失方位。

dart 复制代码
from:Alignment.topCenter,是顶部下拉弹窗 TopSheet;
from:Alignment.bottomCenter,是底部上拉弹窗 BottomSheet;
from:Alignment.centerLeft | Alignment.centerRight, 是两侧弹窗 Drawer;
from:Alignment.center,是渐变弹窗 Dialog & Toast;

3、已添加进

pub.dev/packages/n_...

相关推荐
IT_陈寒3 小时前
Vite静态资源加载把我坑惨了
前端·人工智能·后端
herinspace3 小时前
管家婆实用贴-如何分离和附加数据库
开发语言·前端·javascript·数据库·语音识别
小码哥_常4 小时前
从MVC到MVI:一文吃透架构模式进化史
前端
嗷o嗷o4 小时前
Android BLE 的 notify 和 indicate 到底有什么区别
前端
豹哥学前端4 小时前
别再背“var 提升,let/const 不提升”了:揭开暂时性死区的真实面目
前端·面试
lar_slw4 小时前
k8s部署前端项目
前端·容器·kubernetes
拉拉肥_King5 小时前
Ant Design Table 横向滚动条神秘消失?我是如何一步步找到真凶的
前端·javascript
GreenTea5 小时前
DeepSeek-V4 技术报告深度分析:基础研究创新全景
前端·人工智能·后端
河阿里5 小时前
HTML5标准完全教学手册
前端·html·html5