import 'dart:math'; import 'package:flutter/material.dart'; class AutoCompleteDropdownButton extends StatefulWidget { final String label; final T? selectedItem; final List items; final void Function(T? value) onChanged; final List Function(String? value)? applyQuery; final TextEditingController controller; const AutoCompleteDropdownButton( {Key? key, this.selectedItem, required this.label, required this.items, required this.onChanged, this.applyQuery, required this.controller}) : super(key: key); @override _AutoCompleteDropdownButtonState createState() => _AutoCompleteDropdownButtonState(); } class _AutoCompleteDropdownButtonState extends State> { late List options; late List suggestions; final FocusNode focusNode = FocusNode(); final LayerLink layerLink = LayerLink(); OverlayEntry? entry; bool isOpen = false; @override void initState() { super.initState(); setState(() { options = widget.items; suggestions = []; }); } @override void dispose() { focusNode.dispose(); super.dispose(); } void toggleOverlay() { isOpen ? hideOverlay() : showOverlay(); } void showOverlay() { hideOverlay(); List items = []; Divider? divider; ScrollController _scrollController = ScrollController(); final overlay = Overlay.of(context)!; final renderBox = context.findRenderObject() as RenderBox; if (widget.selectedItem != null) { items.add(buildListTile(widget.selectedItem!)); } if (suggestions.isNotEmpty) { items.addAll(suggestions .where((item) => item != widget.selectedItem) .map((item) => buildListTile(item))); } if ((widget.selectedItem != null || suggestions.isNotEmpty) && (options.length - suggestions.length - (widget.selectedItem == null ? 0 : 1) > 0)) { divider = const Divider(height: 10); items.add(divider); } items.addAll(options .where((item) => !(widget.selectedItem != null && item.toString() == widget.selectedItem!.toString()) && !suggestions.contains(item)) .map((item) => buildListTile(item)) .toList()); final screenHeight = MediaQuery.of(context).size.height; final neededHeight = renderBox.size.height * (items.length - 1) + (divider?.height ?? renderBox.size.height); final availableHeight = screenHeight - (renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height); bool displayAbove = neededHeight > availableHeight && screenHeight - availableHeight > availableHeight; final height = min(neededHeight, max(availableHeight, screenHeight - availableHeight - 55) - 55); entry = OverlayEntry( builder: (context) => Positioned( width: renderBox.size.width, height: height, child: CompositedTransformFollower( link: layerLink, targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft, followerAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft, offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)), showWhenUnlinked: false, child: Scrollbar( controller: _scrollController, // thumbVisibility: true, isAlwaysShown: true, child: Material( elevation: 8, child: SingleChildScrollView( controller: _scrollController, child: Column( children: items, ), ), ), ), ), ), ); overlay.insert(entry!); isOpen = true; } ListTile buildListTile(T item) { return ListTile( onTap: () => handleChanged(item), selected: item == widget.selectedItem, title: Row( children: [ Flexible( child: Text(item.toString()), ), ], ), ); } void hideOverlay() { entry?.remove(); setState(() { entry = null; isOpen = false; }); } void handleChanged(T? item) { widget.onChanged(item); hideOverlay(); } void onChangeQuery(String value) { if (value.trim() == '' || (widget.selectedItem != null && value == widget.selectedItem!.toString())) { setState(() { suggestions = []; }); } else { if (widget.applyQuery == null) { setState(() { suggestions = widget.items.where((item) { String itemString = item.toString().toLowerCase(); String valueLowercase = value.toLowerCase(); return itemString.contains(valueLowercase); }).toList(); }); } else { setState(() { suggestions = widget.applyQuery!(value) .where((item) => item != widget.selectedItem) .toList(); }); } showOverlay(); } } @override Widget build(BuildContext context) { return Focus( focusNode: focusNode, onFocusChange: (isFocused) { if (!isFocused) { hideOverlay(); } }, child: CompositedTransformTarget( link: layerLink, child: TextFormField( onChanged: onChangeQuery, onTap: toggleOverlay, controller: widget.controller, decoration: InputDecoration( labelText: widget.label, suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ widget.selectedItem != null ? IconButton( onPressed: () => handleChanged(null), icon: const Icon(Icons.close), iconSize: 20.0, ) : Container(), IconButton( onPressed: toggleOverlay, icon: const Icon(Icons.arrow_drop_down), ), ], ), ), ), ), ); } }