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:

  1. The submenu items are not clickable - clicks seem to pass through to elements behind the submenu
  2. 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