Flutter实现类似安卓可拖动主应用菜单的路由方案咨询
Flutter实现类似安卓可拖动主应用菜单的路由方案咨询
嘿,我完全懂你的需求——想做那种安卓上可以拖动呼出的主应用菜单,还希望用路由来管理(毕竟新页面用路由更符合Flutter的设计规范),而且不想用Stack加GestureDetector这种可能性能一般的方案。之前试PopupRoute的时候丢了背景页的滚动手势控制动画,确实挺头疼的,我来给你几个可行的思路:
核心方案:自定义路由(Custom Route)
PopupRoute的问题在于它默认会拦截所有手势,而且过渡动画的灵活性有限,所以自定义路由才是最优解。你可以用PageRouteBuilder快速定制,完全掌控过渡动画和手势交互,还能让背景页保持可交互状态。
1. 基础自定义路由框架
先写一个简单的自定义路由,让菜单从底部滑入,同时支持拖动关闭:
class DraggableMenuRoute extends PageRouteBuilder { final AnimationController? animationController; DraggableMenuRoute({this.animationController}) : super( pageBuilder: (context, animation, secondaryAnimation) => const DraggableMenuPage(), transitionsBuilder: (context, animation, secondaryAnimation, child) { // 绑定拖动手势和动画 return GestureDetector( // 实时更新动画进度 onPanUpdate: (details) { final controller = animationController ?? animation as AnimationController; controller.value -= details.delta.dy / MediaQuery.of(context).size.height; }, // 拖动结束后判断是展开还是收起 onPanEnd: (details) { final controller = animationController ?? animation as AnimationController; if (controller.value > 0.5) { controller.forward(); } else { controller.reverse().then((_) => Navigator.pop(context)); } }, // 滑动过渡动画 child: SlideTransition( position: Tween<Offset>( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)), child: child, ), ); }, opaque: false, // 关键:让背景页可见 barrierDismissible: true, barrierColor: Colors.black.withOpacity(0.3), transitionDuration: const Duration(milliseconds: 300), ); } // 对应的菜单页面 class DraggableMenuPage extends StatelessWidget { const DraggableMenuPage({super.key}); @override Widget build(BuildContext context) { return Align( alignment: Alignment.bottomCenter, child: Container( height: MediaQuery.of(context).size.height * 0.7, width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: // 你的菜单内容 ), ); } }
调用的时候直接用:
Navigator.push(context, DraggableMenuRoute());
2. 关联背景页的滚动手势
如果想让背景页的滚动直接控制菜单的收起/展开,你可以在背景页的ScrollController里添加监听器,联动路由的动画控制器:
// 背景页的代码 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final ScrollController _scrollController = ScrollController(); AnimationController? _menuAnimationController; @override void initState() { super.initState(); _scrollController.addListener(_handleScroll); } void _handleScroll() { if (_menuAnimationController != null && _menuAnimationController!.isAnimating == false) { // 根据滚动偏移调整菜单的显示进度 double scrollOffset = _scrollController.offset; _menuAnimationController!.value = scrollOffset / 200; // 这里的系数可以根据需求调整 // 如果滚动到顶,直接收起菜单 if (scrollOffset <= 0) { _menuAnimationController!.reverse().then((_) => Navigator.pop(context)); } } } void _openMenu() { _menuAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); Navigator.push( context, DraggableMenuRoute(animationController: _menuAnimationController), ).then((_) => _menuAnimationController = null); } @override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( controller: _scrollController, itemCount: 50, itemBuilder: (context, index) => ListTile(title: Text('Item $index')), ), floatingActionButton: FloatingActionButton( onPressed: _openMenu, child: const Icon(Icons.menu), ), ); } }
为什么这个方案比Stack+GestureDetector好?
- 路由自带页面生命周期管理,菜单的创建、销毁都符合Flutter的规范,不会有内存泄漏风险
- 自定义路由的动画是和Flutter的路由动画系统绑定的,性能更稳定,不会出现手势冲突的问题
- 可以轻松集成路由的其他特性,比如
Hero动画、页面返回值传递等
额外注意点
- 记得设置
opaque: false,否则背景页会被遮挡 - 可以根据需求调整动画的曲线和拖动的灵敏度,比如用
Curves.elasticOut让动画更自然 - 如果需要菜单从侧边滑入,只需要修改
SlideTransition的begin偏移量为Offset(1, 0)或者Offset(-1, 0)就行
备注:内容来源于stack exchange,提问作者Nscpt
相关产品推荐
相关产品推荐

