如何使用Flutter实现与参考示例效果一致的自定义popup menu弹出菜单
实现思路
- 放弃系统默认的
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
相关产品推荐
相关产品推荐

