Fluent UI中MenuFlyoutSubItem在非按钮组件上消失问题求助
问题现象
在Fluent UI框架中,MenuFlyoutSubItem与按钮组件搭配时可正常展开子菜单,但和ShaderMask+FlutterLogo这类非标准组件结合时,悬停MenuFlyoutSubItem能显示侧边子菜单,但移动光标后子菜单立即消失,无法进行点击操作。
代码复现
child: GestureDetector( onTapUp: (d) { final targetContext = contextAttachKey.currentContext; if (targetContext == null) return; final box = targetContext.findRenderObject() as RenderBox; final position = box.localToGlobal( d.localPosition, ancestor: Navigator.of(context).context.findRenderObject(), ); void showFlyout(Offset position) { contextController.showFlyout( barrierColor: Colors.black.withOpacity(0.4), position: position, barrierRecognizer: TapGestureRecognizer() ..onTap = () { Navigator.of(context).pop(); } ..onSecondaryTapUp = (d) { Navigator.of(context).pop(); final box = Navigator.of(context) .context .findRenderObject() as RenderBox; final position = box.localToGlobal( d.localPosition, ancestor: box, ); showFlyout(position); }, builder: (context) { return MenuFlyout(items: [ MenuFlyoutItem( leading: const Icon(FluentIcons.share), text: const Text('Share'), onPressed: Flyout.of(context).close, ), MenuFlyoutSubItem( text: const Text('Send to'), items: (_) => [ MenuFlyoutItem( text: const Text('Bluetooth'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Desktop (shortcut)'), onPressed: Flyout.of(context).close, ), MenuFlyoutSubItem( text: const Text('Compressed file'), items: (context) => [ MenuFlyoutItem( text: const Text('Compress and email'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Compress to .7z'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Compress to .zip'), onPressed: Flyout.of(context).close, ), ], ), ], ), ]); }, ); } showFlyout(position); }, child: FlyoutTarget( key: contextAttachKey, controller: contextController, child: ShaderMask( shaderCallback: (rect) { final color = context.read<AppTheme>().color.defaultBrushFor( FluentTheme.of(context).brightness, ); return LinearGradient( colors: [ color, color, ], ).createShader(rect); }, child: const FlutterLogo( size: 400.0, ), ), ), )
原因分析
- Flyout位置计算偏差:手动通过
localToGlobal计算的position未正确关联FlyoutTarget上下文,导致子菜单hover区域与父菜单交互逻辑错位,光标移动时误触发barrier关闭事件。 - Barrier手势识别逻辑缺陷:当前
barrierRecognizer会在任意点击时关闭Flyout,子菜单展开后,光标从父菜单项移向子菜单的过程中,容易触发barrier的误判定。 - 非标准组件上下文缺失:按钮组件内置Fluent UI交互上下文,而
ShaderMask这类自定义组件没有,导致MenuFlyoutSubItem的hover状态无法稳定维持。
解决方案
方案1:优化Flyout附着与Barrier识别逻辑
修改showFlyout实现,使用FlyoutTarget自带的附着定位,同时优化barrier手势识别,避免子菜单展开时误关闭:
void showFlyout() { contextController.showFlyout( barrierColor: Colors.black.withOpacity(0.4), // 依赖FlyoutTarget自动定位,替代手动计算 attachMode: FlyoutAttachMode.preferredPosition, barrierRecognizer: TapGestureRecognizer() ..onTap = (TapUpDetails d) { // 仅点击不在MenuFlyout区域内时关闭 final renderObject = context.findRenderObject(); if (renderObject is RenderBox) { final localPos = renderObject.globalToLocal(d.globalPosition); if (!renderObject.hitTest(BoxHitTestResult(), position: localPos)) { Navigator.of(context).pop(); } } else { Navigator.of(context).pop(); } } ..onSecondaryTapUp = (d) { Navigator.of(context).pop(); showFlyout(); }, builder: (context) { return MenuFlyout(items: [ MenuFlyoutItem( leading: const Icon(FluentIcons.share), text: const Text('Share'), onPressed: Flyout.of(context).close, ), MenuFlyoutSubItem( text: const Text('Send to'), items: (_) => [ MenuFlyoutItem( text: const Text('Bluetooth'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Desktop (shortcut)'), onPressed: Flyout.of(context).close, ), MenuFlyoutSubItem( text: const Text('Compressed file'), items: (context) => [ MenuFlyoutItem( text: const Text('Compress and email'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Compress to .7z'), onPressed: Flyout.of(context).close, ), MenuFlyoutItem( text: const Text('Compress to .zip'), onPressed: Flyout.of(context).close, ), ], ), ], ), ]); }, ); }
方案2:给自定义组件添加Fluent交互包装
将ShaderMask+FlutterLogo包装在ButtonStyleButton中,借用标准按钮的交互上下文:
child: FlyoutTarget( key: contextAttachKey, controller: contextController, child: ButtonStyleButton( onPressed: () {}, // 仅借用交互上下文,无需实际逻辑 style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.transparent), overlayColor: MaterialStateProperty.all(Colors.transparent), ), child: ShaderMask( shaderCallback: (rect) { final color = context.read<AppTheme>().color.defaultBrushFor( FluentTheme.of(context).brightness, ); return LinearGradient( colors: [ color, color, ], ).createShader(rect); }, child: const FlutterLogo( size: 400.0, ), ), ), ),
验证说明
方案1通过优化Flyout定位逻辑和barrier点击判定,避免光标移动时误触发关闭;方案2通过借用标准按钮的交互上下文,让自定义组件拥有与按钮一致的Flyout交互环境,两种方案均可解决子菜单快速消失的问题。
内容的提问来源于stack exchange,提问作者Yant
相关产品推荐
相关产品推荐

