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

Flutter中Modal Bottom Sheet内SizedBox滚动消失问题求解决方案

解决Modal Bottom Sheet全屏时CustomScrollView顶部Sliver滚动消失的问题

当Modal Bottom Sheet设置isScrollControlled: true实现全屏后,CustomScrollView向下滚动时,顶部用于占位AppBar高度的SliverToBoxAdapter会随滚动消失——这是因为它属于滚动内容的一部分,会被滚出视口。以下是两种可行的解决方案:

方案1:将顶部占位移到CustomScrollView外部

把AppBar高度的SizedBox从CustomScrollView的slivers列表中移出,作为BottomSheet的直接子元素,和CustomScrollView同级。这样占位区域会固定在顶部,不会随滚动移动。

修改后的代码示例:

// BottomSheet的子结构改为Column包裹
Column(
  children: [
    // 固定的顶部占位,不随滚动消失
    SizedBox(
      height: AppBar().preferredSize.height,
    ),
    Expanded(
      child: CustomScrollView(
        controller: scrollController,
        slivers: [
          SliverSheetBar(
            title: tr('invoice_steps.step1.branch.title'),
            searchHint: tr('invoice_steps.step1.branch.search_hint'),
            onSearch: (searchTerm) {
              page = 1;
              if (searchTerm.isNotEmpty) {
                this.searchTerm = searchTerm;
              } else {
                this.searchTerm = null;
              }
              scrollController.moveToTop();
              controller.add(GetBranches(page, limit, searchTerm));
            },
            sheetItemsLength: state is BranchesSuccess
                ? tr(
                    'invoice_steps.step1.branch.total_number',
                    args: [
                      replaceWithFarsiNumber(
                        context,
                        state.totalNumOfBranches.toString(),
                      ),
                    ],
                  )
                : null,
          ),
          const SliverToBoxAdapter(
            child: SizedBox(
              height: 7.0,
            ),
          ),
          SliverToBoxAdapter(
            child: Builder(
              builder: (context) {
                if (state is BranchesSuccess) {
                  noNextPages = state.noNextPages;
                  if (!noNextPages!) {
                    page++;
                  }
                  return state.branches.isNotEmpty
                      ? ListView.builder(
                          shrinkWrap: true,
                          itemCount: state.branches.length + 1,
                          physics: const NeverScrollableScrollPhysics(),
                          itemBuilder: (context, index) {
                            return buildItem(state, index);
                          },
                        )
                      : searchTerm == null
                          ? EmptyList(
                              emptyMessage: tr(
                                  'invoice_steps.step1.branch.empty_message'),
                            )
                          : NoSearchResults(
                              message: tr(
                                  'invoice_steps.step1.branch.no_suggestions'),
                            );
                } else if (state is BranchesFailure) {
                  return ErrorMessage(
                    errorMessage: state.failureMessage,
                  );
                }
                return const RadioListTileShimmer();
              },
            ),
          ),
        ],
      ),
    ),
  ],
)

方案2:用SliverPersistentHeader固定顶部占位

如果必须将占位区域放在CustomScrollView内部,可以用SliverPersistentHeader替代SliverToBoxAdapter,通过设置pinned: true让它始终固定在视口顶部。

修改后的代码示例:

CustomScrollView(
  controller: scrollController,
  slivers: [
    SliverPersistentHeader(
      pinned: true, // 固定在顶部不滚动
      delegate: _SizedBoxHeaderDelegate(
        height: AppBar().preferredSize.height,
      ),
    ),
    // 以下sliver内容保持不变
    SliverSheetBar(
      title: tr('invoice_steps.step1.branch.title'),
      searchHint: tr('invoice_steps.step1.branch.search_hint'),
      onSearch: (searchTerm) {
        page = 1;
        if (searchTerm.isNotEmpty) {
          this.searchTerm = searchTerm;
        } else {
          this.searchTerm = null;
        }
        scrollController.moveToTop();
        controller.add(GetBranches(page, limit, searchTerm));
      },
      sheetItemsLength: state is BranchesSuccess
          ? tr(
              'invoice_steps.step1.branch.total_number',
              args: [
                replaceWithFarsiNumber(
                  context,
                  state.totalNumOfBranches.toString(),
                ),
              ],
            )
          : null,
    ),
    const SliverToBoxAdapter(
      child: SizedBox(
        height: 7.0,
      ),
    ),
    SliverToBoxAdapter(
      child: Builder(
        builder: (context) {
          if (state is BranchesSuccess) {
            noNextPages = state.noNextPages;
            if (!noNextPages!) {
              page++;
            }
            return state.branches.isNotEmpty
                ? ListView.builder(
                    shrinkWrap: true,
                    itemCount: state.branches.length + 1,
                    physics: const NeverScrollableScrollPhysics(),
                    itemBuilder: (context, index) {
                      return buildItem(state, index);
                    },
                  )
                : searchTerm == null
                    ? EmptyList(
                        emptyMessage: tr(
                            'invoice_steps.step1.branch.empty_message'),
                      )
                    : NoSearchResults(
                        message: tr(
                            'invoice_steps.step1.branch.no_suggestions'),
                      );
          } else if (state is BranchesFailure) {
            return ErrorMessage(
              errorMessage: state.failureMessage,
            );
          }
          return const RadioListTileShimmer();
        },
      ),
    ),
  ],
)

// 自定义SliverPersistentHeaderDelegate类
class _SizedBoxHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double height;

  _SizedBoxHeaderDelegate({required this.height});

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox(height: height);
  }

  @override
  double get maxExtent => height;

  @override
  double get minExtent => height;

  @override
  bool shouldRebuild(_SizedBoxHeaderDelegate oldDelegate) {
    return height != oldDelegate.height;
  }
}

方案1实现简单,适合不需要将占位与滚动内容绑定的场景;方案2则适合必须在CustomScrollView内部处理顶部占位的情况,按需选择即可。

内容的提问来源于stack exchange,提问作者Haya Hesham Abdel-azim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 03:03:17