Flutter Web中showMenu在窗口调整大小时不跟随锚点移动问题求解
问题根因
系统showMenu方法弹出的菜单本质是独立的路由,弹出时仅会根据你传入的一次性坐标计算位置,不会监听屏幕尺寸变化动态更新锚点坐标,所以窗口resize时锚点按钮位置发生变化后,菜单仍停留在旧坐标就会出现错位。
方案1:屏幕尺寸变化时自动隐藏菜单(实现成本最低,稳定性高)
通过WidgetsBindingObserver监听屏幕尺寸变化事件,触发时主动关闭已弹出的菜单,修改代码如下:
class _ZHomeMenuBarState extends State<ZHomeMenuBar> with WidgetsBindingObserver { final GlobalKey _mesKey = GlobalKey(); final GlobalKey _accKey = GlobalKey(); // 存储当前弹出的菜单路由 PopupRoute? _currentMenuRoute; @override void initState() { super.initState(); // 注册系统事件监听 WidgetsBinding.instance.addObserver(this); } @override void dispose() { // 移除监听 WidgetsBinding.instance.removeObserver(this); super.dispose(); } // 屏幕尺寸变化回调 @override void didChangeMetrics() { super.didChangeMetrics(); // 如果有已弹出的菜单,主动关闭 if (_currentMenuRoute?.isActive == true) { Navigator.of(context).removeRoute(_currentMenuRoute!); _currentMenuRoute = null; } } _showMessage(context) async { final RenderBox renderBox = _mesKey.currentContext?.findRenderObject() as RenderBox; final Size size = renderBox.size; final Offset offset = renderBox.localToGlobal(Offset.zero); // 拿到菜单路由实例 final menuRoute = showMenu( context: context, position: RelativeRect.fromLTRB(offset.dx, offset.dy + size.height, offset.dx + size.width, offset.dy + size.height), items: [ PopupMenuItem<String>(child: const Text('menu option 1'), value: '1'), PopupMenuItem<String>(child: const Text('menu option 2'), value: '2'), PopupMenuItem<String>(child: const Text('menu option 3'), value: '3'), ]); _currentMenuRoute = menuRoute as PopupRoute; // 菜单关闭后清空引用 await menuRoute.popped; _currentMenuRoute = null; } // 其余build等原有代码保持不变 @override Widget build(BuildContext context) { final zuser = Provider.of<ZUser?>(context); return Container( height: 66, decoration: BoxDecoration(color: context.backgroundColor), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Text(zuser == null ? "" : zuser.displayName ?? ""), const Spacer(), ZTextButton(text: "Portfolio", onPressed: () => {}), context.sh, ZTextButton( key: _mesKey, text: "Messages", onPressed: () { _showMessage(context); }), context.sh, ZTextButton(key: _accKey, text: "Account", onPressed: () {}), context.sh, ], ), ); } }
方案2:菜单始终跟随锚点移动(体验更好)
放弃系统showMenu的硬编码坐标定位方式,改用Flutter提供的CompositedTransformTarget+CompositedTransformFollower组件锚定位置,菜单会自动跟随锚点元素移动,无需手动计算坐标也不会出现错位,核心实现逻辑:
- 为需要弹出菜单的按钮绑定
LayerLink,外层包裹CompositedTransformTarget - 自定义下拉菜单组件,外层包裹
CompositedTransformFollower绑定同一个LayerLink,通过Overlay插入到页面中 - 尺寸变化时框架会自动同步Follower和Target的位置,无需额外处理
示例核心代码片段:
class _ZHomeMenuBarState extends State<ZHomeMenuBar> { final GlobalKey _mesKey = GlobalKey(); final GlobalKey _accKey = GlobalKey(); // 绑定锚点和菜单的链路对象 final LayerLink _messageLayerLink = LayerLink(); OverlayEntry? _menuOverlay; // 关闭菜单的通用方法 _closeMenu() { _menuOverlay?.remove(); _menuOverlay = null; } _showAutoFollowMenu() { // 点击其他区域关闭菜单的逻辑可根据需求自行补充 if(_menuOverlay != null) _closeMenu(); _menuOverlay = OverlayEntry( builder: (context) => CompositedTransformFollower( link: _messageLayerLink, showWhenUnlinked: false, // 调整菜单在按钮下方的偏移量 offset: const Offset(0, 48), child: Material( elevation: 8, child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('menu option 1'), onTap: _closeMenu ), ListTile( title: const Text('menu option 2'), onTap: _closeMenu ), ListTile( title: const Text('menu option 3'), onTap: _closeMenu ), ], ), ), ), ); Overlay.of(context).insert(_menuOverlay!); } @override Widget build(BuildContext context) { final zuser = Provider.of<ZUser?>(context); return Container( height: 66, decoration: BoxDecoration(color: context.backgroundColor), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Text(zuser == null ? "" : zuser.displayName ?? ""), const Spacer(), ZTextButton(text: "Portfolio", onPressed: () => {}), context.sh, // 给按钮包裹锚定目标组件 CompositedTransformTarget( link: _messageLayerLink, child: ZTextButton( key: _mesKey, text: "Messages", onPressed: () { _showAutoFollowMenu(); }), ), context.sh, ZTextButton(key: _accKey, text: "Account", onPressed: () {}), context.sh, ], ), ); } @override void dispose() { _closeMenu(); super.dispose(); } }
内容的提问来源于stack exchange,提问作者AlexK
相关产品推荐
相关产品推荐

