You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 13:02:33