Flutter:如何向下拉菜单列表添加搜索TextField与TextButton两项?
在Flutter下拉菜单中添加搜索框和按钮的实现方案
Flutter默认的DropdownButton无法直接嵌入输入框和按钮这类交互组件,我们可以通过两种方式实现高度定制的下拉菜单,满足需求:
方式一:使用PopupMenuButton自定义弹出内容
通过PopupMenuButton的itemBuilder可以灵活组合搜索框、按钮和选项列表,代码示例如下:
import 'package:flutter/material.dart'; class CustomDropdown extends StatefulWidget { const CustomDropdown({super.key}); @override State<CustomDropdown> createState() => _CustomDropdownState(); } class _CustomDropdownState extends State<CustomDropdown> { // 模拟下拉选项数据 final List<String> _options = ['选项1', '选项2', '选项3', '选项4', '选项5']; String? _selectedOption; final TextEditingController _searchController = TextEditingController(); List<String> _filteredOptions = []; @override void initState() { super.initState(); _filteredOptions = _options; _searchController.addListener(_filterOptions); } void _filterOptions() { final query = _searchController.text.toLowerCase(); setState(() { _filteredOptions = _options .where((option) => option.toLowerCase().contains(query)) .toList(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return PopupMenuButton<String>( // 自定义下拉触发按钮 child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(_selectedOption ?? '请选择'), const SizedBox(width: 8), const Icon(Icons.arrow_drop_down), ], ), ), // 构建弹出菜单内容 itemBuilder: (context) { return [ // 搜索输入框 PopupMenuItem( enabled: false, // 禁用点击事件,避免干扰输入 child: TextField( controller: _searchController, decoration: const InputDecoration( hintText: '搜索选项', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), ), ), // 自定义按钮 PopupMenuItem( enabled: false, child: TextButton( onPressed: () { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('自定义按钮被点击')), ); }, child: const Text('新增选项'), ), ), // 分割线 const PopupMenuDivider(), // 过滤后的选项列表 ..._filteredOptions.map((option) { return PopupMenuItem<String>( value: option, child: Text(option), onTap: () { setState(() { _selectedOption = option; }); }, ); }), ]; }, ); } } // 调用示例 void main() { runApp(const MaterialApp( home: Scaffold( body: Center(child: CustomDropdown()), ), )); }
关键说明:
- 用
PopupMenuItem包裹搜索框和按钮时,设置enabled: false避免触发菜单的默认点击关闭逻辑 - 为搜索框添加文本监听,实时过滤选项列表
- 可完全自定义触发按钮的样式,替代默认外观
方式二:使用showMenu完全自定义弹出布局
如果需要更灵活的布局控制(比如调整组件间距、滚动行为),可以直接调用showMenu方法传入自定义布局:
import 'package:flutter/material.dart'; class CustomDropdown2 extends StatefulWidget { const CustomDropdown2({super.key}); @override State<CustomDropdown2> createState() => _CustomDropdown2State(); } class _CustomDropdown2State extends State<CustomDropdown2> { final List<String> _options = ['选项1', '选项2', '选项3', '选项4', '选项5']; String? _selectedOption; final TextEditingController _searchController = TextEditingController(); List<String> _filteredOptions = []; @override void initState() { super.initState(); _filteredOptions = _options; _searchController.addListener(_filterOptions); } void _filterOptions() { final query = _searchController.text.toLowerCase(); setState(() { _filteredOptions = _options .where((option) => option.toLowerCase().contains(query)) .toList(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _showCustomMenu() async { final RenderBox button = context.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final Offset position = button.localToGlobal( Offset.zero, ancestor: overlay, ); await showMenu( context: context, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, position.dx + button.size.width, position.dy + button.size.height + 400, // 限制菜单最大高度 ), items: [], // 留空,使用自定义child child: Container( width: button.size.width, constraints: const BoxConstraints(maxHeight: 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 搜索框 Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: const InputDecoration( hintText: '搜索选项', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), ), ), // 自定义按钮 TextButton( onPressed: () { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('新增选项被点击')), ); }, child: const Text('新增选项'), ), const Divider(height: 1), // 可滚动的选项列表 Expanded( child: ListView.builder( shrinkWrap: true, itemCount: _filteredOptions.length, itemBuilder: (context, index) { final option = _filteredOptions[index]; return ListTile( title: Text(option), onTap: () { setState(() { _selectedOption = option; }); Navigator.pop(context); }, ); }, ), ), ], ), ), ); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _showCustomMenu, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(_selectedOption ?? '请选择'), const SizedBox(width: 8), const Icon(Icons.arrow_drop_down), ], ), ), ); } } // 调用示例 void main() { runApp(const MaterialApp( home: Scaffold( body: Center(child: CustomDropdown2()), ), )); }
关键说明:
- 通过
showMenu的child参数传入完全自定义的布局,自由组合各类组件 - 手动计算菜单弹出位置,确保显示在触发按钮下方
- 使用
ListView.builder处理大量选项的滚动需求,提升性能
内容的提问来源于stack exchange,提问作者arcthurus
相关产品推荐
相关产品推荐

