You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fluent UI中MenuFlyoutSubItem在非按钮组件上消失问题求助

问题: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,
                ),
              ),
            ),
          )

原因分析

  1. Flyout位置计算偏差:手动通过localToGlobal计算的position未正确关联FlyoutTarget上下文,导致子菜单hover区域与父菜单交互逻辑错位,光标移动时误触发barrier关闭事件。
  2. Barrier手势识别逻辑缺陷:当前barrierRecognizer会在任意点击时关闭Flyout,子菜单展开后,光标从父菜单项移向子菜单的过程中,容易触发barrier的误判定。
  3. 非标准组件上下文缺失:按钮组件内置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 20:04:52