普通视图

发现新文章,点击刷新页面。
昨天以前首页

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

作者 SoaringHeart
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、已添加进

pub.dev/packages/n_…

Flutter调试组件:打印任意组件尺寸位置信息 NRenderBox

作者 SoaringHeart
2026年2月28日 22:20

一、需求来源

当页面元素特别多,比较杂,又必须获取某个组件尺寸位置时,一个个加 GlobalKey 有太麻烦,这是使用一个封装好的组件就特别有用了。然后就有了 NRenderBox 组件,可以打印出子组件的位置及尺寸。

二、使用

NRenderBox(
  child: Container(
    padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(
      color: Colors.transparent,
      border: Border.all(color: Colors.blue),
      borderRadius: BorderRadius.all(Radius.circular(0)),
    ),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        NNetworkImage(
          width: 50,
          height: 60,
          url: AppRes.image.urls.random ?? '',
        ),
        Text("选项"),
      ],
    ),
  ),
)
flutter: NRenderBox rect: Rect.fromLTRB(88.5, 322.0, 157.5, 413.0)

三、NRenderBox源码

import 'package:flutter/material.dart';

/// 点击打印尺寸
class NRenderBox extends StatefulWidget {
  const NRenderBox({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<NRenderBox> createState() => _NRenderBoxState();
}

class _NRenderBoxState extends State<NRenderBox> {
  final renderKey = GlobalKey();

  RenderBox? get renderBox {
    final ctx = renderKey.currentContext;
    if (ctx == null) {
      return null;
    }
    final box = ctx.findRenderObject() as RenderBox?;
    return box;
  }

  Offset? get renderPosition {
    return renderBox?.localToGlobal(Offset.zero);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      key: renderKey,
      onTap: () {
        if (renderBox == null) {
          return;
        }
        final position = renderPosition;
        final size = renderBox!.size;
        final rect = Rect.fromLTWH(position!.dx, position.dy, size.width, size.height);
        debugPrint("$widget rect: $rect");
      },
      child: widget.child,
    );
  }
}

github

❌
❌