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

如何使用Flutter实现与参考示例效果一致的自定义popup menu弹出菜单

Flutter 自定义 PopupMenu 弹出菜单实现教程

实现思路

  • 放弃系统默认的PopupMenuButton组件,该组件样式限制较多,难以实现高度自定义的效果
  • 采用showGeneralDialog方法挂载自定义菜单层,可自由控制菜单的弹出位置、动画效果、样式风格
  • 通过GlobalKey获取触发按钮的位置信息,计算菜单的弹出偏移量,保证菜单对齐触发按钮

完整实现代码

import 'package:flutter/material.dart';

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

  @override
  State<CustomPopupMenuDemo> createState() => _CustomPopupMenuDemoState();
}

class _CustomPopupMenuDemoState extends State<CustomPopupMenuDemo> {
  // 触发按钮的GlobalKey,用于获取位置
  final GlobalKey _triggerKey = GlobalKey();

  // 展示自定义菜单
  void _showCustomPopup() async {
    // 获取触发按钮的位置信息
    RenderBox renderBox = _triggerKey.currentContext!.findRenderObject() as RenderBox;
    Offset offset = renderBox.localToGlobal(Offset.zero);
    double triggerWidth = renderBox.size.width;
    double triggerHeight = renderBox.size.height;

    await showGeneralDialog(
      context: context,
      barrierDismissible: true,
      barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
      barrierColor: Colors.transparent,
      transitionDuration: const Duration(milliseconds: 150),
      pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) {
        return Stack(
          children: [
            // 菜单定位在触发按钮下方,可根据需求调整偏移
            Positioned(
              left: offset.dx,
              top: offset.dy + triggerHeight + 4,
              child: Material(
                color: Colors.transparent,
                child: Container(
                  width: 180,
                  padding: const EdgeInsets.symmetric(vertical: 8),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(8),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(0.1),
                        blurRadius: 10,
                        spreadRadius: 2,
                      )
                    ],
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      _buildMenuItem(Icons.edit, "编辑", () {
                        Navigator.of(context).pop();
                        // 处理编辑逻辑
                      }),
                      _buildMenuItem(Icons.share, "分享", () {
                        Navigator.of(context).pop();
                        // 处理分享逻辑
                      }),
                      const Divider(height: 1, thickness: 1),
                      _buildMenuItem(Icons.delete, "删除", () {
                        Navigator.of(context).pop();
                        // 处理删除逻辑
                      }, textColor: Colors.red),
                    ],
                  ),
                ),
              ),
            )
          ],
        );
      },
      transitionBuilder: (context, animation, secondary, child) {
        // 弹出动画:渐显+从上往下滑入
        return FadeTransition(
          opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
          child: SlideTransition(
            position: Tween<Offset>(begin: const Offset(0, -0.1), end: Offset.zero).animate(animation),
            child: child,
          ),
        );
      },
    );
  }

  // 封装菜单项
  Widget _buildMenuItem(IconData icon, String text, VoidCallback onTap, {Color? textColor}) {
    return InkWell(
      onTap: onTap,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        child: Row(
          children: [
            Icon(icon, size: 20, color: textColor ?? Colors.black87),
            const SizedBox(width: 12),
            Text(
              text,
              style: TextStyle(fontSize: 14, color: textColor ?? Colors.black87),
            )
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("自定义菜单演示"),
        actions: [
          // 触发按钮,绑定GlobalKey
          IconButton(
            key: _triggerKey,
            icon: const Icon(Icons.more_vert),
            onPressed: _showCustomPopup,
          )
        ],
      ),
    );
  }
}

效果调整说明

  • 如需修改菜单宽度、背景色、圆角等属性,直接修改Container的decoration对应参数即可
  • 菜单项的布局、图标文字样式都可以自由修改,支持添加选中态、角标等自定义元素
  • 如果需要菜单弹出在触发按钮的上方、左侧、右侧,只要修改Positioned的left/top参数的计算逻辑即可
  • 动画效果也可以自定义,比如修改为缩放弹出、从侧边滑入等,调整transitionBuilder的动画逻辑即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 06:36:00