Flutter最佳实践:路由弹窗终极版NSlidePopupRoute

一、需求来源

最近需要实现弹窗,动画方向分为:

  1. 从屏幕顶部滑动到屏幕内,top->Center。
  2. 从屏幕底部滑动到屏幕内,bottom->Center。
  3. 从屏幕左侧滑动到屏幕内,left->Center。
  4. 从屏幕右侧滑动到屏幕内,right->Center。
  5. 直接显示在屏幕中间,Center->Center。

最终实现以 Alignment 参数为动画方向,弹窗内容高度和宽度自定义,实现从哪个方向弹出就从哪个方向消失的高自由度。彻底突破了 ModalBottomSheet 的方向显示。 效果图如下:

二、使用示例

less 复制代码
import 'package:flutter/material.dart';
import 'package:n_slide_popup/n_slide_popup.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Alignment> alignments = [    Alignment.topLeft,    Alignment.topCenter,    Alignment.topRight,    Alignment.centerLeft,    Alignment.center,    Alignment.centerRight,    Alignment.bottomLeft,    Alignment.bottomCenter,    Alignment.bottomRight,  ];

  var alignment = Alignment.center;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text("direction from Alignment."),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ...alignments.map((e) {
                var name = e.toString().split('.')[1];
                return ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    elevation: 0,
                    tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                    minimumSize: const Size(64, 36),
                  ),
                  onPressed: () {
                    alignment = e;
                    debugPrint("$alignment ${alignment.x} ${alignment.y}");
                    onPopupRoute();
                  },
                  child: Text(
                    name,
                    style: TextStyle(color: Colors.white),
                  ),
                );
              }),
            ],
          )
        ],
      ),
    );
  }

  Future<void> onPopupRoute() async {
    final route = NSlidePopupRoute(
      from: alignment,
      builder: (_) {
        return buildPopupView(alignment: alignment, argsDismiss: {"b": "88"});
      },
    );
    final result = await Navigator.of(context).push(route);
    print(["result", result.runtimeType, result]);
  }

  Widget buildPopupView({required Alignment alignment, Map<String, dynamic>? argsDismiss}) {
    return Align(
      alignment: alignment,
      child: Container(
        width: 300,
        height: 400,
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: Colors.green,
          border: Border.all(color: Colors.blue),
          borderRadius: BorderRadius.all(Radius.circular(0)),
        ),
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop(argsDismiss);
          },
          child: Text("dismiss"),
        ),
      ),
    );
  }
}

三、源码

php 复制代码
//
//  NSlidePopupRoute.dart
//  n_slide_popup
//
//  Created by shang on 2025/12/27.
//  Copyright © 2025/12/27 shang. All rights reserved.
//


import 'package:flutter/material.dart';

/// 最新滑入弹窗
class NSlidePopupRoute<T> extends PopupRoute<T> {
  NSlidePopupRoute({
    super.settings,
    required this.builder,
    this.from = Alignment.bottomCenter,
    this.barrierColor = const Color(0x80000000),
    this.barrierDismissible = true,
    this.duration = const Duration(milliseconds: 300),
    this.barrierLabel,
    this.curve = Curves.easeOutCubic,
  });

  final WidgetBuilder builder;

  /// 从哪个方向进入(推荐:topCenter / bottomCenter / centerLeft / centerRight)
  final Alignment from;

  final Duration duration;

  final Curve curve;

  @override
  final bool barrierDismissible;

  @override
  final Color barrierColor;

  @override
  final String? barrierLabel;

  @override
  Duration get transitionDuration => duration;

  // 展示
  static Future<T?> show<T>({
    required BuildContext context,
    required WidgetBuilder builder,
    Alignment from = Alignment.bottomCenter,
    Duration duration = const Duration(milliseconds: 300),
    Curve curve = Curves.easeOutCubic,
    Color barrierColor = const Color(0x80000000),
    bool barrierDismissible = true,
    String? barrierLabel,
    bool useRootNavigator = true,
    RouteSettings? routeSettings,
  }) {
    return Navigator.of(context, rootNavigator: useRootNavigator).push(
      NSlidePopupRoute<T>(
        builder: builder,
        from: from,
        duration: duration,
        curve: curve,
        barrierColor: barrierColor,
        barrierDismissible: barrierDismissible,
        barrierLabel: barrierLabel,
        settings: routeSettings,
      ),
    );
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    // 直接返回背景和内容,不应用动画
    return Material(
      color: barrierColor,
      child: const SizedBox.expand(), // 只负责背景
    );
  }

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    final content = builder(context);

    // ⭐ 中心弹窗:Fade
    if (from == Alignment.center) {
      return FadeTransition(
        opacity: animation.drive(
          CurveTween(curve: Curves.easeOut),
        ),
        child: content,
      );
    }

    // ⭐ 其余方向:Slide
    return FadeTransition(
      opacity: animation,
      child: SlideTransition(
        position: animation.drive(
          Tween<Offset>(
            begin: _alignmentToOffset(from),
            end: Offset.zero,
          ).chain(
            CurveTween(curve: curve),
          ),
        ),
        child: content,
      ),
    );
  }

  /// Alignment → Offset(关键点)
  Offset _alignmentToOffset(Alignment alignment) {
    return Offset(
      alignment.x.sign,
      alignment.y.sign,
    );
  }
}

最后、总结

  1. NSlidePopupRoute 代码实现极简,没有过多的底层封装,是为了追求极致的自由度。
  2. 当 alignment == Alignment.center 时,它是 Dialog弹窗。
  3. 从顶部滑出时,它可以是顶部通知 Toast。
  4. 从底部滑出时,它可以是 ModalBottomSheet,SnackBar。

github

pub.dev

相关推荐
EnCi Zheng8 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen12 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技13 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人24 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实25 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha36 分钟前
三目运算符
linux·服务器·前端
晓晨的博客43 分钟前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习