Flutter打开键盘时弹窗组件被挤压的问题求助
Flutter弹窗键盘唤起时RenderFlex溢出及容器挤压问题解决
问题
自定义Tile点击后通过Hero组件过渡到弹窗,弹窗内点击输入框唤起键盘时触发RenderFlex溢出错误。用SingleChildScrollView包裹后溢出解决,但弹窗容器会被挤压。
错误日志
Reload already in progress, ignoring request Reloaded 1 of 971 libraries in 853ms (compile: 173 ms, reload: 284 ms, reassemble: 250 ms). D/InputMethodManager(30094): showSoftInput() view=io.flutter.embedding.android.FlutterView{3275d02 VFE...... .F...... 0,0-1080,2400 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT I/AssistStructure(30094): Flattened final assist data: 472 bytes, containing 1 windows, 3 views D/InputConnectionAdaptor(30094): The input method toggled cursor monitoring on D/InsetsController(30094): show(ime(), fromIme=true) ════════ Exception caught by rendering library ═════════════════════════════════ The following assertion was thrown during layout: A RenderFlex overflowed by 90 pixels on the bottom. The relevant error-causing widget was Column my_dialog.dart:88 You can inspect this widget using the 'Inspect Widget' button in the VS Code notification. The overflowing RenderFlex has an orientation of Axis.vertical. The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and black striped pattern. This is usually caused by the contents being too big for the RenderFlex. Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the RenderFlex to fit within the available space instead of being sized to their natural size. This is considered an error condition because it indicates that there is content that cannot be seen. If the content is legitimately bigger than the available space, consider clipping it with a ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex, like a ListView. The specific RenderFlex in question is: RenderFlex#70131 relayoutBoundary=up4 OVERFLOWING ════════════════════════════════════════════════════════════════════════════════
原代码(my_dialog.dart)
// ignore_for_file: override_on_non_overriding_member import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:smooth_like_flutter/components/service_field.dart'; import 'package:smooth_like_flutter/dialog_visibility.dart'; class AlignedDialogRoute extends PageRoute<void> { AlignedDialogRoute({ super.settings, this.padding = EdgeInsets.zero, this.alignment = Alignment.center, required this.heroTag, required this.iconHeroTag, required this.textHeroTag, required this.builder, }) : super(fullscreenDialog: false); final EdgeInsets padding; final Alignment alignment; final String title = 'Followers'; final double priceOfOne = 0.25; final quantityController = TextEditingController(); final linkController = TextEditingController(); var totalPrice = ValueNotifier<double>(0); /// The tag used to match the Hero transition widgets. final String heroTag; final String textHeroTag; final String iconHeroTag; /// The builder will create the widget that we want to show in the dialog. final Widget Function( BuildContext, Animation<double>, Animation<double>, ) builder; @override Widget buildPage( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, ) { return Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Colors.transparent, body: Stack( children: [ GestureDetector( onTap: () { setState(() { isDialogVisible = false; }); Navigator.pop(context); }, child: SizedBox( width: double.infinity, height: double.infinity, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( color: Colors.black.withOpacity(0), ), ), ), ), Align( alignment: Alignment.center, child: Hero( tag: heroTag, child: Container( height: 450, width: 280, decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1.5), color: Colors.grey[100], borderRadius: const BorderRadius.all(Radius.circular(10))), ), ), ), Align( alignment: Alignment.center, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 350), child: SingleChildScrollView( child: Column( children: [ const SizedBox( height: 10, ), Hero( tag: iconHeroTag, child: const Icon( Icons.group_rounded, color: Colors.black, size: 40, ), ), Hero( tag: textHeroTag, child: Material( type: MaterialType.transparency, child: Text( title, textAlign: TextAlign.center, style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w800, ), ), ), ), const SizedBox( height: 5, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: totalPrice, builder: (context, value, child) { return Text( value.toString(), style: TextStyle(color: Colors.grey[700]), ); }, ), SizedBox( width: 22, height: 22, child: Image.asset('assets/images/coin.png'), ) ], ), const SizedBox( height: 30, ), AnimatedOpacity( duration: const Duration(milliseconds: 1000), curve: Curves.easeInOutExpo, opacity: checkDialog(isDialogVisible), child: MyServiceField( type: TextInputType.number, controller: quantityController, hint: 'Quantity', onChange: (fieldData) { var fieldNum = int.parse(fieldData); totalPrice.value = fieldNum * priceOfOne; }, ), ), const SizedBox( height: 10, ), MyServiceField( type: TextInputType.emailAddress, controller: linkController, hint: 'Link', ), const SizedBox( height: 49, ), GestureDetector( onTap: () { Navigator.pop(context); setState(() { isDialogVisible = false; }); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( icon: const Icon( Icons.error_outline, color: Colors.black, ), title: const Text("Notice"), alignment: Alignment.center, contentTextStyle: const TextStyle(color: Colors.grey), content: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), child: const Text( "Your order has been placed and will be completed within 2 to 10 Hours.", style: TextStyle(), ), ), ], ), actions: [ Center( child: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Center( child: Container( width: 200, height: 50, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.all( Radius.circular(10))), child: const Center( child: Text( 'Ok', style: TextStyle(color: Colors.white), )), ), ), )) ], ); }); }, child: Container( width: 200, height: 50, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.all(Radius.circular(30))), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 100), child: const Text( 'Place Order', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), ), ), ) ], ), ), ), ), ], ), ); } @override bool get opaque => false; @override bool get barrierDismissible => true; @override Color? get barrierColor => Colors.black54; @override String? get barrierLabel => "Dialog dismissal barrier"; @override bool get maintainState => true; @override Duration get transitionDuration => const Duration(milliseconds: 500); } checkDialog(bool isDialogVisible) { double opacity; if (isDialogVisible) { opacity = 0.0; } else { opacity = 1.0; } return opacity; }
解决方案
问题根源
- 背景容器和内容层是两个独立的Align组件,布局变化不同步,键盘弹出时内容层被挤压。
- SingleChildScrollView被ConstrainedBox限制了最大高度,无法自适应键盘弹出后的可用空间。
- 背景容器使用固定高度,无法响应布局变化。
修改后的核心代码
@override Widget buildPage( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, ) { return Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Colors.transparent, body: Stack( children: [ // 背景遮罩保留不变 GestureDetector( onTap: () { setState(() { isDialogVisible = false; }); Navigator.pop(context); }, child: SizedBox( width: double.infinity, height: double.infinity, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( color: Colors.black.withOpacity(0), ), ), ), ), // 合并Hero容器与内容组件,确保布局联动 Align( alignment: Alignment.center, child: Hero( tag: heroTag, child: Container( width: 280, // 用约束替代固定高度,适配键盘弹出后的空间 constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, minHeight: 450, ), decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1.5), color: Colors.grey[100], borderRadius: const BorderRadius.all(Radius.circular(10))), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 10), Hero( tag: iconHeroTag, child: const Icon( Icons.group_rounded, color: Colors.black, size: 40, ), ), Hero( tag: textHeroTag, child: Material( type: MaterialType.transparency, child: Text( title, textAlign: TextAlign.center, style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w800, ), ), ), ), const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: totalPrice, builder: (context, value, child) { return Text( value.toString(), style: TextStyle(color: Colors.grey[700]), ); }, ), SizedBox( width: 22, height: 22, child: Image.asset('assets/images/coin.png'), ) ], ), const SizedBox(height: 30), AnimatedOpacity( duration: const Duration(milliseconds: 1000), curve: Curves.easeInOutExpo, opacity: checkDialog(isDialogVisible), child: MyServiceField( type: TextInputType.number, controller: quantityController, hint: 'Quantity', onChange: (fieldData) { var fieldNum = int.parse(fieldData); totalPrice.value = fieldNum * priceOfOne; }, ), ), const SizedBox(height: 10), MyServiceField( type: TextInputType.emailAddress, controller: linkController, hint: 'Link', ), const SizedBox(height: 49), GestureDetector( onTap: () { Navigator.pop(context); setState(() { isDialogVisible = false; }); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( icon: const Icon( Icons.error_outline, color: Colors.black, ), title: const Text("Notice"), alignment: Alignment.center, contentTextStyle: const TextStyle(color: Colors.grey), content: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), child: const Text( "Your order has been placed and will be completed within 2 to 10 Hours.", style: TextStyle(), ), ), ], ), actions: [ Center( child: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Container( width: 200, height: 50, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.all( Radius.circular(10))), child: const Center( child: Text( 'Ok', style: TextStyle(color: Colors.white), )), ), )) ], ); }); }, child: Container( width: 200, height: 50, decoration: const BoxDecoration( color: Colors.black, borderRadius: BorderRadius.all(Radius.circular(30))), child: const Center( child: Text( 'Place Order', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), ), ) ], ), ), ), ), ), ], ), ); }
关键修改点
- 合并Hero容器:把背景容器和内容组件放在同一个Hero里,确保布局变化同步,避免挤压。
- 动态高度约束:用
BoxConstraints设置弹窗的最大高度为屏幕的70%,最小高度保持450,适配不同屏幕和键盘状态。 - 优化ScrollView:去掉ConstrainedBox的maxHeight限制,添加padding避免内容贴边,Column设置
mainAxisSize: MainAxisSize.min让内容高度自适应。
内容的提问来源于stack exchange,提问作者Blek Bomor
相关产品推荐
相关产品推荐

