如何解决Flutter中ContextMenuController搭配AdaptiveTextSelectionToolbar的溢出问题?
直接使用SingleChildScrollView包裹AdaptiveTextSelectionToolbar会丢失原有的尺寸约束,导致菜单组件试图占满全屏。解决核心是先给菜单设置最大高度限制,再嵌套滚动组件,既保留自适应尺寸,又支持滚动。
具体实现步骤
- 用
ConstrainedBox限制菜单最大高度(比如设为屏幕高度的60%,避免遮挡过多内容); - 在
ConstrainedBox内部嵌套SingleChildScrollView,指定垂直滚动方向; - 将原
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
相关产品推荐
相关产品推荐

