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

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组件锚定位置,菜单会自动跟随锚点元素移动,无需手动计算坐标也不会出现错位,核心实现逻辑:

  1. 为需要弹出菜单的按钮绑定LayerLink,外层包裹CompositedTransformTarget
  2. 自定义下拉菜单组件,外层包裹CompositedTransformFollower绑定同一个LayerLink,通过Overlay插入到页面中
  3. 尺寸变化时框架会自动同步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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 15:21:00