admin管理员组文章数量:1124566
I'm implementing a navigation bar with a hoverable submenu in Flutter. When hovering over the "HOW IT WORKS" menu item, a submenu appears, but I'm encountering two issues:
- The submenu items are not clickable - clicks seem to pass through to elements behind the submenu
- The hover states and menu closing behavior are inconsistent
- The submenu should open when "HOW IT WORKS" is hovered, and should remain open if the users cursor is on the submenu
- The submenu should close if "HOW IT WORKS" is not being hovered, the buffer zone is not being hovered, and the sub menu is not being hovered.
Here's the relevant code for the submenu implementation:
bool _isMenuOpen = false;
bool _isHovering = {};
bool _isSubmenuHovered = false;
final GlobalKey _howItWorksKey = GlobalKey();
final GlobalKey _submenuKey = GlobalKey();
Offset _howItWorksPosition = Offset.zero;
double _howItWorksWidth = 0;
Widget _buildNavItem(String text, {bool isDrawer = false}) {
final routes = AuthStateWidget.of(context).cachedRoutes;
final dynamic routeData = routes[text];
if (isDrawer) return _buildNavItemInDrawer(text);
return MouseRegion(
onEnter: (_) {
print('NavItem Enter: $text');
setState(() {
_isHovering[text] = true;
if (text == 'HOW IT WORKS') {
// Get position after state update to ensure layout is complete
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_howItWorksKey.currentContext != null) {
final RenderBox box = _howItWorksKey.currentContext!.findRenderObject() as RenderBox;
setState(() {
_howItWorksPosition = box.localToGlobal(Offset.zero);
_howItWorksWidth = box.size.width;
_isMenuOpen = true;
});
print('HOW IT WORKS position set to: $_howItWorksPosition, width: $_howItWorksWidth');
}
});
} else {
_isMenuOpen = false;
_isHovering['HOW IT WORKS'] = false;
}
});
print('After Enter - isHovering: ${_isHovering[text]}, isMenuOpen: $_isMenuOpen');
},
onExit: (event) {
print('NavItem Exit: $text at ${event.position}');
if (text != 'HOW IT WORKS') {
setState(() => _isHovering[text] = false);
}
print('After Exit - isHovering: ${_isHovering[text]}, isMenuOpen: $_isMenuOpen');
},
child: Container(
key: text == 'HOW IT WORKS' ? _howItWorksKey : null,
child: TextButton(
onPressed: () {
if (routeData is Map<String, dynamic>) {
Navigator.pushNamed(context, routeData['route'] as String);
} else if (routeData is String) {
Navigator.pushNamed(context, routeData);
}
},
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black87),
overlayColor: MaterialStateProperty.all(Colors.transparent),
padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 8)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
text,
style: GoogleFonts.montserrat(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: const Color(0xFF0A12FF),
),
),
const SizedBox(height: 4),
_buildUnderline(text),
],
),
),
),
);
}
Widget _buildSubMenuItem(String text, String route) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
print('Clicked: $text -> $route'); // Debug print
Navigator.of(context).pushNamed(route);
},
onHover: (hovering) {
print('Hovering over $text: $hovering'); // Debug hover
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AspectRatio(
aspectRatio: 16/9,
child: Container(
color: Colors.grey[200],
child: Center(
child: Text(
text,
style: GoogleFonts.montserrat(
fontSize: 14,
fontWeight: FontWeight.w500,
color: const Color(0xFF0A12FF),
),
),
),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final bool isMobile = screenWidth < 1024;
final authState = AuthStateWidget.of(context);
final routes = authState.cachedRoutes;
final isAuthenticated = authState.isAuthenticated;
_handleLayoutChange(isMobile);
return Stack(
clipBehavior: Clip.none, // Add this
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9E3FC),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isMobile) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.menu, size: 24),
onPressed: () {
Scaffold.of(context).openDrawer();
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
iconSize: 24,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
],
),
Expanded(
child: Center(
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: constraints.maxWidth,
height: 40,
child: Center(
child: _buildLogoWithText(
logoHeight: 32,
fontSize: screenWidth < 400 ? 24 : 32,
),
),
);
},
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (screenWidth > 350)
_buildButton('GET STARTED'),
if (screenWidth > 350)
const SizedBox(width: 8),
if (isAuthenticated)
_buildProfileMenu()
else if (screenWidth > 350)
_buildButton('LOGIN', isPrimary: true),
const SizedBox(width: 4),
_buildCartIcon(isMobile: true),
],
),
] else ...[
SizedBox(
height: 80,
child: _buildLogoWithText(
logoHeight: 40,
fontSize: 40,
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: routes.keys
.map((text) => _buildNavItem(text))
.toList(),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isAuthenticated) ...[
_buildProfileMenu(),
] else ...[
_buildButton('GET STARTED'),
const SizedBox(width: 8),
_buildButton('LOGIN', isPrimary: true),
],
_buildCartIcon(isMobile: false),
],
),
],
],
),
),
),
if (_isMenuOpen)
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
// Buffer zone
if (_howItWorksKey.currentContext != null)
Positioned(
top: (_howItWorksKey.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero).dy + 30,
left: (_howItWorksKey.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero).dx,
width: (_howItWorksKey.currentContext!.findRenderObject() as RenderBox)
.size.width,
height: 150,
child: MouseRegion(
onEnter: (_) {
print('Buffer Zone Enter');
setState(() {
_isMenuOpen = true;
_isHovering['HOW IT WORKS'] = true;
});
},
onExit: (_) {
print('Buffer Zone Exit');
},
child: Container(
color: Colors.red.withOpacity(0.2), // Debug visibility
),
),
),
// Submenu
Positioned(
top: 77,
left: 0,
right: 0,
child: MouseRegion(
onEnter: (_) {
print('Submenu Enter');
setState(() {
_isMenuOpen = true;
_isHovering['HOW IT WORKS'] = true;
});
},
onExit: (event) {
print('Submenu Exit at: ${event.position}');
// Get actual submenu content bounds
final submenuBox = _submenuKey.currentContext?.findRenderObject() as RenderBox?;
if (submenuBox != null) {
final submenuPos = submenuBox.localToGlobal(Offset.zero);
final submenuHeight = submenuBox.size.height;
final submenuBottom = submenuPos.dy + submenuHeight;
print('Submenu content bounds: top=${submenuPos.dy}, bottom=$submenuBottom');
// Get buffer zone bounds
final bufferBox = _howItWorksKey.currentContext?.findRenderObject() as RenderBox?;
final bufferArea = bufferBox != null
? Rect.fromLTWH(
bufferBox.localToGlobal(Offset.zero).dx,
bufferBox.localToGlobal(Offset.zero).dy + 30,
bufferBox.size.width,
150
)
: null;
bool isInBufferZone = bufferArea?.contains(event.position) ?? false;
bool isBelowSubmenu = event.position.dy > submenuBottom;
print('Cursor Y: ${event.position.dy}');
print('Is in buffer zone: $isInBufferZone');
print('Is below submenu: $isBelowSubmenu');
if (isBelowSubmenu || (!isInBufferZone && event.position.dy > 77)) {
print('Closing submenu');
setState(() {
_isMenuOpen = false;
_isHovering['HOW IT WORKS'] = false;
});
}
}
},
child: Material(
elevation: 4,
color: const Color(0xFFD9E3FC),
child: Container(
key: _submenuKey, // Add key here to measure actual content
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 48),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (var subItem in {
'Our Process': '/how-it-works',
'Homekin Designers': '/designers',
'Find Your Style': '/style-finder',
'Reviews': '/reviews'
}.entries)
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _buildSubMenuItem(subItem.key, subItem.value),
),
),
],
),
),
),
),
),
],
),
),
)
],
);
}
What I've tried:
- Added Material widget as parent
- Implemented MouseRegion for hover detection
- Added debug prints to track hover states
- Attempted to create buffer zones for smoother hover transitions
- What I notice is the second my cursor goes below a height of 80.3 pixels it wants to close the sub menu even if my cursor is hovering the submenu
- I feel as though there is something I'm missing when implementing the sub menu functionality
The hover detection works initially for hovering "HOW IT WORKS" but the sub menu does not close after, the interaction with submenu items doesn't work as expected as well as the submenu items can't be clicked. How can I fix the click-through issue and fix the hover behaviour? Thanks so much!
本文标签: widgetFlutter Implement a hover submenu in an appbarStack Overflow
版权声明:本文标题:widget - Flutter: Implement a hover submenu in an app-bar - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736638883a1945947.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论