Flutter中无需Future.delayed同时处理SearchDropdown焦点与点击
解决Flutter自定义SearchDropdown焦点与下拉项选择冲突问题
问题分析
你当前用Future.delayed延迟隐藏下拉,本质是因为点击下拉项时,文本框先失焦触发_handleFocusChange,导致下拉被提前隐藏,点击事件无法被捕获。这种依赖延迟的方式不仅不优雅,还可能因设备性能差异出现问题。
最佳实践方案
核心思路是区分点击区域:点击下拉列表内部时正常处理选中逻辑,点击外部区域才隐藏下拉。同时调整焦点与下拉显示的绑定逻辑,具体实现如下:
1. 全局点击监听
在创建OverlayEntry时,用GestureDetector包裹下拉内容和全局遮罩:
- 下拉区域内部点击:触发项选中逻辑
- 下拉区域外部点击:隐藏下拉并移除焦点
2. 调整焦点处理逻辑
移除_handleFocusChange中的延迟,改为:
- 获焦时直接显示下拉
- 失焦时不立即隐藏,交给全局点击事件处理(避免点击下拉项时误触发隐藏)
3. 统一状态管理
封装_hideDropdown方法,集中处理下拉隐藏与Overlay移除逻辑,避免状态不一致
修改后的完整代码片段
class SearchDropdown extends StatefulWidget { final List<SearchDropdownItem> items; final Function(String) onTextChanged; final Function(dynamic) onItemSelected; final String selectedTextValue; final String? Function(String?)? validator; final String? headerText; const SearchDropdown({ super.key, required this.items, required this.onTextChanged, required this.onItemSelected, required this.selectedTextValue, this.headerText, this.validator, }); @override _SearchDropdownState createState() => _SearchDropdownState(); } class _SearchDropdownState extends State<SearchDropdown> { final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); bool _isShowDropdown = false; OverlayEntry? _overlayEntry; final LayerLink _layerLink = LayerLink(); void _onTextChanged() { widget.onTextChanged(_controller.text); } void _clearText() { _controller.clear(); _onTextChanged(); _handleItemSelected("", null); } void _handleItemSelected(String title, dynamic value) { _controller.text = title; widget.onItemSelected(value); _focusNode.unfocus(); _hideDropdown(); } @override void initState() { super.initState(); _controller.text = widget.selectedTextValue; _controller.addListener(_onTextChanged); _focusNode.addListener(_handleFocusChange); } void _handleFocusChange() { if (_focusNode.hasFocus) { if (mounted) { setState(() { _isShowDropdown = true; }); } } // 失焦时不立即隐藏,交给全局点击处理 } void _showDropdown() { if (_overlayEntry != null) { _overlayEntry!.remove(); } _overlayEntry = _createOverlayEntry(); Overlay.of(context).insert(_overlayEntry!); } void _hideDropdown() { if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; } if (mounted) { setState(() { _isShowDropdown = false; }); } } @override void dispose() { _controller.dispose(); _focusNode.dispose(); _hideDropdown(); // 销毁时主动移除Overlay super.dispose(); } OverlayEntry _createOverlayEntry() { RenderBox renderBox = context.findRenderObject() as RenderBox; var size = renderBox.size; var offset = renderBox.localToGlobal(Offset.zero); return OverlayEntry( builder: (context) => Stack( children: [ // 全局透明遮罩,点击外部区域隐藏下拉 GestureDetector( onTap: () => _hideDropdown(), child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, color: Colors.transparent, ), ), Positioned( left: offset.dx, top: offset.dy + size.height, width: size.width, child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, border: Border.all(color: Theme.of(context).colorScheme.onSurface), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(UIConstants.radius), bottomRight: Radius.circular(UIConstants.radius), ), ), child: Material( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(UIConstants.radius), bottomRight: Radius.circular(UIConstants.radius), ), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200, minHeight: 0), child: ListView( shrinkWrap: true, padding: EdgeInsets.zero, children: widget.items.map((item) { return ListTile( title: Text(item.title), onTap: () { _handleItemSelected(item.title, item.value); }, ); }).toList(), ), ), ), ), ), ), ], ), ); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { _isShowDropdown ? _showDropdown() : _hideDropdown(); }); return CompositedTransformTarget( link: _layerLink, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.headerText != null) ...[ Text(widget.headerText ?? "", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 5), ], TextFormField( controller: _controller, focusNode: _focusNode, validator: widget.validator, decoration: const InputDecoration( border: OutlineInputBorder(), suffixIcon: Icon(Icons.arrow_drop_down), ), // 补充你原本代码中TextFormField的其他属性 ), ], ), ); } } // 补充SearchDropdownItem定义 class SearchDropdownItem { final String title; final dynamic value; SearchDropdownItem({required this.title, required this.value}); }
关键修改说明
- 新增全局透明遮罩,点击下拉外部区域时触发隐藏逻辑
- 移除
_handleFocusChange中的延迟代码,避免点击项时误隐藏下拉 - 封装
_hideDropdown统一处理下拉隐藏与Overlay移除,保证状态一致性 - 组件销毁时主动清理Overlay,避免内存泄漏
内容的提问来源于stack exchange,提问作者Johnny
相关产品推荐
相关产品推荐

