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

相关推荐
Csvn8 小时前
WASM
前端
好的佩奇8 小时前
蓝牙之通信架构
android·flutter·蓝牙
修己xj9 小时前
我用 AI 给自己写了一个 Markdown 编辑器,然后把它开源了
前端
FRing039 小时前
vue2干货总结2(v-bind、v-for、v-model、指令修饰符、v-bind操作class和style)
开发语言·前端·vue.js·vue2
幸运小猿10 小时前
模拟慢上传-处理超时重试image 丢失线上问题
前端·javascript·html
moshuying11 小时前
网购能买Token了!
前端
h_654321012 小时前
echarts实现柱状图加一条横线
前端·echarts
风骏时光牛马12 小时前
生成一个后端标题,只需要一个标题,不需要其他的任何文本。
前端
子非鱼@Itfuture12 小时前
关于前后端联调中登录状态/登录信息失效?
java·前端
郭小李12 小时前
openFeign入门
java·开发语言·前端