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

如何解决Flutter中ContextMenuController搭配AdaptiveTextSelectionToolbar的溢出问题?

解决ContextMenuController搭配AdaptiveTextSelectionToolbar的绘制溢出问题

直接使用SingleChildScrollView包裹AdaptiveTextSelectionToolbar会丢失原有的尺寸约束,导致菜单组件试图占满全屏。解决核心是先给菜单设置最大高度限制,再嵌套滚动组件,既保留自适应尺寸,又支持滚动。

具体实现步骤

  1. 用ConstrainedBox限制菜单最大高度(比如设为屏幕高度的60%,避免遮挡过多内容);
  2. 在ConstrainedBox内部嵌套SingleChildScrollView,指定垂直滚动方向;
  3. 将原AdaptiveTextSelectionToolbar.buttonItems作为滚动组件的子项。

修改后的关键代码片段

替换原contextMenuBuilder中的返回内容:

return ConstrainedBox(
  constraints: BoxConstraints(
    maxHeight: MediaQuery.of(context).size.height * 0.6,
  ),
  child: SingleChildScrollView(
    child: AdaptiveTextSelectionToolbar.buttonItems(
      anchors: TextSelectionToolbarAnchors(
        primaryAnchor: offset,
      ),
      buttonItems: <ContextMenuButtonItem>[
        // 你的菜单项列表
        ...
      ],
    ),
  ),
);

完整修改后的示例代码

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const ContextMenuControllerExampleApp());

/// A builder that includes an Offset to draw the context menu at.
typedef ContextMenuBuilder = Widget Function(
    BuildContext context, Offset offset);

class ContextMenuControllerExampleApp extends StatefulWidget {
  const ContextMenuControllerExampleApp({super.key});

  @override
  State<ContextMenuControllerExampleApp> createState() =>
      _ContextMenuControllerExampleAppState();
}

class _ContextMenuControllerExampleAppState
    extends State<ContextMenuControllerExampleApp> {
  void _showDialog(BuildContext context) {
    Navigator.of(context).push(
      DialogRoute<void>(
        context: context,
        builder: (BuildContext context) =>
            const AlertDialog(title: Text('You clicked print!')),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    // On web, disable the browser's context menu since this example uses a custom
    // Flutter-rendered context menu.
    if (kIsWeb) {
      BrowserContextMenu.disableContextMenu();
    }
  }

  @override
  void dispose() {
    if (kIsWeb) {
      BrowserContextMenu.enableContextMenu();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Context menu outside of text'),
        ),
        body: _ContextMenuRegion(
          contextMenuBuilder: (BuildContext context, Offset offset) {
            // 修改后的菜单结构:添加高度约束和滚动组件
            return ConstrainedBox(
              constraints: BoxConstraints(
                maxHeight: MediaQuery.of(context).size.height * 0.6,
              ),
              child: SingleChildScrollView(
                child: AdaptiveTextSelectionToolbar.buttonItems(
                  anchors: TextSelectionToolbarAnchors(
                    primaryAnchor: offset,
                  ),
                  buttonItems: <ContextMenuButtonItem>[
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                    ContextMenuButtonItem(
                      onPressed: () {
                        ContextMenuController.removeAny();
                        _showDialog(context);
                      },
                      label: 'Print',
                    ),
                  ],
                ),
              ),
            );
          },
          // In this case this wraps a big open space in a GestureDetector in
          // order to show the context menu, but it could also wrap a single
          // widget like an Image to give it a context menu.
          child: ListView(
            children: <Widget>[
              Container(height: 20.0),
              const Text(
                  'Right click (desktop) or long press (mobile) anywhere, not just on this text, to show the custom menu.'),
            ],
          ),
        ),
      ),
    );
  }
}

/// Shows and hides the context menu based on user gestures.
///
/// By default, shows the menu on right clicks and long presses.
class _ContextMenuRegion extends StatefulWidget {
  /// Creates an instance of [_ContextMenuRegion].
  const _ContextMenuRegion({
    required this.child,
    required this.contextMenuBuilder,
  });

  /// Builds the context menu.
  final ContextMenuBuilder contextMenuBuilder;

  /// The child widget that will be listened to for gestures.
  final Widget child;

  @override
  State<_ContextMenuRegion> createState() => _ContextMenuRegionState();
}

class _ContextMenuRegionState extends State<_ContextMenuRegion> {
  Offset? _longPressOffset;

  final ContextMenuController _contextMenuController = ContextMenuController();

  static bool get _longPressEnabled {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
        return true;
      case TargetPlatform.macOS:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return false;
    }
  }

  void _onSecondaryTapUp(TapUpDetails details) {
    _show(details.globalPosition);
  }

  void _onTap() {
    if (!_contextMenuController.isShown) {
      return;
    }
    _hide();
  }

  void _onLongPressStart(LongPressStartDetails details) {
    _longPressOffset = details.globalPosition;
  }

  void _onLongPress() {
    assert(_longPressOffset != null);
    _show(_longPressOffset!);
    _longPressOffset = null;
  }

  void _show(Offset position) {
    _contextMenuController.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return widget.contextMenuBuilder(context, position);
      },
    );
  }

  void _hide() {
    _contextMenuController.remove();
  }

  @override
  void dispose() {
    _hide();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onSecondaryTapUp: _onSecondaryTapUp,
      onTap: _onTap,
      onLongPress: _longPressEnabled ? _onLongPress : null,
      onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
      child: widget.child,
    );
  }
}

原理说明

  • ConstrainedBox给菜单设定最大高度,避免菜单无限制扩展;
  • SingleChildScrollView在菜单内容超过最大高度时提供滚动能力;
  • 这种嵌套方式不会破坏AdaptiveTextSelectionToolbar的自适应宽度特性,菜单仍会根据内容长度自动调整宽度。

内容的提问来源于stack exchange,提问作者F F

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 14:23:16