Flutter中如何让BottomSheet高度自适应内部PageView的子项内容?
核心问题
你当前的实现存在两个直接导致高度无法自适应的问题:
- 给包裹PageView的Container写死了
height: 600的固定高度 - PageView默认不支持自适应子组件高度,需要手动计算每个页面的实际渲染高度,在切换页面时动态调整
解决方案
我们通过动态计算每个页面高度、监听页面切换更新高度的方式实现需求,同时增加最大高度限制避免内容过长溢出屏幕:
- 把组件改为StatefulWidget,维护当前页面高度状态与PageController监听
- 给每个PageView的子页面根节点绑定GlobalKey,用于获取布局完成后的实际高度
- 页面首次渲染完成后获取初始页高度,切换页面时同步更新当前高度
- 移除原有的固定高度配置,替换为动态高度+最大高度限制
完整实现代码
import 'package:flutter/material.dart'; class Test extends StatefulWidget { @override State<Test> createState() => _TestState(); } class _TestState extends State<Test> { final PageController controller = PageController(initialPage: 0); // 存储每个页面的GlobalKey final List<GlobalKey> pageKeys = [GlobalKey(), GlobalKey(), GlobalKey()]; // 当前页面高度 double? currentPageHeight; // 底部弹窗最大高度限制(屏幕高度的80%,可自行调整) late double maxSheetHeight; @override void initState() { super.initState(); // 监听页面切换,更新高度 controller.addListener(() { if (controller.page?.round() == controller.page) { _updatePageHeight(controller.page!.toInt()); } }); } @override void didChangeDependencies() { maxSheetHeight = MediaQuery.of(context).size.height * 0.8; super.didChangeDependencies(); } // 更新指定页面的高度 void _updatePageHeight(int index) { final renderBox = pageKeys[index].currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { setState(() { currentPageHeight = renderBox.size.height; }); } } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Container( padding: const EdgeInsets.symmetric(horizontal: 20), child: Center( child: ElevatedButton( child: const Text('BottomSheet'), onPressed: () { showModalBottomSheet( isScrollControlled: true, context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadiusDirectional.only( topEnd: Radius.circular(25), topStart: Radius.circular(25), ), ), builder: (context) => Container( padding: const EdgeInsetsDirectional.only( start: 20, end: 20, bottom: 30, top: 8, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 移除固定高度,改为动态高度限制 Container( constraints: BoxConstraints( maxHeight: maxSheetHeight, minHeight: currentPageHeight ?? 0, ), child: PageView( controller: controller, children: <Widget>[ SingleChildScrollView( child: Column( key: pageKeys[0], mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: List.generate( 5, (index) => Container( height: 60, margin: const EdgeInsetsDirectional.only(bottom: 10), width: double.infinity, color: Colors.yellow, child: Center( child: Text( 'Widgets $index', style: const TextStyle(fontSize: 20), ), ), ), ), ), ), SingleChildScrollView( child: Column( key: pageKeys[1], mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: List.generate( 10, (index) => Container( height: 60, margin: const EdgeInsetsDirectional.only(bottom: 10), width: double.infinity, color: Colors.yellow, child: Center( child: Text( 'Widgets $index', style: const TextStyle(fontSize: 20), ), ), ),), ), ), SingleChildScrollView( child: Column( key: pageKeys[2], mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: List.generate( 12, (index) => Container( height: 60, margin: const EdgeInsetsDirectional.only(bottom: 10), width: double.infinity, color: Colors.yellow, child: Center( child: Text( 'Widgets $index', style: const TextStyle(fontSize: 20), ), ), ),), ), ), ], ), ), ], ), ), ).then((_) { // 关闭弹窗后重置高度状态,避免下次打开出现高度异常 setState(() { currentPageHeight = null; }); }); // 弹窗首次渲染完成后获取初始页高度 WidgetsBinding.instance.addPostFrameCallback((_) { _updatePageHeight(0); }); }, ), ), ), ); } }
关键说明
- 动态获取的高度已经包含了子组件的margin、padding等间距,不需要额外计算
- 最大高度限制可根据需求自行调整,内容超过最大高度时SingleChildScrollView会自动开启滚动,不会溢出屏幕
- 页面切换时高度会自动平滑适配对应页面的内容高度
内容的提问来源于stack exchange,提问作者Mohammed
相关产品推荐
相关产品推荐

