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, ); }, ), ], ), ); } }
修改要点说明
- 移除模态路由:删除原
ridePopUp方法,将弹窗改为Positioned组件嵌入页面Stack,避免阻塞背景交互。 - 弹窗状态控制:用
ValueNotifier管理弹窗显示状态,页面初始化时默认开启,满足加载完成自动弹出需求。 - 保障地图交互:开启GoogleMap的交互控件,弹窗仅占据底部区域,不影响地图其他区域操作。
- 顶部按钮交互:顶部按钮位于Stack上层,未被弹窗遮挡,可正常响应点击事件。
内容的提问来源于stack exchange,提问作者Aadarsh Pandey
相关产品推荐
相关产品推荐

