Flutter实现鼠标悬停图标时显示下拉菜单的方案咨询
Flutter鼠标悬停图标弹出下拉菜单的优化实现
我想要在Flutter中实现鼠标悬停图标时弹出下拉菜单的功能,预期效果如下:

以下是我尝试的代码实现,但这个方案依赖遍历Widget树触发点击,不够理想,希望得到更合理的解决方法:
import 'package:flutter/material.dart'; import 'package:prove/responsive/responsive.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { GlobalKey popUpButtonKey = GlobalKey(); openPopUpItem() { GestureDetector? detector; searchForGestureDetector(BuildContext element) { element.visitChildElements((element) { if (element.widget != null && element.widget is GestureDetector) { detector = element.widget as GestureDetector; } else { searchForGestureDetector(element); } }); } searchForGestureDetector(popUpButtonKey.currentContext!); detector!.onTap!(); } @override Widget build(BuildContext context) { return Scaffold( endDrawer: Drawer( backgroundColor: const Color(0xFF262533), elevation: 10, child: Padding( padding: const EdgeInsets.all(18.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 30, ), PopupMenuButton( tooltip: '', child: Text( 'Escorts', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), Padding(padding: EdgeInsets.all(10.0)), PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Agenturen & Clubs', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Escortagenturen', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Bordelle', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Laufhauser', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Saunaclubs', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Domina & BDSM-Studios', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Tantra & Massaage-Studios', style: TextStyle(color: Colors.white), ), ), ), ]), Padding(padding: EdgeInsets.all(10.0)), PopupMenuButton( tooltip: '', child: Text( 'Inserieren', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), Padding(padding: EdgeInsets.all(10.0)), PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Werben', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Werbenformate', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Preise', style: TextStyle(color: Colors.white), ), ), ), ]), Padding(padding: EdgeInsets.all(10.0)), PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Blog', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Archiv', style: TextStyle(color: Colors.white), ), ), ), ]), const Padding( padding: EdgeInsets.all(10.0), ), PopupMenuButton( position: PopupMenuPosition.under, tooltip: '', child: const Text( 'Kontakt', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), ], ), ), ), backgroundColor: const Color(0xFF262533), body: ListView( children: [ Row( children: <Widget>[ const SizedBox( width: 20, ), SizedBox( height: 80, width: 185, child: Image.asset('assets/images/logo2.png'), ), Spacer(), if (!Responsive.isMobile(context)) PopupMenuButton( tooltip: '', child: Text( 'Escorts', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), Padding(padding: EdgeInsets.all(10.0)), if (!Responsive.isMobile(context)) PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Agenturen & Clubs', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Escortagenturen', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Bordelle', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Laufhauser', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Saunaclubs', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Domina & BDSM-Studios', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Tantra & Massaage-Studios', style: TextStyle(color: Colors.white), ), ), ), ]), Padding(padding: EdgeInsets.all(10.0)), if (!Responsive.isMobile(context)) PopupMenuButton( tooltip: '', child: Text( 'Inserieren', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), Padding(padding: EdgeInsets.all(10.0)), if (!Responsive.isMobile(context)) PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Werben', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Werbenformate', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Preise', style: TextStyle(color: Colors.white), ), ), ), ]), Padding(padding: EdgeInsets.all(10.0)), if (!Responsive.isMobile(context)) PopupMenuButton( tooltip: '', color: Color(0xFF262533), position: PopupMenuPosition.under, child: Text( 'Blog', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Archiv', style: TextStyle(color: Colors.white), ), ), ), ]), const Padding( padding: EdgeInsets.all(10.0), ), if (!Responsive.isMobile(context)) PopupMenuButton( position: PopupMenuPosition.under, tooltip: '', child: const Text( 'Kontakt', style: TextStyle( color: Colors.white, fontSize: 24, fontFamily: 'Poppins', ), ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[]), Spacer(), InkWell( onHover: (value) { if (value) openPopUpItem(); }, onTap: () {}, child: PopupMenuButton( key: popUpButtonKey, color: Color(0xFF262533), tooltip: '', position: PopupMenuPosition.under, child: Icon( Icons.person, color: Colors.white, ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: ListTile( title: Text( 'Login', style: TextStyle(color: Colors.white), ), ), ), const PopupMenuItem( child: ListTile( title: Text( 'Register', style: TextStyle(color: Colors.white), ), ), ), ], ),), const Padding( padding: EdgeInsets.all(10.0), ), PopupMenuButton( color: Color(0xFF262533), tooltip: '', position: PopupMenuPosition.under, child: Icon( Icons.search, color: Colors.white, ), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ PopupMenuItem( child: ListTile( title: TextField( decoration: InputDecoration( suffixIcon: Container( decoration: BoxDecoration( color: Colors.pink, borderRadius: BorderRadius.all( Radius.circular(8.0))), child: new IconButton( style: IconButton.styleFrom( shape: CircleBorder()), icon: new Icon( Icons.arrow_right, color: Colors.white, ), onPressed: (() {}), iconSize: 25, ), ), filled: true, //<-- SEE HERE fillColor: Colors.white, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10.0), borderSide: BorderSide( width: 1, color: Color(0xFFE51A72), ), ), hintText: 'Suchen...', hintStyle: TextStyle(fontSize: 15), ), ), ), ), ]), SizedBox( width: 20, ), if (!Responsive.isDesktop(context)) Builder( builder: (context) => IconButton( onPressed: () { Scaffold.of(context).openEndDrawer(); }, icon: Icon( Icons.menu, color: Colors.white, size: 30, ))), ], ) ], ), ); } }
优化方案
思路
放弃原方案中遍历Widget树触发点击的hack方式,改用Flutter原生API实现:
- 使用
MouseRegion监听鼠标的进入/离开事件 - 调用
showMenu方法手动弹出下拉菜单,或通过Overlay自定义菜单样式 - 状态管理控制菜单的显示与隐藏
代码实现(改造个人图标部分)
在_HomeScreenState中添加如下逻辑:
// 处理菜单显示 void _showUserMenu() async { final RenderBox button = context.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = RelativeRect.fromRect( Rect.fromPoints( button.localToGlobal(Offset.zero, ancestor: overlay), button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay), ), Offset.zero & overlay.size, ); await showMenu( context: context, position: position, color: const Color(0xFF262533), items: const [ PopupMenuItem( child: Text('Login', style: TextStyle(color: Colors.white)), value: 'login', ), PopupMenuItem( child: Text('Register', style: TextStyle(color: Colors.white)), value: 'register', ), ], ); }
然后替换原代码中InkWell包裹的PopupMenuButton部分:
MouseRegion( onEnter: (_) => _showUserMenu(), child: const Icon( Icons.person, color: Colors.white, ), )
方案优势
- 代码更可靠,不依赖Widget树内部结构的遍历
- 完全基于Flutter原生API实现,符合框架设计规范
- 可灵活定制菜单的样式、位置和交互逻辑
内容的提问来源于stack exchange,提问作者James James
相关产品推荐
相关产品推荐

