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

Flutter中保持ModalBottomSheet不关闭,实现背景页面元素交互

Flutter实现弹窗常驻时背景地图与顶部按钮可交互

需求说明

需要实现以下功能:

  • BookRideScreen页面加载完成后自动显示底部弹窗
  • 弹窗保持显示状态时,用户可点击页面顶部的两个按钮,同时能与背景中的GoogleMap进行交互

关键修改思路

原实现使用的showModalBottomSheet属于模态路由,会阻塞下层页面的交互。因此需要将弹窗改为页面Stack布局的一部分,而非独立的模态路由,以此实现背景与弹窗的同时交互。


一、修改后的底部弹窗组件

import ...

class RideBottomPopupSheet extends ConsumerStatefulWidget {
  const RideBottomPopupSheet({
    super.key,
    required this.content,
    required this.actionVisible,
    this.onDone,
  });

  final Widget content;
  final VoidCallback? onDone;
  final bool actionVisible;

  @override
  ConsumerState createState() => _RideBottomPopupSheetState();
}

class _RideBottomPopupSheetState extends ConsumerState<RideBottomPopupSheet> {
  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: 0,
      right: 0,
      bottom: 0,
      child: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(10.0)
              .copyWith(bottom: MediaQuery.of(context).viewInsets.bottom + 40),
          child: Container(
            clipBehavior: Clip.antiAlias,
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.all(
                Radius.circular(20),
              ),
              color: Colors.white,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  decoration: const BoxDecoration(
                    borderRadius: BorderRadius.all(
                      Radius.circular(20),
                    ),
                    color: Colors.white,
                  ),
                  width: double.maxFinite,
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8),
                    child: widget.content,
                  ),
                ),
                if (widget.actionVisible) ...[
                  CustomContainer(
                    borderRadius: 0,
                    width: double.maxFinite,
                    child: TextButton(
                      style: TextButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor: Constants.primaryColor,
                      ),
                      clipBehavior: Clip.hardEdge,
                      onPressed: widget.onDone,
                      child: Text(
                        'Continue',
                        overflow: TextOverflow.fade,
                        style: GoogleFonts.inter(
                          fontSize: 16,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }
}

二、修改后的BookRideScreen页面

class BookRideScreen extends ConsumerStatefulWidget {
  const BookRideScreen({super.key});

  @override
  ConsumerState createState() => _BookRideScreenState();
}

class _BookRideScreenState extends ConsumerState<BookRideScreen> {
  StateProvider<int> terminalSelected = StateProvider((ref) => 1);
  // 控制弹窗显示状态,页面加载完成默认显示
  final ValueNotifier<bool> _showPopup = ValueNotifier(true);

  @override
  void initState() {
    super.initState();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  }

  @override
  void dispose() {
    _showPopup.dispose();
    super.dispose();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
        overlays: SystemUiOverlay.values);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          const GoogleMap(
            initialCameraPosition: CameraPosition(
              target: GoogleMapsConstants.terminal1Location,
              zoom: 18,
            ),
            // 开启地图交互控件
            zoomControlsEnabled: true,
            myLocationEnabled: true,
          ),
          Positioned(
            top: MediaQuery.of(context).viewPadding.top,
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton(
                  onPressed: () {
                    // 添加按钮点击逻辑
                  },
                  clipBehavior: Clip.antiAlias,
                  style: ElevatedButton.styleFrom(
                    padding: EdgeInsets.zero,
                    foregroundColor: const Color(0xff344054),
                    backgroundColor: Colors.white,
                    shape: const CircleBorder(),
                  ),
                  child: SvgPicture.asset('assets/svgs/uyufjdx.svg'),
                ),
                const SizedBox(width: 5),
                ElevatedButton(
                  onPressed: () {
                    // 添加按钮点击逻辑
                  },
                  clipBehavior: Clip.antiAlias,
                  style: ElevatedButton.styleFrom(
                    foregroundColor: const Color(0xff344054),
                    backgroundColor: Colors.white,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(50.0),
                    ),
                  ),
                  child: SvgPicture.asset('assets/svgs/gutdw.svg'),
                ),
              ],
            ),
          ),
          // 通过ValueNotifier控制弹窗显示/隐藏
          ValueListenableBuilder<bool>(
            valueListenable: _showPopup,
            builder: (context, show, child) {
              if (!show) return const SizedBox.shrink();
              return RideBottomPopupSheet(
                content: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    /// 终端1选项
                    Padding(
                      padding: const EdgeInsets.only(left: 18.0),
                      child: InkWell(
                        onTap: () {
                          ref.read(terminalSelected.notifier).update((state) => 1);
                        },
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: [
                            Consumer(builder: (context, ref, child) {
                              return Checkbox(
                                value: ref.watch(terminalSelected) == 1,
                                materialTapTargetSize: MaterialTapTargetSize.padded,
                                activeColor: Constants.primaryColor,
                                shape: const CircleBorder(),
                                side: const BorderSide(
                                  width: 2,
                                  style: BorderStyle.solid,
                                  color: Color(0xffd9d9d9),
                                ),
                                onChanged: (bool? value) {
                                  if (value == true) {
                                    ref.read(terminalSelected.notifier).update((state) => 1);
                                  }
                                },
                              );
                            }),
                            Expanded(
                              child: ListTile(
                                contentPadding: EdgeInsets.zero,
                                title: Text(
                                  'Terminal 1',
                                  style: GoogleFonts.inter(
                                    fontSize: 18,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                                subtitle: Text(
                                  'Pick Up Point',
                                  style: GoogleFonts.inter(
                                    fontSize: 12,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                    const Divider(
                      color: Color(0xffd0d5dd),
                      thickness: 1.8,
                    ),
                    /// 终端2选项
                    Padding(
                      padding: const EdgeInsets.only(left: 18.0),
                      child: InkWell(
                        onTap: () {
                          ref.read(terminalSelected.notifier).update((state) => 2);
                        },
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: [
                            Consumer(builder: (context, ref, child) {
                              return Checkbox(
                                value: ref.watch(terminalSelected) == 2,
                                materialTapTargetSize: MaterialTapTargetSize.padded,
                                activeColor: Constants.primaryColor,
                                shape: const CircleBorder(),
                                side: const BorderSide(
                                  width: 2,
                                  style: BorderStyle.solid,
                                  color: Color(0xffd9d9d9),
                                ),
                                onChanged: (bool? value) {
                                  if (value == true) {
                                    ref.read(terminalSelected.notifier).update((state) => 2);
                                  }
                                },
                              );
                            }),
                            Expanded(
                              child: ListTile(
                                contentPadding: EdgeInsets.zero,
                                title: Text(
                                  'Terminal 2',
                                  style: GoogleFonts.inter(
                                    fontSize: 18,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                                subtitle: Text(
                                  'Pick Up Point',
                                  style: GoogleFonts.inter(
                                    fontSize: 12,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                    const Divider(
                      color: Color(0xffd0d5dd),
                      thickness: 1.8,
                    ),
                    /// 终端3选项
                    Padding(
                      padding: const EdgeInsets.only(left: 18.0),
                      child: InkWell(
                        onTap: () {
                          ref.read(terminalSelected.notifier).update((state) => 3);
                        },
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: [
                            Consumer(builder: (context, ref, child) {
                              return Checkbox(
                                value: ref.watch(terminalSelected) == 3,
                                materialTapTargetSize: MaterialTapTargetSize.padded,
                                activeColor: Constants.primaryColor,
                                shape: const CircleBorder(),
                                side: const BorderSide(
                                  width: 2,
                                  style: BorderStyle.solid,
                                  color: Color(0xffd9d9d9),
                                ),
                                onChanged: (bool? value) {
                                  if (value == true) {
                                    ref.read(terminalSelected.notifier).update((state) => 3);
                                  }
                                },
                              );
                            }),
                            Expanded(
                              child: ListTile(
                                contentPadding: EdgeInsets.zero,
                                title: Text(
                                  'Terminal 3',
                                  style: GoogleFonts.inter(
                                    fontSize: 18,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                                subtitle: Text(
                                  'Pick Up Point',
                                  style: GoogleFonts.inter(
                                    fontSize: 12,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff344054),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
                onDone: () {
                  // 点击Continue按钮可隐藏弹窗
                  _showPopup.value = false;
                },
                actionVisible: true,
              );
            },
          ),
        ],
      ),
    );
  }
}

修改要点说明

  1. 移除模态路由:删除原ridePopUp方法,将弹窗改为Positioned组件嵌入页面Stack,避免阻塞背景交互。
  2. 弹窗状态控制:用ValueNotifier管理弹窗显示状态,页面初始化时默认开启,满足加载完成自动弹出需求。
  3. 保障地图交互:开启GoogleMap的交互控件,弹窗仅占据底部区域,不影响地图其他区域操作。
  4. 顶部按钮交互:顶部按钮位于Stack上层,未被弹窗遮挡,可正常响应点击事件。

内容的提问来源于stack exchange,提问作者Aadarsh Pandey

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 01:14:54