Flutter中ExpansionTile折叠时Container溢出问题的修复方案
问题:ExpansionTile折叠时Container出现溢出
我正在实现一个动态Container,使其高度随ExpansionTile的状态变化:展开时Container高度增加,折叠时恢复正常高度。但折叠时出现了内容溢出的问题。
- 折叠状态:界面底部出现内容溢出
- 展开状态:界面显示正常
原代码实现
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Address extends StatefulWidget { const Address({super.key}); @override State<Address> createState() => _AddressState(); } class _AddressState extends State<Address> { FocusNode addressText = FocusNode(); bool _open = false; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( centerTitle: true, title: Container( width: 200, child: PreferredSize( preferredSize: Size.fromHeight(10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: LinearProgressIndicator( value: 1, // Value for first step backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), SizedBox(width: 7.0), Expanded( child: LinearProgressIndicator( value: 1, // Value for second step backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), SizedBox(width: 7.0), Expanded( child: LinearProgressIndicator( value: 0.15, // Value for third step backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), ], ), ), ) ), body: SingleChildScrollView( child: Container( padding: EdgeInsets.all(30.0), width: MediaQuery.of(context).size.width, height: _open ? MediaQuery.of(context).size.height-1/9*(MediaQuery.of(context).size.height)+100 : MediaQuery.of(context).size.height-1/9*(MediaQuery.of(context).size.height), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Enter your address",style: TextStyle(fontSize: 22,fontFamily: "GraphikMedium"),), SizedBox(height: 12,), Text("Enter the street address of your primary residence. Please do not use a PO box or business address", style: TextStyle(color: Colors.grey,fontSize: 16),), SizedBox(height: 24,), Text("Search for Address",style: TextStyle(fontSize: 16.0,fontFamily: "GraphikMedium", color: addressText.hasFocus ? Colors.blue : Colors.black), ), SizedBox(height: 8.0,), Focus( onFocusChange: (focus){ setState(() {}); }, child: TextFormField( focusNode: addressText, decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue) ), hintText: "Enter your address", hintStyle: TextStyle(color: Colors.grey) ), ), ), SizedBox(height: 40,), Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ListTileTheme( contentPadding: EdgeInsets.all(0), dense: true, child: ExpansionTile( title: Text("Enter Address Manually",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), onExpansionChanged: (bool value) { setState(() { _open = value; }); }, children: [ Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 8.0,), Text("Address line 1",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("Address line 2 (optional)",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("City",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("City",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), ], ), ) ], ), ), ) ], ), Container( width: MediaQuery.of(context).size.width, child: ElevatedButton( onPressed: (){ Navigator.pushNamed(context, '/user-purpose'); }, child: Text( "Continue", style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.normal, color: Colors.white, fontFamily: "GraphikMedium", ), ), style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)) ), minimumSize: MaterialStateProperty.all(Size.fromHeight(60.0)), backgroundColor: MaterialStateProperty.all(Colors.blue), shadowColor: MaterialStateProperty.all(Colors.transparent) ), ), ) ], ), ), ) ); } }
问题原因
手动给Container设置了固定高度,折叠状态下这个高度远大于实际内容高度,再加上Column使用了mainAxisAlignment: MainAxisAlignment.spaceBetween,会强制把底部按钮推到Container的底部,导致内容区域被挤压,最终引发溢出。另外,SingleChildScrollView本身就是用来处理内容超出屏幕的情况,固定高度完全没必要,反而会干扰布局计算。
修复方案
方案1:移除固定高度,让内容自适应
直接去掉Container的height属性,让它根据内容自动调整高度,SingleChildScrollView会自动处理滚动逻辑:
修改后的核心代码片段:
body: SingleChildScrollView( child: Container( padding: EdgeInsets.all(30.0), width: MediaQuery.of(context).size.width, // 移除手动设置的height属性 child: Column( // 替换spaceBetween为start,避免内容被强制分散 mainAxisAlignment: MainAxisAlignment.start, children: [ // ... 原有内容组件 ... // 给按钮添加顶部间距,保持和上方内容的距离 SizedBox(height: 20), Container( width: MediaQuery.of(context).size.width, child: ElevatedButton(/* ... 原有按钮代码 ... */), ) ], ), ), )
方案2:让按钮固定在屏幕底部(推荐)
如果希望折叠状态下“Continue”按钮始终在屏幕底部,展开时随内容滚动,可以把body改成Column,上方用Expanded包裹SingleChildScrollView,下方放置按钮:
修改后的完整代码:
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Address extends StatefulWidget { const Address({super.key}); @override State<Address> createState() => _AddressState(); } class _AddressState extends State<Address> { FocusNode addressText = FocusNode(); bool _open = false; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( centerTitle: true, title: Container( width: 200, child: PreferredSize( preferredSize: Size.fromHeight(10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: LinearProgressIndicator( value: 1, backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), SizedBox(width: 7.0), Expanded( child: LinearProgressIndicator( value: 1, backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), SizedBox(width: 7.0), Expanded( child: LinearProgressIndicator( value: 0.15, backgroundColor: Color(0xD9D9D9D9), valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), ), ], ), ), ) ), body: Column( children: [ Expanded( child: SingleChildScrollView( padding: EdgeInsets.all(30.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Enter your address",style: TextStyle(fontSize: 22,fontFamily: "GraphikMedium"),), SizedBox(height: 12,), Text("Enter the street address of your primary residence. Please do not use a PO box or business address", style: TextStyle(color: Colors.grey,fontSize: 16),), SizedBox(height: 24,), Text("Search for Address",style: TextStyle(fontSize: 16.0,fontFamily: "GraphikMedium", color: addressText.hasFocus ? Colors.blue : Colors.black), ), SizedBox(height: 8.0,), Focus( onFocusChange: (focus){ setState(() {}); }, child: TextFormField( focusNode: addressText, decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue) ), hintText: "Enter your address", hintStyle: TextStyle(color: Colors.grey) ), ), ), SizedBox(height: 40,), Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ListTileTheme( contentPadding: EdgeInsets.all(0), dense: true, child: ExpansionTile( title: Text("Enter Address Manually",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), onExpansionChanged: (bool value) { setState(() { _open = value; }); }, children: [ Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 8.0,), Text("Address line 1",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("Address line 2 (optional)",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("City",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), SizedBox(height: 8.0,), Text("City",style: TextStyle(fontFamily: "GraphikRegular",fontSize: 16.0),), TextFormField( decoration: InputDecoration( enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.grey) ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ) ), ), ], ), ) ], ), ), ) ], ), ), ), // 底部按钮区域,固定在屏幕底部 Padding( padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0), child: Container( width: MediaQuery.of(context).size.width, child: ElevatedButton( onPressed: (){ Navigator.pushNamed(context, '/user-purpose'); }, child: Text( "Continue", style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.normal, color: Colors.white, fontFamily: "GraphikMedium", ), ), style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)) ), minimumSize: MaterialStateProperty.all(Size.fromHeight(60.0)), backgroundColor: MaterialStateProperty.all(Colors.blue), shadowColor: MaterialStateProperty.all(Colors.transparent) ), ), ), ) ], ) ); } }
这个方案的优势:
- 折叠状态下按钮固定在屏幕底部,符合用户操作预期
- 展开状态下内容可滚动,按钮随内容自然下移,不会出现溢出
- 无需手动计算高度,布局自适应各种屏幕尺寸
内容的提问来源于stack exchange,提问作者Name
相关产品推荐
相关产品推荐

