Flutter:实现自定义Bottom Sheet高度随内容自适应
可拖拽Bottom Sheet自适应内容高度解决方案
问题描述
我正在实现一个可拖拽的Bottom Sheet,支持手动展开/折叠,也能按住滑动临时折叠。目前功能基本正常,但Bottom Sheet高度无法自适应内容——现在弹窗底部刚好停在“Line 6”下方,因为我硬编码了maxExtent值。
原始代码
library draggable_bottom_sheet; import 'package:flutter/material.dart'; class DraggableTest extends StatefulWidget { const DraggableTest({super.key}); @override State<DraggableTest> createState() => _DraggableTestState(); } class _DraggableTestState extends State<DraggableTest> { bool _collapsed = false; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Bottom Sheet')), body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ OutlinedButton( onPressed: () { setState(() { _collapsed = !_collapsed; }); }, child: Text(_collapsed ? 'Expand' : 'Collapse')), DraggableBottomSheet( expandedWidget: _expandedWidget(), backgroundColor: Colors.yellow, collapsed: _collapsed, maxExtent: 250, ), ], ), ), ); } Widget _expandedWidget() { return ListView( physics: const NeverScrollableScrollPhysics(), children: const [ Text('Title', style: TextStyle(fontSize: 22)), Text('Line 1', style: TextStyle(fontSize: 18)), Text('Line 2', style: TextStyle(fontSize: 18)), Text('Line 3', style: TextStyle(fontSize: 18)), Text('Line 4', style: TextStyle(fontSize: 18)), Text('Line 5', style: TextStyle(fontSize: 18)), Text('Line 6', style: TextStyle(fontSize: 18)), ], ); } } class DraggableBottomSheet extends StatefulWidget { final Widget expandedWidget; final Color backgroundColor; final bool collapsed; final double maxExtent; const DraggableBottomSheet({ required widget.expandedWidget, required widget.backgroundColor, required widget.collapsed, required widget.maxExtent, super.key, }); @override State<DraggableBottomSheet> createState() => _DraggableBottomSheetState(); } class _DraggableBottomSheetState extends State<DraggableBottomSheet> { double _currentExtent = 0.0; final _minExtent = 50.0; bool _isDragging = false; @override void initState() { _currentExtent = widget.collapsed ? _minExtent : widget.maxExtent; // <<<< 问题根源:硬编码高度 super.initState(); } @override Widget build(BuildContext context) { return Container( color: widget.backgroundColor, child: Align(alignment: Alignment.bottomCenter, child: _sheet()), ); } Widget _sheet() { return GestureDetector( onVerticalDragUpdate: _onVerticalDragUpdate, onVerticalDragStart: _onVerticalDragStart, onVerticalDragEnd: _onVerticalDragEnd, child: AnimatedContainer( curve: Curves.bounceOut, duration: _isDragging ? Duration.zero : const Duration(milliseconds: 500), height: widget.collapsed ? _minExtent : _currentExtent, child: widget.collapsed ? null : widget.expandedWidget, ), ); } void _onVerticalDragEnd(DragEndDetails details) { setState(() { _isDragging = false; _currentExtent = widget.maxExtent; }); } void _onVerticalDragStart(DragStartDetails details) { setState(() { _isDragging = true; }); } void _onVerticalDragUpdate(DragUpdateDetails details) { if (widget.collapsed) return; final newExtent = (_currentExtent - details.delta.dy).roundToDouble(); if (newExtent >= _minExtent && newExtent <= widget.maxExtent) { setState(() => _currentExtent = newExtent); } } }
修改后的代码
library draggable_bottom_sheet; import 'package:flutter/material.dart'; class DraggableTest extends StatefulWidget { const DraggableTest({super.key}); @override State<DraggableTest> createState() => _DraggableTestState(); } class _DraggableTestState extends State<DraggableTest> { bool _collapsed = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Bottom Sheet')), body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ OutlinedButton( onPressed: () => setState(() => _collapsed = !_collapsed), child: Text(_collapsed ? '展开' : '收起'), ), DraggableBottomSheet( expandedWidget: _expandedWidget(), backgroundColor: Colors.yellow, collapsed: _collapsed, ), ], ), ), ); } Widget _expandedWidget() { // 替换ListView为Column,避免不必要的滚动容器,便于计算高度 return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('Title', style: TextStyle(fontSize: 22)), Text('Line 1', style: TextStyle(fontSize: 18)), Text('Line 2', style: TextStyle(fontSize: 18)), Text('Line 3', style: TextStyle(fontSize: 18)), Text('Line 4', style: TextStyle(fontSize: 18)), Text('Line 5', style: TextStyle(fontSize: 18)), Text('Line 6', style: TextStyle(fontSize: 18)), ], ); } } class DraggableBottomSheet extends StatefulWidget { final Widget expandedWidget; final Color backgroundColor; final bool collapsed; const DraggableBottomSheet({ required this.expandedWidget, required widget.backgroundColor, required widget.collapsed, super.key, }); @override State<DraggableBottomSheet> createState() => _DraggableBottomSheetState(); } class _DraggableBottomSheetState extends State<DraggableBottomSheet> { double _currentExtent = 0.0; final _minExtent = 50.0; bool _isDragging = false; late double _contentHeight; final GlobalKey _contentKey = GlobalKey(); @override void initState() { super.initState(); // 组件渲染完成后计算内容实际高度 WidgetsBinding.instance.addPostFrameCallback((_) => _calculateContentHeight()); } @override void didUpdateWidget(covariant DraggableBottomSheet oldWidget) { super.didUpdateWidget(oldWidget); // 内容或折叠状态变化时,重新计算高度 if (oldWidget.expandedWidget != widget.expandedWidget || oldWidget.collapsed != widget.collapsed) { WidgetsBinding.instance.addPostFrameCallback((_) => _calculateContentHeight()); } } void _calculateContentHeight() { final renderBox = _contentKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { setState(() { _contentHeight = renderBox.size.height; // 根据折叠状态初始化高度 _currentExtent = widget.collapsed ? _minExtent : _contentHeight; }); } } @override Widget build(BuildContext context) { return Container( color: widget.backgroundColor, child: Align(alignment: Alignment.bottomCenter, child: _sheet()), ); } Widget _sheet() { return GestureDetector( onVerticalDragUpdate: _onVerticalDragUpdate, onVerticalDragStart: _onVerticalDragStart, onVerticalDragEnd: _onVerticalDragEnd, child: AnimatedContainer( curve: Curves.bounceOut, duration: _isDragging ? Duration.zero : const Duration(milliseconds: 500), height: widget.collapsed ? _minExtent : _currentExtent, child: widget.collapsed ? null : KeyedSubtree( key: _contentKey, child: widget.expandedWidget, ), ), ); } void _onVerticalDragEnd(DragEndDetails details) { setState(() { _isDragging = false; // 拖拽结束后恢复到内容原高度 _currentExtent = _contentHeight; }); } void _onVerticalDragStart(DragStartDetails details) { setState(() { _isDragging = true; }); } void _onVerticalDragUpdate(DragUpdateDetails details) { if (widget.collapsed) return; final newExtent = (_currentExtent - details.delta.dy).roundToDouble(); // 限制拖拽范围在最小高度和内容实际高度之间 if (newExtent >= _minExtent && newExtent <= _contentHeight) { setState(() => _currentExtent = newExtent); } } }
关键改动说明
- 替换滚动容器:把不可滚动的ListView换成Column,设置
mainAxisSize: MainAxisSize.min让内容自动适配高度,简化高度计算。 - 动态计算内容高度:通过GlobalKey获取内容的RenderBox,在组件渲染完成后计算实际高度,替代硬编码的maxExtent。
- 适配状态变化:在
didUpdateWidget中监听内容或折叠状态的变化,重新计算高度,确保动态内容也能正确适配。 - 优化拖拽逻辑:拖拽范围限制在最小高度和内容实际高度之间,避免出现内容截断或空白区域。
内容的提问来源于stack exchange,提问作者Denis
相关产品推荐
相关产品推荐

