Flutter中如何设置内嵌Navigator的BottomSheet最小尺寸
问题描述
我需要实现一个嵌套子Navigator的BottomSheet,要求BottomSheet的尺寸始终与Navigator内当前展示的单个页面内容高度一致,尝试多种实现方式均未达到预期效果,目标效果如下:
原有实现代码如下:
class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter(BuildContext context) { DemoSubNavigatorPopup.show(context); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => _incrementCounter(context), tooltip: 'Increment', child: const Icon(Icons.add), ), ); } } class DemoSubNavigatorPopup extends StatelessWidget { DemoSubNavigatorPopup({Key? key}) : super(key: key); static show(BuildContext context) { showModalBottomSheet( context: context, builder: (context) { return DemoSubNavigatorPopup(); }); } final subNavigatorKey = GlobalKey<NavigatorState>(); @override Widget build(BuildContext context) { return Navigator( key: subNavigatorKey, onGenerateRoute: (setting) { return MaterialPageRoute(builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SubPageScreen(); })); }, child: Text('Next SubPage'), ) ], ); }); }, ); } } class SubPageScreen extends StatelessWidget { const SubPageScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { Navigator.pop(context); }, child: Text('Back')), Text('SubPage') ], ); } }
失效原因
原有代码无法实现自适应的核心原因有两点:
Navigator组件默认布局逻辑是撑满父组件传递的所有可用约束,不会跟随内部子页面的内容高度自适应。哪怕子页面设置了mainAxisSize: MainAxisSize.min,也只会在Navigator给定的固定高度范围内调整自身布局,无法反向影响Navigator的外层尺寸。showModalBottomSheet默认会给弹出内容施加「最大高度为屏幕高度90%」的约束,且子Navigator路由切换时不会触发BottomSheet的尺寸重算,切换到高度不同的子页面时BottomSheet高度不会同步更新。
解决方案
核心改动点
- 将
DemoSubNavigatorPopup从无状态组件改为有状态组件,添加路由监听器监听子Navigator的入栈、出栈事件,每次路由切换时触发组件重建,同步更新BottomSheet高度 - 用
IntrinsicHeight组件包裹子Navigator,打破Navigator默认撑满父约束的布局逻辑,强制其按照当前可见子页面的固有高度布局(Navigator会自动将非可见路由设为Offstage,不会参与固有高度计算) - 给
showModalBottomSheet设置isScrollControlled: true,移除默认的高度上限约束,允许内容按照自身高度自适应展示 - 所有子路由页面保留
mainAxisSize: MainAxisSize.min配置,确保自身高度贴合内容
修改后可运行代码
class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter(BuildContext context) { DemoSubNavigatorPopup.show(context); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => _incrementCounter(context), tooltip: 'Increment', child: const Icon(Icons.add), ), ); } } // 路由监听器,监听子Navigator的路由切换事件 final RouteObserver<ModalRoute> routeObserver = RouteObserver<ModalRoute>(); class DemoSubNavigatorPopup extends StatefulWidget { DemoSubNavigatorPopup({Key? key}) : super(key: key); static show(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, // 解除默认高度限制 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { return DemoSubNavigatorPopup(); }); } final subNavigatorKey = GlobalKey<NavigatorState>(); @override State<DemoSubNavigatorPopup> createState() => _DemoSubNavigatorPopupState(); } class _DemoSubNavigatorPopupState extends State<DemoSubNavigatorPopup> with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); // 订阅路由事件 routeObserver.subscribe(this, ModalRoute.of(context)!); } @override void dispose() { // 取消订阅 routeObserver.unsubscribe(this); super.dispose(); } // 路由入栈时触发重建 @override void didPush() { setState(() {}); } // 路由出栈时触发重建 @override void didPop() { setState(() {}); } @override Widget build(BuildContext context) { // 用IntrinsicHeight包裹Navigator,让其适配子页面高度 return IntrinsicHeight( child: Navigator( key: widget.subNavigatorKey, observers: [routeObserver], onGenerateRoute: (setting) { return MaterialPageRoute(builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 20), TextButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SubPageScreen(); })); }, child: const Text('Next SubPage'), ), const SizedBox(height: 20), ], ); }); }, ), ); } } class SubPageScreen extends StatelessWidget { const SubPageScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 40), TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('Back')), const Text('SubPage'), const SizedBox(height: 80), ], ); } }
注意事项
- 如果子页面内包含滚动组件(如
ListView、GridView),需要给滚动组件设置shrinkWrap: true,让滚动组件自适应内容高度,否则IntrinsicHeight计算高度时会抛出无界高度异常。 - 如果子页面存在输入框,需要给BottomSheet外层添加
viewInsets对应的padding,避免键盘弹出遮挡内容。
内容的提问来源于stack exchange,提问作者Trần Văn Hiếu
相关产品推荐
相关产品推荐

