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

Flutter中能否实现嵌套式PopupMenu?

在Flutter中实现嵌套PopupMenu(Windows风格子菜单)

当然可以实现!Flutter本身没有原生的多级PopupMenu组件,但我们可以通过组合PopupMenuButton和手动调用showMenu方法,完美复刻Windows那种点击菜单项弹出子菜单的效果。

核心实现思路

  • 父级菜单用常规的PopupMenuButton创建,每个需要弹出子菜单的项,点击时不直接执行逻辑,而是触发子菜单的显示
  • 利用showMenu方法手动弹出子菜单,通过RelativeRect精准控制子菜单的位置,让它出现在父菜单项的右侧(和Windows样式一致)
  • 处理菜单的关闭逻辑,确保点击外部、选择菜单项后能正确关闭所有层级的菜单

完整代码示例

import 'package:flutter/material.dart';

class NestedPopupMenuDemo extends StatefulWidget {
  const NestedPopupMenuDemo({super.key});

  @override
  State<NestedPopupMenuDemo> createState() => _NestedPopupMenuDemoState();
}

class _NestedPopupMenuDemoState extends State<NestedPopupMenuDemo> {
  // 用于跟踪父菜单项的位置,来定位子菜单
  final GlobalKey _parentMenuItemKey = GlobalKey();

  void _showSubMenu() {
    final RenderBox renderBox = _parentMenuItemKey.currentContext!.findRenderObject() as RenderBox;
    final Offset offset = renderBox.localToGlobal(Offset.zero);
    final Size size = renderBox.size;

    // 计算子菜单的位置:在父菜单项的右侧,对齐顶部
    final RelativeRect position = RelativeRect.fromLTRB(
      offset.dx + size.width,
      offset.dy,
      MediaQuery.of(context).size.width - (offset.dx + size.width),
      MediaQuery.of(context).size.height - offset.dy,
    );

    showMenu(
      context: context,
      position: position,
      items: [
        const PopupMenuItem(
          value: 'sub_item1',
          child: Text('子菜单选项1'),
        ),
        const PopupMenuItem(
          value: 'sub_item2',
          child: Text('子菜单选项2'),
        ),
        const PopupMenuItem(
          value: 'sub_item3',
          child: Text('子菜单选项3'),
        ),
      ],
    ).then((value) {
      if (value != null) {
        // 处理子菜单选项的点击逻辑
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('你选择了:$value')),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('嵌套PopupMenu演示'),
        actions: [
          PopupMenuButton<String>(
            itemBuilder: (context) => [
              PopupMenuItem(
                key: _parentMenuItemKey,
                value: 'parent_item1',
                child: const Text('父菜单选项1'),
                onTap: () {
                  // 延迟执行:避免父菜单直接关闭导致子菜单无法弹出
                  WidgetsBinding.instance.addPostFrameCallback((_) => _showSubMenu());
                },
              ),
              const PopupMenuItem(
                value: 'parent_item2',
                child: Text('父菜单选项2'),
              ),
              const PopupMenuItem(
                value: 'parent_item3',
                child: Text('父菜单选项3'),
              ),
            ],
            onSelected: (value) {
              // 处理父菜单中不需要子菜单的项的逻辑
              if (value != 'parent_item1') {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('你选择了:$value')),
                );
              }
            },
          ),
        ],
      ),
      body: const Center(
        child: Text('点击右上角菜单查看嵌套效果'),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: NestedPopupMenuDemo()));
}

关键细节说明

  1. 菜单定位:通过GlobalKey获取父菜单项的渲染对象,计算它在屏幕上的位置,然后让子菜单显示在它的右侧,完全贴合Windows的视觉习惯
  2. 延迟执行:在PopupMenuItem的onTap中,我们需要用WidgetsBinding.instance.addPostFrameCallback来延迟显示子菜单,因为默认情况下点击PopupMenuItem会立即关闭父菜单,延迟执行可以确保子菜单正常弹出
  3. 逻辑分离:父菜单的onSelected处理不需要子菜单的项,子菜单的点击逻辑则在showMenu的then回调中处理,清晰区分不同层级的操作

优化建议

  • 可以自定义PopupMenuThemeData来调整菜单的样式,比如背景色、文字大小,让它更贴近Windows风格
  • 如果需要更深层级的菜单,可以重复这个逻辑,在子菜单的项中再次调用showMenu
  • 可以添加淡入淡出动画,让子菜单的弹出更平滑自然

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 21:19:06