Flutter升级3.22.3后ShowModalBottomSheet顶部溢出问题
Flutter 3.22.3升级后ShowModalBottomSheet顶部溢出问题
问题背景
需求为让showModalBottomSheet始终显示在屏幕上半部分,Flutter 3.22.3版本升级前显示正常,但升级后BottomSheet出现顶部溢出异常。
期望效果
模态底部弹窗始终显示在屏幕上半部分。
异常表现
弹窗内容超出屏幕顶部,出现布局溢出警告。
相关代码
最初的弹窗实现代码
var ret = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) { return Container( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Text("Just a title"), ConstrainedBox( constraints: new BoxConstraints( maxHeight: MediaQuery.of(context).size.height / 4.0 * 3.0, ), child: AmountPopup( scrollController: scrollController, amount: model, autoFocusAmount: needAutoFocus, draggableScrollableController: draggableScrollableController!, onChangeSize: (size) { print(size); }, )), ], )); });
实际使用的弹窗显示方法
Future<void> showAmountPopup(BuildContext context, {AppAmountModel? model, bool needAutoFocus = false}) async { ScrollController scrollController = ScrollController(); var ret = await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12)), ), builder: ((context) { final ratio = DeviceHelper.logicalHeight / DeviceHelper.logicalWidth; if (ratio < 1) { return DraggableScrollableSheet( initialChildSize: getMinimumFractional(), minChildSize: getMinimumFractional(), maxChildSize: getMaximumFractional(), controller: draggableScrollableController, snap: true, expand: false, builder: (context, scrollController) { return AmountPopup( autoFocusAmount: needAutoFocus, scrollController: scrollController, amount: model, draggableScrollableController: draggableScrollableController!, onChangeSize: (size) { print(size); }, ); }, ); } else { return Wrap( children: [ AmountPopup( scrollController: scrollController, amount: model, autoFocusAmount: needAutoFocus, draggableScrollableController: draggableScrollableController!, onChangeSize: (size) { print(size); }, ) ], ); } })); if (ret == true) { isChange = true; needDescription = false; needImage = false; } scrollController.dispose(); update(); }
AmountPopup布局代码
ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12)), child: Container( color: Colors.white, padding: EdgeInsets.fromLTRB(16, layout.systemPadding.top + 16, 16, MediaQuery.of(context).viewInsets.bottom), alignment: Alignment.topCenter, // height: _size?.height, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ topButtons(context), Flexible( child: GestureDetector( onTap: () { FocusManager.instance.primaryFocus?.unfocus(); }, child: ListView( shrinkWrap: true, // physics: _size == null ? NeverScrollableScrollPhysics() : AlwaysScrollableScrollPhysics(), controller: widget.scrollController, children: [ const SizedBox( height: 16, ), ///Title Text( Localizator().getString( type: eLocalizatorText.expenseType) ?? 'Expense type', style: styleProvider.getTextStyle( color: styleProvider.secondaryColor, fontSize: 14, height: 20 / 14, fontWeight: FontWeight.w400), ), const SizedBox( height: 8, ), /// Select Expense type AppItemPickerCustom<Widget>( key: Key(tempAmount!.expenseType?.title ?? ''), style: styleProvider, layout: layout, title: Localizator() .getString(type: eLocalizatorText.selectItem), itemsInit: tempAmount!.expenseType != null ? [ getAppExpenseTypes( tempAmount!.expenseType!, styleProvider) ] : null, items: expenseTypes! .map( (e) => getAppExpenseTypes(e, styleProvider)) .toList(), decorator: styleProvider.getBoxDecorator( color: styleProvider .getSecondary(eAppColor.color100), border: needExpenseType ? Border.all(color: colorError, width: 1) : null), rightIcon: AppIcons.angle_down( color: styleProvider.primaryColor, size: 16, ), leftIcon: tempAmount!.expenseType?.iconCode != null ? AppIcons( size: 20, color: styleProvider.primaryColor, code: tempAmount!.expenseType!.iconCode!, ) : null, onChoosed: (choosed) { updateSelectedExpenseType(choosed); isChange = true; update(); }), if (needExpenseType) getRequiredWidget(), if (tempAmount ?.expenseType?.entityDescription?.isNotEmpty ?? false) Padding( padding: const EdgeInsets.only(top: 16), child: Text( tempAmount!.expenseType!.entityDescription!, style: styleProvider.getTextStyle( color: styleProvider.black, fontSize: 14, height: 20 / 14), ), ), const SizedBox( height: 16, ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ /// Amount Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( Localizator().getString( type: eLocalizatorText.amount) ?? 'Amount', style: styleProvider.getTextStyle( color: styleProvider.secondaryColor, fontSize: 14, height: 20 / 14, fontWeight: FontWeight.w400), ), const SizedBox( height: 8, ), CommonFieldEditorWidget( key: Key(tempAmount!.amount.toString()), style: styleProvider.getTextStyle( color: Colors.black), textInputType: const TextInputType.numberWithOptions( decimal: true), inputFormatters: FormatterHelper.amountFormatter(), setValue: tempAmount!.amount != null ? NumFormatter.formatting( float: tempAmount!.amount) : null, focusNode: focusNodeAmount, onChange: (text) { tempAmount = tempAmount! .copy(amount: double.tryParse(text)); isChange = true; }, onSubmitted: (text) { tempAmount = tempAmount! .copy(amount: double.tryParse(text)); update(); }, onTap: () {}, inputDecoration: styleProvider.getFieldDecoration( '', ), boxDecoration: styleProvider.getBoxDecorator( border: needAmount ? Border.all( color: colorError, width: 1) : null), ), if (needAmount) getRequiredWidget(), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( Localizator().getString( type: eLocalizatorText.currency) ?? 'Currency', style: styleProvider.getTextStyle( color: styleProvider.secondaryColor, fontSize: 14, height: 20 / 14, fontWeight: FontWeight.w400), ), const SizedBox( height: 8, ), DisabledMask( disabled: currencyDisabled, child: AppItemPicker( key: Key(tempAmount!.currency?.code ?? 'CHF'), style: styleProvider, layout: layout, title: Localizator().getString( type: eLocalizatorText.selectItem), itemsInit: [ tempAmount!.currency?.code ?? 'CHF' ], items: availableCurrencies .map((e) => e.code) .toList(), decorator: styleProvider.getBoxDecorator( color: styleProvider.getSecondary( eAppColor.color100), border: needCurrency ? Border.all( color: colorError, width: 1) : null), rightIcon: AppIcons.angle_down( color: styleProvider.primaryColor, size: 16, ), onChoosed: (choosed) { if (choosed.isNotEmpty) { tempAmount = tempAmount!.copy( currency: availableCurrencies .firstWhereOrNull( (element) => choosed.first == element.code)); isChange = true; update(); } }), ), if (needCurrency) getRequiredWidget() ], ), ) ], ), const SizedBox(height: 16), ...costObjectHelper.getCostObjectSelector( style: styleProvider, externHorzPadding: 16 * 2, layout: layout, onChangeTypeOfCostObject: (p0) { isChange = true; setCostObjectHelper(type: p0); update(); }, ), // const SizedBox( // height: 16, // ), ...costObjectHelper.getCostObjectWidget( key: UniqueKey(), style: styleProvider, onChangeItem: (type, main, position) { if (_entryCostCentr == null || _entryCostCentr?.key != type) { _entryCostCentr = MapEntry( type, List<AppCostItem?>.filled(2, null)); } _entryCostCentr!.value[0] = main; _entryCostCentr!.value[1] = position; if (type == ePageCostItem.costCentr) { if (main != null) { tempAmount = tempAmount!.copy( costCenter: main, clearPSP: true, clearHomeCostCenter: true, clearSalesOrder: true); } if (position != null) { tempAmount = tempAmount!.copy( costCenterPosition: position, clearPSP: true, clearHomeCostCenter: true, clearSalesOrder: true); } } if (type == ePageCostItem.salesOrder) { if (main != null) { tempAmount = tempAmount!.copy( salesOrder: main, clearCostCenter: true, clearHomeCostCenter: true, clearPSP: true); } if (position != null) { tempAmount = tempAmount!.copy( salesOrderPosition: position, clearCostCenter: true, clearHomeCostCenter: true, clearPSP: true); } } if (type == ePageCostItem.psp) { if (main != null) { tempAmount = tempAmount!.copy( psp: main, clearCostCenter: true, clearHomeCostCenter: true, clearSalesOrder: true); } } if (type == ePageCostItem.homeCostCenter) { if (main != null) { tempAmount = tempAmount!.copy( homeCostCenter: main, clearCostCenter: true, clearSalesOrder: true, clearPSP: true); } } isChange = true; update(); }, ), SizedBox( height: MediaQuery.of(context).padding.bottom + 32, ), ], ), ), ), ], ), )),
解决方案
Flutter 3.22.x版本对showModalBottomSheet的安全区域处理和布局约束逻辑做了调整,可通过以下方式修复:
1. 统一使用DraggableScrollableSheet约束高度
在竖屏分支(ratio >=1)中替换Wrap为DraggableScrollableSheet,通过maxChildSize固定弹窗最大高度为屏幕一半,避免内容溢出:
else { return DraggableScrollableSheet( initialChildSize: 0.5, // 初始占屏幕一半 minChildSize: 0.5, maxChildSize: 0.5, // 限制最大高度为屏幕一半 expand: false, builder: (context, scrollController) { return AmountPopup( scrollController: scrollController, amount: model, autoFocusAmount: needAutoFocus, draggableScrollableController: draggableScrollableController!, onChangeSize: (size) { print(size); }, ); }, ); }
2. 修正AmountPopup的安全区域内边距
由于showModalBottomSheet已设置useSafeArea: true,手动添加的layout.systemPadding.top会导致内容上移溢出,移除该部分:
padding: EdgeInsets.fromLTRB(16, 16, 16, MediaQuery.of(context).viewInsets.bottom),
3. 移除冗余约束
如果已通过DraggableScrollableSheet限制高度,可移除最初代码中的ConstrainedBox,避免多重约束冲突。
4. 验证滚动控制器绑定
确保AmountPopup内的ListView正确使用传入的scrollController,避免布局计算错误。
内容的提问来源于stack exchange,提问作者user9365729
相关产品推荐
相关产品推荐

