Flutter键盘弹出时FAB上移问题解决求助
问题描述
在Flutter应用中尝试让FloatingActionButton(FAB)在键盘弹出时固定在屏幕底部,但键盘出现时FAB始终会上移。
相关代码
class RecentDmConversationsPageBody extends Stateful Widget { const RecentDmConversationsPageBody({super.key}); @override State<RecentDmConversationsPageBody> createState() => _RecentDmConversationsPageBodyState(); } class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{ RecentDmConversationsView? model; Unreads? unreadsModel; final TextEditingController _searchController = TextEditingController(); List<DmNarrow> _filteredConversations = []; bool _isSearching = false; @override void onNewStore() { model?.removeListener(_modelChanged); model = PerAccountStoreWidget.of(context).recentDmConversationsView ..addListener(_modelChanged); unreadsModel?.removeListener(_modelChanged); unreadsModel = PerAccountStoreWidget.of(context).unreads ..addListener(_modelChanged); _applySearchFilter(); } @override void initState() { super.initState(); _searchController.addListener(_applySearchFilter); } @override void dispose() { model?.removeListener(_modelChanged); unreadsModel?.removeListener(_modelChanged); _searchController.dispose(); super.dispose(); } void _modelChanged() { setState(() { // The actual state lives in [model] and [unreadsModel]. // This method was called because one of those just changed. _applySearchFilter(); }); } void _applySearchFilter() { final query = _searchController.text.toLowerCase(); if (query.isEmpty) { _filteredConversations = List.from(model!.sorted); } else { _filteredConversations = model!.sorted.where((narrow) { final store = PerAccountStoreWidget.of(context); final selfUser = store.users[store.selfUserId]!; final otherRecipientIds = narrow.otherRecipientIds; if (otherRecipientIds.isEmpty) { return selfUser.fullName.toLowerCase().contains(query); } else { return otherRecipientIds.any((id) { final user = store.users[id]; return user?.fullName.toLowerCase().contains(query) ?? false; }); } }).toList(); } setState(() { _isSearching = query.isNotEmpty; }); } @override Widget build(BuildContext context) { // Check if there are any DMs at all in the original model if (model!.sorted.isEmpty) { return const EmptyDmState(); } return Scaffold( backgroundColor: DesignVariables.of(context).mainBackground, resizeToAvoidBottomInset: true, body: SafeArea( // Don't pad the bottom here; we want the list content to do that. bottom: false, child: Stack( children: [Column( children: [ SearchRow(controller: _searchController), Expanded( child: ListView.builder( itemCount: _filteredConversations.length + (_isSearching ? 1 : 0), itemBuilder: (context, index) { if(index < _filteredConversations.length) { final narrow = _filteredConversations[index]; return RecentDmConversationsItem( narrow: narrow, unreadCount: unreadsModel!.countInDmNarrow(narrow), searchQuery: _searchController.text, ); } else{ return const NewDirectMessageButton(); } }), ) ], ), ], )), floatingActionButton: Visibility( visible: !_isSearching, child: const NewDmButton(), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ); } } class NewDirectMessageButton extends StatelessWidget { const NewDirectMessageButton({super.key}); @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); return Container( margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), // Match the button's shape color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity ), child: FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size(137, 44), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), backgroundColor: Colors.transparent, ), onPressed: (){ Navigator.of(context).push( NewDmScreen.buildRoute(context: context) ); }, icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24), label: Text( 'New Direct Message', style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600), ), ), ); } } class NewDmButton extends StatelessWidget { const NewDmButton({super.key}); @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); return Container( padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16), decoration: BoxDecoration( boxShadow: const [ BoxShadow( color: Color(0x662B0E8A), // 40% opacity for #2B0E8A offset: Offset(0, 4), // X: 0, Y: 4 blurRadius: 16, // Blur: 16 spreadRadius: 0, // Spread: 0 ), ], borderRadius: BorderRadius.circular(28), // Match the button's shape ), child: FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size(137, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), backgroundColor: designVariables.fabBg, ), onPressed: (){ Navigator.of(context).push( NewDmScreen.buildRoute(context: context) ); }, icon: const Icon(Icons.add, color: Colors.white, size: 24), label: Text( 'New DM', style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500), ), ), ); } }
已尝试的解决方案
- 在Scaffold中设置
resizeToAvoidBottomInset: false,可阻止页面主体调整大小,但FAB仍会上移; - 在Scaffold外部使用
Stack搭配Positioned布局,FAB仍会随键盘弹出移位; - 通过
MediaQuery.of(context).viewInsets.bottom检测键盘可见性并控制FAB显示,仅能隐藏FAB,无法阻止其显示时的移位。
期望效果
- FAB始终固定在屏幕底部,不受键盘弹出影响;
- 键盘弹出时不会将FAB向上推送。
可行解决方案
方法1:动态调整FAB的底部偏移
修改NewDmButton的布局,通过MediaQuery获取键盘高度,动态抵消键盘带来的底部偏移,让FAB始终贴在屏幕物理底部:
class NewDmButton extends StatelessWidget { const NewDmButton({super.key}); @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; return Container( padding: EdgeInsets.fromLTRB(12, 8.0, 16.0, 16 + keyboardHeight), decoration: BoxDecoration( boxShadow: const [ BoxShadow( color: Color(0x662B0E8A), offset: Offset(0, 4), blurRadius: 16, spreadRadius: 0, ), ], borderRadius: BorderRadius.circular(28), ), child: FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size(137, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), backgroundColor: designVariables.fabBg, ), onPressed: (){ Navigator.of(context).push(NewDmScreen.buildRoute(context: context)); }, icon: const Icon(Icons.add, color: Colors.white, size: 24), label: Text( 'New DM', style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500), ), ), ); } }
同时保持Scaffold的resizeToAvoidBottomInset: true,确保页面内容正常上移避免被键盘遮挡。
方法2:将FAB挂载到全局Overlay
利用Flutter的Overlay组件,让FAB脱离页面布局层级,直接挂载到全局Overlay层,完全不受键盘调整影响:
class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{ // ... 原有变量 OverlayEntry? _fabOverlayEntry; @override void initState() { super.initState(); _searchController.addListener(_applySearchFilter); _fabOverlayEntry = OverlayEntry(builder: (context) { return Positioned( bottom: 16, left: MediaQuery.of(context).size.width / 2 - 68.5, child: Visibility( visible: !_isSearching, child: const NewDmButton(), ), ); }); WidgetsBinding.instance.addPostFrameCallback((_) { Overlay.of(context).insert(_fabOverlayEntry!); }); } @override void dispose() { model?.removeListener(_modelChanged); unreadsModel?.removeListener(_modelChanged); _searchController.dispose(); _fabOverlayEntry?.remove(); super.dispose(); } @override void setState(VoidCallback fn) { super.setState(fn); _fabOverlayEntry?.markNeedsBuild(); } // ... 原有方法 }
然后移除原Scaffold中的floatingActionButton和floatingActionButtonLocation配置。
方法3:用bottomNavigationBar替代FAB
如果不需要FAB的特殊定位逻辑,可将其放在bottomNavigationBar中,配合resizeToAvoidBottomInset: false实现固定:
return Scaffold( backgroundColor: DesignVariables.of(context).mainBackground, resizeToAvoidBottomInset: false, body: SafeArea( // ... 原有body内容 ), bottomNavigationBar: Visibility( visible: !_isSearching, child: Padding( padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), child: const NewDmButton(), ), ), );
注意调整NewDmButton内部的padding,避免多余边距。
内容的提问来源于stack exchange,提问作者Dhanesh Sawant
相关产品推荐
相关产品推荐

