普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月15日掘金 前端

别再乱用 @State 了!鸿蒙状态管理避坑指南,看完省 3 天脱发时间

2026年1月15日 09:19
哈喽,兄弟们,我是 V 哥! 最近有粉丝在群里发了个截图,代码里密密麻麻全是 @State,看得我密集恐惧症都犯了。他说:“V 哥,我的 App 怎么越改越卡?明明只是改了列表里的一个文字,整个页面都
昨天 — 2026年1月14日掘金 前端

2025年终总结-都在喊前端已死,这一年我的焦虑、挣扎与重组:AI 时代如何摆正自己的位置

2026年1月14日 19:32
前言 今年这一年,我整个人一直处于一种紧绷的焦虑状态。 这种焦虑来自于一种真切的危机感:作为前端,我发现自己曾经引以为傲的“技术壁垒”在 AI 面前像纸一样薄。但最近,我突然想通了。当我意识到 AI

深入掌握 AI 全栈项目中的路由功能:从基础到进阶的全面解析

2026年1月14日 18:32
在构建现代 Web 应用的过程中,路由管理是必不可少的一部分。本文将深入探讨 AI 全栈项目中的路由功能,涵盖基础知识、进阶概念以及常见的最佳实践。无论您是初学者还是有一定经验的开发者,希望本文能够帮

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

作者 SoaringHeart
2026年1月14日 18:16

一、需求来源

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

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

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

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.41.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.43.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.45.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.48.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.51.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.53.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.36.59.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.37.01.png

Simulator Screenshot - iPhone 16 - 2025-12-27 at 11.37.04.png

二、使用示例

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"),
        ),
      ),
    );
  }
}

三、源码

//
//  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

❌
❌