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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 05:27:22