You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现类似React Native的可拖拽模态页面导航?

实现带背景缩放+拖拽关闭的Navigator模态页面

默认的fullscreenDialog: true只能实现基础的全屏模态跳转,没法做到背景缩放和拖拽关闭效果。要实现需求,需要自定义PageRoute来控制路由过渡动画和手势交互,以下是具体实现方案:

1. 自定义模态路由类

创建一个继承自PageRouteBuilder的自定义路由,在过渡动画中处理背景页面的缩放,同时添加垂直拖拽手势来触发关闭:

import 'package:flutter/material.dart';

class ZoomDraggableModalRoute<T> extends PageRouteBuilder<T> {
  final Widget Function(BuildContext) builder;

  ZoomDraggableModalRoute({required this.builder})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => builder(context),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            // 背景页面缩放动画:从1.0缩到0.9
            final scaleAnimation = Tween<double>(begin: 1.0, end: 0.9).animate(
              CurvedAnimation(parent: animation, curve: Curves.easeInOut),
            );

            return Stack(
              children: [
                // 背景页面(原页面)的缩放效果
                ScaleTransition(
                  scale: scaleAnimation,
                  child: ModalBarrier(color: Colors.black.withOpacity(0.3)),
                ),
                // 模态页面的入场动画:从底部滑入
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(0, 1),
                    end: Offset.zero,
                  ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
                  child: _DraggableModalWrapper(child: child),
                ),
              ],
            );
          },
          opaque: false, // 必须设为false才能看到背景缩放效果
          barrierDismissible: true,
          barrierColor: Colors.transparent,
        );
}

// 封装拖拽手势的组件
class _DraggableModalWrapper extends StatefulWidget {
  final Widget child;

  const _DraggableModalWrapper({required this.child});

  @override
  State<_DraggableModalWrapper> createState() => _DraggableModalWrapperState();
}

class _DraggableModalWrapperState extends State<_DraggableModalWrapper> {
  double _dragOffset = 0;
  bool _isDismissing = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onVerticalDragUpdate: (details) {
        if (_isDismissing) return;
        setState(() {
          _dragOffset += details.delta.dy;
          // 限制拖拽距离不超过屏幕高度的一半
          _dragOffset = _dragOffset.clamp(0, MediaQuery.of(context).size.height / 2);
        });
      },
      onVerticalDragEnd: (details) {
        if (_dragOffset > MediaQuery.of(context).size.height / 6) {
          // 拖拽距离足够,关闭模态
          _isDismissing = true;
          Navigator.pop(context);
        } else {
          // 拖拽距离不足,回弹
          setState(() => _dragOffset = 0);
        }
      },
      child: Transform.translate(
        offset: Offset(0, _dragOffset),
        child: widget.child,
      ),
    );
  }
}

2. 使用自定义路由跳转

替换原来的MaterialPageRoute,用自定义的ZoomDraggableModalRoute:

Navigator.push(
  context,
  ZoomDraggableModalRoute(
    builder: (context) => SectionCRoute(),
  ),
);

3. 模态页面样式优化

为了更贴近示例效果,建议给SectionCRoute添加顶部圆角和阴影:

class SectionCRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
        boxShadow: [
          BoxShadow(color: Colors.black12, blurRadius: 10, spreadRadius: 2),
        ],
      ),
      child: // 你的页面内容
    );
  }
}

说明

  • 背景缩放动画通过ScaleTransition实现,配合路由的入场动画曲线,自然过渡
  • 拖拽关闭通过监听垂直拖拽手势,根据拖拽距离判断是否关闭,同时添加回弹效果
  • opaque: false是关键,确保路由不会完全遮挡背景页面
  • 可以根据需求调整缩放比例、拖拽阈值、动画曲线等参数

内容的提问来源于stack exchange,提问作者zmix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 23:26:23