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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 19:31:02